diff --git a/README.md b/README.md index 0d060145..d79ce803 100644 --- a/README.md +++ b/README.md @@ -65,22 +65,19 @@ import os import uuid from sumup import Sumup -from sumup.checkouts import CreateCheckoutBody client = Sumup(api_key="sup_sk_MvxmLOl0...") merchant_code = os.environ["SUMUP_MERCHANT_CODE"] # Create a checkout checkout = client.checkouts.create( - body=CreateCheckoutBody( - amount=10.00, - currency="EUR", - checkout_reference=str(uuid.uuid4()), - merchant_code=merchant_code, - description="Test payment", - redirect_url="https://example.com/success", - return_url="https://example.com/webhook", - ) + amount=10.00, + currency="EUR", + checkout_reference=str(uuid.uuid4()), + merchant_code=merchant_code, + description="Test payment", + redirect_url="https://example.com/success", + return_url="https://example.com/webhook", ) print(f"Checkout ID: {checkout.id}") @@ -91,7 +88,6 @@ print(f"Checkout Reference: {checkout.checkout_reference}") ```python from sumup import Sumup -from sumup.readers import CreateReaderCheckoutBody, CreateReaderCheckoutBodyTotalAmount client = Sumup(api_key="sup_sk_MvxmLOl0...") @@ -99,15 +95,13 @@ client = Sumup(api_key="sup_sk_MvxmLOl0...") reader_checkout = client.readers.create_checkout( reader_id="your-reader-id", merchant_code="your-merchant-code", - body=CreateReaderCheckoutBody( - total_amount=CreateReaderCheckoutBodyTotalAmount( - value=1000, # 10.00 EUR (amount in cents) - currency="EUR", - minor_unit=2, - ), - description="Coffee purchase", - return_url="https://example.com/webhook", - ), + total_amount={ + "value": 1000, # 10.00 EUR (amount in cents) + "currency": "EUR", + "minor_unit": 2, + }, + description="Coffee purchase", + return_url="https://example.com/webhook", ) print(f"Reader checkout created: {reader_checkout}") diff --git a/codegen/pkg/builder/builder.go b/codegen/pkg/builder/builder.go index 011ef2e4..019c26d3 100644 --- a/codegen/pkg/builder/builder.go +++ b/codegen/pkg/builder/builder.go @@ -29,6 +29,8 @@ type Builder struct { // schemasByTag maps tags to respective schema references. schemasByTag map[string][]*base.SchemaProxy + // requestSchemasByTag maps tags to schemas referenced from request bodies/query params. + requestSchemasByTag map[string][]*base.SchemaProxy pathsByTag map[string]*v3.Paths @@ -58,10 +60,11 @@ func New(cfg Config, opts ...Option) *Builder { } b := &Builder{ - cfg: cfg, - schemasByTag: make(map[string][]*base.SchemaProxy), - pathsByTag: make(map[string]*v3.Paths), - templates: templates, + cfg: cfg, + schemasByTag: make(map[string][]*base.SchemaProxy), + requestSchemasByTag: make(map[string][]*base.SchemaProxy), + pathsByTag: make(map[string]*v3.Paths), + templates: templates, } for _, o := range opts { diff --git a/codegen/pkg/builder/collect.go b/codegen/pkg/builder/collect.go index ba70a141..2077192c 100644 --- a/codegen/pkg/builder/collect.go +++ b/codegen/pkg/builder/collect.go @@ -51,41 +51,51 @@ func (b *Builder) collectPaths() { func (b *Builder) collectSchemas() { // Map of schemas grouped by tag schemasByTag := make(map[string][]*base.SchemaProxy) + requestSchemasByTag := make(map[string][]*base.SchemaProxy) for path, pathItem := range b.spec.Paths.PathItems.FromOldest() { for method, op := range pathItem.GetOperations().FromOldest() { - c := make(SchemaProxyCollection, 0, 100) - c.collectSchemasInResponse(op) - c.collectSchemasInParams(op) - c.collectSchemasInRequest(op) - - for _, schema := range c { - if schema.GetReference() == "" { - continue - } - - if len(op.Tags) == 0 { - slog.Error("no tags for schema under operation", - slog.String("path", path), - slog.String("method", method), - slog.String("ref", schema.GetReference()), - ) - continue - } - - for _, tag := range op.Tags { - tagLower := strings.ToLower(tag) - if !slices.ContainsFunc(schemasByTag[tagLower], func(sp *base.SchemaProxy) bool { - return sp.GetReference() == schema.GetReference() - }) { - schemasByTag[tagLower] = append(schemasByTag[tagLower], schema) - } - } - } + allSchemas := make(SchemaProxyCollection, 0, 100) + allSchemas.collectSchemasInResponse(op) + allSchemas.collectSchemasInParams(op) + allSchemas.collectSchemasInRequest(op) + b.collectSchemasByTag(schemasByTag, path, method, op, allSchemas) + + requestSchemas := make(SchemaProxyCollection, 0, 100) + requestSchemas.collectSchemasInParams(op) + requestSchemas.collectSchemasInRequest(op) + b.collectSchemasByTag(requestSchemasByTag, path, method, op, requestSchemas) } } b.schemasByTag = schemasByTag + b.requestSchemasByTag = requestSchemasByTag +} + +func (b *Builder) collectSchemasByTag(dest map[string][]*base.SchemaProxy, path, method string, op *v3.Operation, schemas SchemaProxyCollection) { + for _, schema := range schemas { + if schema.GetReference() == "" { + continue + } + + if len(op.Tags) == 0 { + slog.Error("no tags for schema under operation", + slog.String("path", path), + slog.String("method", method), + slog.String("ref", schema.GetReference()), + ) + continue + } + + for _, tag := range op.Tags { + tagLower := strings.ToLower(tag) + if !slices.ContainsFunc(dest[tagLower], func(sp *base.SchemaProxy) bool { + return sp.GetReference() == schema.GetReference() + }) { + dest[tagLower] = append(dest[tagLower], schema) + } + } + } } type SchemaProxyCollection []*base.SchemaProxy diff --git a/codegen/pkg/builder/doc.go b/codegen/pkg/builder/doc.go index 80b08603..5ff22ab1 100644 --- a/codegen/pkg/builder/doc.go +++ b/codegen/pkg/builder/doc.go @@ -47,12 +47,6 @@ func methodDoc(operation *v3.Operation) string { return formatDoc(out.String()) } -// operationParamsDoc creates godoc comment for a struct representing -// parameters of an operation. -func operationParamsDoc(name string, operation *v3.Operation) string { - return formatDoc(name + ": query parameters for " + operation.OperationId) -} - // schemaDoc creates godoc for a schema. func schemaDoc(name string, schema *base.Schema) string { out := new(strings.Builder) @@ -191,5 +185,10 @@ func formatDoc(s string) string { } } - return strings.TrimSpace(out.String()) + return escapePythonDocString(strings.TrimSpace(out.String())) +} + +func escapePythonDocString(s string) string { + s = strings.ReplaceAll(s, "\\", "\\\\") + return s } diff --git a/codegen/pkg/builder/intermediate_representation.go b/codegen/pkg/builder/intermediate_representation.go index c81da55d..ac659929 100644 --- a/codegen/pkg/builder/intermediate_representation.go +++ b/codegen/pkg/builder/intermediate_representation.go @@ -19,11 +19,17 @@ type ClassDeclaration struct { Description string // AdditionalPropertiesType holds the value type for additional properties if enabled. AdditionalPropertiesType string + // RequestOnly marks request-only helper types that should not emit response models. + RequestOnly bool + // GenerateInput indicates this type needs an Input companion. + GenerateInput bool } type OneOfDeclaration struct { - Name string - Options []string + Name string + Options []string + RequestOnly bool + GenerateInput bool } // Property holds the information for Property of a type. @@ -50,6 +56,9 @@ type EnumDeclaration[E cmp.Ordered] struct { // Comment holds the description of the type Comment string Values []E + // RequestOnly marks request-only helper types that should only emit input aliases. + RequestOnly bool + GenerateInput bool } type Response struct { @@ -68,11 +77,24 @@ type TypeAlias struct { Type string // Comment holds the description of the type Comment string + // RequestOnly marks request-only helper types that should only emit input aliases. + RequestOnly bool + GenerateInput bool } func (ta *TypeAlias) String() string { buf := new(strings.Builder) + if ta.RequestOnly { + fmt.Fprintf(buf, "%sInput = %s\n", ta.Name, inputTypeName(ta.Type)) + if ta.Comment != "" { + fmt.Fprintf(buf, "'''\n%s\n'''\n", ta.Comment) + } + return buf.String() + } fmt.Fprintf(buf, "%s = %s\n", ta.Name, ta.Type) + if ta.GenerateInput { + fmt.Fprintf(buf, "%sInput = %s\n", ta.Name, inputTypeName(ta.Type)) + } if ta.Comment != "" { fmt.Fprintf(buf, "'''\n%s\n'''\n", ta.Comment) } diff --git a/codegen/pkg/builder/methods.go b/codegen/pkg/builder/methods.go index 231948ae..e1649289 100644 --- a/codegen/pkg/builder/methods.go +++ b/codegen/pkg/builder/methods.go @@ -36,7 +36,9 @@ type Method struct { ResponseType *string Path string PathParams []Parameter - QueryParams *Parameter + QueryFields []Property + BodyType string + BodyFields []Property HasBody bool Responses []Response } @@ -48,13 +50,72 @@ func (mt Method) ParamsString() string { res.WriteString(", ") res.WriteString(fmt.Sprintf("%s: %s", strcase.ToSnake(p.Name), p.Type)) } - if mt.QueryParams != nil { + needsKeywordOnly := mt.HasFlattenedBody() || len(mt.QueryFields) > 0 + if mt.HasFlattenedBody() { + if needsKeywordOnly { + res.WriteString(", *") + } + for _, p := range mt.BodyFields { + res.WriteString(", ") + res.WriteString(p.MethodParameterString(true)) + } + } else if mt.HasBody { res.WriteString(", ") - res.WriteString(fmt.Sprintf("%s: typing.Optional[%s] = None", strcase.ToSnake(mt.QueryParams.Name), mt.QueryParams.Type)) + res.WriteString(fmt.Sprintf("body: %sInput", mt.BodyType)) + } + if len(mt.QueryFields) > 0 { + if !mt.HasFlattenedBody() { + res.WriteString(", *") + } + for _, p := range mt.QueryFields { + res.WriteString(", ") + res.WriteString(p.MethodParameterString(false)) + } } return res.String() } +func (mt Method) HasFlattenedBody() bool { + return len(mt.BodyFields) > 0 +} + +func (mt Method) BodyInitString() string { + if !mt.HasFlattenedBody() { + return "" + } + + var buf strings.Builder + buf.WriteString("body_data: dict[str, typing.Any] = {}\n") + for _, field := range mt.BodyFields { + if field.Optional { + fmt.Fprintf(&buf, "if not isinstance(%s, NotGivenType):\n", field.FieldName()) + fmt.Fprintf(&buf, "\tbody_data[%q] = %s\n", field.WireName(), field.BodyArgumentExpr(true)) + } else { + fmt.Fprintf(&buf, "body_data[%q] = %s\n", field.WireName(), field.BodyArgumentExpr(true)) + } + } + + return buf.String() +} + +func (mt Method) QueryInitString() string { + if len(mt.QueryFields) == 0 { + return "" + } + + var buf strings.Builder + buf.WriteString("query_data: dict[str, typing.Any] = {}\n") + for _, field := range mt.QueryFields { + if field.Optional { + fmt.Fprintf(&buf, "if not isinstance(%s, NotGivenType) and %s is not None:\n", field.FieldName(), field.FieldName()) + fmt.Fprintf(&buf, "\tquery_data[%q] = %s\n", field.WireName(), field.BodyArgumentExpr(false)) + } else { + fmt.Fprintf(&buf, "query_data[%q] = %s\n", field.WireName(), field.BodyArgumentExpr(false)) + } + } + return buf.String() +} + // pathsToMethods converts openapi3 path to golang methods. func (b *Builder) pathsToMethods(paths *v3.Paths) ([]*Method, error) { allMethods := make([]*Method, 0, paths.PathItems.Len()) @@ -128,25 +189,18 @@ func (b *Builder) operationToMethod(method, path string, o *v3.Operation) (*Meth } hasBody := false + bodyType := "" if o.RequestBody != nil { mt, ok := o.RequestBody.Content.Get("application/json") if ok && mt.Schema != nil { - params = append(params, Parameter{ - Name: "body", - Type: strcase.ToCamel(o.OperationId) + "Body", - }) + bodyType = strcase.ToCamel(o.OperationId) + "Body" hasBody = true } } - var queryParams *Parameter - if slices.ContainsFunc(o.Parameters, func(p *v3.Parameter) bool { - return p.In != "path" && p.In != "header" - }) { - queryParams = &Parameter{ - Name: "params", - Type: strcase.ToCamel(o.OperationId) + "Params", - } + queryFields, err := b.buildQueryFields(o) + if err != nil { + return nil, fmt.Errorf("build query parameters: %w", err) } responses := make([]Response, 0, o.Responses.Codes.Len()) @@ -194,12 +248,41 @@ func (b *Builder) operationToMethod(method, path string, o *v3.Operation) (*Meth ResponseType: respType, Path: pathBuilder(path), PathParams: params, - QueryParams: queryParams, + QueryFields: queryFields, + BodyType: bodyType, HasBody: hasBody, Responses: responses, }, nil } +func (b *Builder) buildQueryFields(o *v3.Operation) ([]Property, error) { + if len(o.Parameters) == 0 { + return nil, nil + } + + fields := make([]Property, 0) + paramsTypeName := strcase.ToCamel(o.OperationId) + "Params" + for _, p := range o.Parameters { + if p.In == "path" || p.In == "header" { + continue + } + if p.Schema == nil { + return nil, fmt.Errorf("parameter %q has no schema", p.Name) + } + alias := p.Name + name := parameterFieldName(alias) + typeName, _ := b.genSchema(p.Schema, paramsTypeName+strcase.ToCamel(name)) + fields = append(fields, Property{ + Name: name, + SerializedName: alias, + Type: typeName, + Optional: p.Required == nil || !*p.Required, + Comment: parameterPropertyDoc(p.Schema.Schema()), + }) + } + return fields, nil +} + func (b *Builder) getSuccessResponseType(o *v3.Operation) (*string, error) { type responseInfo struct { content *v3.MediaType diff --git a/codegen/pkg/builder/out.go b/codegen/pkg/builder/out.go index 068f2b1f..e96ced00 100644 --- a/codegen/pkg/builder/out.go +++ b/codegen/pkg/builder/out.go @@ -29,7 +29,11 @@ func (b *Builder) generateSharedTypes() error { continue } typeName := strcase.ToCamel(name) - types = append(types, b.generateSchemaComponents(typeName, schema.Schema())...) + generated := b.generateSchemaComponents(typeName, schema.Schema()) + if b.isRequestSchema(typeName) { + markGenerateInput(generated) + } + types = append(types, generated...) } } usesSecret := usesSecretType(types) @@ -102,6 +106,7 @@ func (b *Builder) generateResourceIndex(tagName string, resourceTypes []string) type resourceTemplateData struct { PackageName string TypeNames []string + InputTypeNames []string Params []Writable Service string Methods []*Method @@ -127,6 +132,7 @@ func (b *Builder) generateResourceFile(tagName string, paths *v3.Paths) ([]strin if err != nil { return nil, fmt.Errorf("convert paths to methods: %w", err) } + flattenMethodBodies(methods, bodyTypes) slog.Info("generating file", slog.String("tag", tag.Name), @@ -142,6 +148,7 @@ func (b *Builder) generateResourceFile(tagName string, paths *v3.Paths) ([]strin if err := b.templates.ExecuteTemplate(serviceBuf, "resource.py.tmpl", resourceTemplateData{ PackageName: strcase.ToSnake(tag.Name), TypeNames: typeNames, + InputTypeNames: inputTypeNames(b.requestSchemaTypeNamesByTag(tagName)), Params: innerTypes, Service: strcase.ToCamel(tag.Name), Methods: methods, @@ -168,6 +175,9 @@ func (b *Builder) generateResourceFile(tagName string, paths *v3.Paths) ([]strin resourceTypes := make([]string, 0, len(innerTypes)) for _, t := range innerTypes { if typ, ok := t.(Type); ok { + if writableRequestOnly(t) { + continue + } resourceTypes = append(resourceTypes, typ.TypeName()) } } @@ -175,6 +185,31 @@ func (b *Builder) generateResourceFile(tagName string, paths *v3.Paths) ([]strin return resourceTypes, nil } +func flattenMethodBodies(methods []*Method, bodyTypes []Writable) { + bodyClasses := make(map[string]*ClassDeclaration) + for _, writable := range bodyTypes { + class, ok := writable.(*ClassDeclaration) + if !ok { + continue + } + if len(class.Fields) == 0 || class.AdditionalPropertiesType != "" { + continue + } + bodyClasses[class.Name] = class + } + + for _, method := range methods { + if !method.HasBody { + continue + } + class, ok := bodyClasses[method.BodyType] + if !ok { + continue + } + method.BodyFields = append([]Property(nil), class.Fields...) + } +} + func (b *Builder) generateResource(tagName string, paths *v3.Paths) error { if tagName == "" { return fmt.Errorf("empty tag name") @@ -322,3 +357,74 @@ func (b *Builder) schemaTypeNamesByTag(tagName string) []string { slices.Sort(typeNames) return typeNames } + +func inputTypeNames(typeNames []string) []string { + res := make([]string, 0, len(typeNames)) + for _, name := range typeNames { + res = append(res, name+"Input") + } + return res +} + +func (b *Builder) requestSchemaTypeNamesByTag(tagName string) []string { + resolvedSchemas := b.requestSchemasByTag[tagName] + typeNames := make([]string, 0, len(resolvedSchemas)) + for _, s := range resolvedSchemas { + if name := b.getReferenceSchema(s); name != "" { + typeNames = append(typeNames, name) + } + } + slices.Sort(typeNames) + return slices.Compact(typeNames) +} + +func (b *Builder) isRequestSchema(typeName string) bool { + for tagName := range b.requestSchemasByTag { + if slices.Contains(b.requestSchemaTypeNamesByTag(tagName), typeName) { + return true + } + } + return false +} + +func markGenerateInput(types []Writable) { + for _, typ := range types { + switch t := typ.(type) { + case *ClassDeclaration: + t.GenerateInput = true + case *TypeAlias: + t.GenerateInput = true + case *OneOfDeclaration: + t.GenerateInput = true + case *EnumDeclaration[string]: + t.GenerateInput = true + case *EnumDeclaration[int]: + t.GenerateInput = true + case *EnumDeclaration[int64]: + t.GenerateInput = true + case *EnumDeclaration[float64]: + t.GenerateInput = true + } + } +} + +func writableRequestOnly(w Writable) bool { + switch typed := w.(type) { + case *ClassDeclaration: + return typed.RequestOnly + case *TypeAlias: + return typed.RequestOnly + case *OneOfDeclaration: + return typed.RequestOnly + case *EnumDeclaration[string]: + return typed.RequestOnly + case *EnumDeclaration[int]: + return typed.RequestOnly + case *EnumDeclaration[int64]: + return typed.RequestOnly + case *EnumDeclaration[float64]: + return typed.RequestOnly + default: + return false + } +} diff --git a/codegen/pkg/builder/transform.go b/codegen/pkg/builder/transform.go index bf3e1858..0b9d0c97 100644 --- a/codegen/pkg/builder/transform.go +++ b/codegen/pkg/builder/transform.go @@ -43,6 +43,8 @@ func (b *Builder) pathsToBodyTypes(paths *v3.Paths) []Writable { if ok && mt.Schema != nil { name := operationName + "Body" bodyObject, additionalTypes := b.createObject(mt.Schema.Schema(), name) + markRequestTypes(additionalTypes) + markRequestType(bodyObject) paramTypes = append(paramTypes, additionalTypes...) paramTypes = append(paramTypes, bodyObject) } @@ -66,7 +68,6 @@ func (b *Builder) pathsToParamTypes(paths *v3.Paths) []Writable { operationName := strcase.ToCamel(opSpec.OperationId) if len(opSpec.Parameters) > 0 { - fields := make([]Property, 0) fieldTypes := make([]Writable, 0) paramsTypeName := operationName + "Params" for _, p := range opSpec.Parameters { @@ -77,29 +78,11 @@ func (b *Builder) pathsToParamTypes(paths *v3.Paths) []Writable { alias := p.Name name := parameterFieldName(alias) - typeName, types := b.genSchema(p.Schema, paramsTypeName+strcase.ToCamel(name)) + _, types := b.genSchema(p.Schema, paramsTypeName+strcase.ToCamel(name)) + markRequestTypes(types) fieldTypes = append(fieldTypes, types...) - - fields = append(fields, Property{ - Name: name, - SerializedName: alias, - Type: typeName, - Optional: p.Required == nil || !*p.Required, - Comment: parameterPropertyDoc(p.Schema.Schema()), - }) - } - - if len(fields) != 0 { - paramsTpl := ClassDeclaration{ - Type: "struct", - Name: paramsTypeName, - Description: operationParamsDoc(paramsTypeName, opSpec), - Fields: fields, - } - - paramTypes = append(paramTypes, fieldTypes...) - paramTypes = append(paramTypes, ¶msTpl) } + paramTypes = append(paramTypes, fieldTypes...) } } } @@ -107,6 +90,31 @@ func (b *Builder) pathsToParamTypes(paths *v3.Paths) []Writable { return paramTypes } +func markRequestTypes(types []Writable) { + for _, typ := range types { + markRequestType(typ) + } +} + +func markRequestType(typ Writable) { + switch t := typ.(type) { + case *ClassDeclaration: + t.RequestOnly = true + case *TypeAlias: + t.RequestOnly = true + case *OneOfDeclaration: + t.RequestOnly = true + case *EnumDeclaration[string]: + t.RequestOnly = true + case *EnumDeclaration[int]: + t.RequestOnly = true + case *EnumDeclaration[int64]: + t.RequestOnly = true + case *EnumDeclaration[float64]: + t.RequestOnly = true + } +} + func parameterFieldName(name string) string { name = strings.ReplaceAll(name, "[]", "") name = strings.ReplaceAll(name, ".", "_") diff --git a/codegen/pkg/builder/types.go b/codegen/pkg/builder/types.go index 3d9a79e5..937d5e44 100644 --- a/codegen/pkg/builder/types.go +++ b/codegen/pkg/builder/types.go @@ -24,6 +24,43 @@ func indent(n int, s string) string { func (c *ClassDeclaration) String() string { buf := new(strings.Builder) + if c.RequestOnly { + if c.AdditionalPropertiesType != "" { + valueType := "typing.Any" + if len(c.Fields) == 0 { + valueType = inputTypeName(c.AdditionalPropertiesType) + } + fmt.Fprintf(buf, "%sInput = typing.Mapping[str, %s]\n", c.Name, valueType) + if c.Description != "" { + fmt.Fprintf(buf, "'''\n%s\n'''\n", c.Description) + } + return buf.String() + } + fmt.Fprintf(buf, "class %sInput(typing_extensions.TypedDict, total=False):\n", c.Name) + if c.Description != "" { + fmt.Fprintf(buf, "\t'''\n\t%s\n\t'''\n", c.Description) + } + if len(c.Fields) == 0 { + fmt.Fprint(buf, "\tpass\n") + } else { + fields := append([]Property(nil), c.Fields...) + slices.SortFunc(fields, func(a, b Property) int { + if a.Optional && !b.Optional { + return 1 + } + if b.Optional && !a.Optional { + return -1 + } + return strings.Compare(a.Name, b.Name) + }) + for _, ft := range fields { + fmt.Fprint(buf, "\n") + fmt.Fprint(buf, indent(1, ft.TypedDictFieldString())) + } + } + fmt.Fprint(buf, "\n") + return buf.String() + } fmt.Fprintf(buf, "class %s(pydantic.BaseModel):\n", c.Name) if c.Description != "" { fmt.Fprintf(buf, "\t'''\n\t%s\n\t'''\n", c.Description) @@ -75,7 +112,31 @@ func (c *ClassDeclaration) String() string { fmt.Fprintf(buf, "\tdef additional_properties(self, value: dict[str, %s]) -> None:\n", c.AdditionalPropertiesType) fmt.Fprint(buf, "\t\tobject.__setattr__(self, \"__pydantic_extra__\", dict(value))\n") } - fmt.Fprint(buf, "\n") + if c.GenerateInput { + fmt.Fprint(buf, "\n") + fmt.Fprintf(buf, "class %sDict(typing_extensions.TypedDict, total=False):\n", c.Name) + if len(c.Fields) == 0 { + fmt.Fprint(buf, "\tpass\n") + } else { + fields := append([]Property(nil), c.Fields...) + slices.SortFunc(fields, func(a, b Property) int { + if a.Optional && !b.Optional { + return 1 + } + if b.Optional && !a.Optional { + return -1 + } + return strings.Compare(a.Name, b.Name) + }) + for _, ft := range fields { + fmt.Fprint(buf, "\n") + fmt.Fprint(buf, indent(1, ft.TypedDictFieldString())) + } + } + fmt.Fprint(buf, "\n") + fmt.Fprintf(buf, "%sInput = %sDict\n", c.Name, c.Name) + fmt.Fprint(buf, "\n") + } return buf.String() } @@ -85,9 +146,20 @@ func (c *ClassDeclaration) TypeName() string { func (o *OneOfDeclaration) String() string { buf := new(strings.Builder) + options := make([]string, 0, len(o.Options)) + for _, option := range o.Options { + options = append(options, inputTypeName(option)) + } + if o.RequestOnly { + fmt.Fprintf(buf, "%sInput = typing.Union[%s]", o.Name, strings.Join(options, ", ")) + return buf.String() + } fmt.Fprintf(buf, "%s = typing.Union[", o.Name) fmt.Fprint(buf, strings.Join(o.Options, ", ")) - fmt.Fprintf(buf, "]") + fmt.Fprintf(buf, "]\n") + if o.GenerateInput { + fmt.Fprintf(buf, "%sInput = typing.Union[%s]", o.Name, strings.Join(options, ", ")) + } return buf.String() } @@ -123,17 +195,72 @@ func (p *Property) String() string { return buf.String() } +func (p Property) FieldName() string { + return pythonFieldName(p.Name) +} + +func (p Property) WireName() string { + if p.SerializedName != "" { + return p.SerializedName + } + return p.FieldName() +} + +func (p Property) MethodParameterString(allowNone bool) string { + typeName := p.MethodParameterType() + if p.Optional { + if allowNone { + return fmt.Sprintf("%s: typing.Union[%s, None, NotGivenType] = NOT_GIVEN", p.FieldName(), typeName) + } + return fmt.Sprintf("%s: typing.Union[%s, NotGivenType] = NOT_GIVEN", p.FieldName(), typeName) + } + + return fmt.Sprintf("%s: %s", p.FieldName(), typeName) +} + +func (p Property) MethodParameterType() string { + return inputTypeName(p.Type) +} + +func (p Property) BodyArgumentExpr(allowNone bool) string { + name := p.FieldName() + if strings.HasPrefix(p.Type, "list[") { + if allowNone && p.Optional { + return fmt.Sprintf("(list(%s) if %s is not None else None)", name, name) + } + return fmt.Sprintf("list(%s)", name) + } + + return name +} + +func (p Property) TypedDictFieldString() string { + typeName := inputTypeName(p.Type) + if p.Comment != "" { + typeName = fmt.Sprintf("typing_extensions.Annotated[%s, typing_extensions.Doc(%#v)]", typeName, p.Comment) + } + if p.Optional { + return fmt.Sprintf("%s: typing_extensions.NotRequired[%s]", p.FieldName(), typeName) + } + + return fmt.Sprintf("%s: typing_extensions.Required[%s]", p.FieldName(), typeName) +} + func (e *EnumDeclaration[E]) String() string { - buf := new(strings.Builder) - fmt.Fprintf(buf, "%s = typing.Union[typing.Literal[", e.Name) + values := make([]string, 0, len(e.Values)) slices.Sort(e.Values) - for i, v := range e.Values { - if i != 0 { - fmt.Fprint(buf, ", ") - } - fmt.Fprintf(buf, "%#v", v) + for _, v := range e.Values { + values = append(values, fmt.Sprintf("%#v", v)) + } + alias := fmt.Sprintf("typing.Union[typing.Literal[%s], %s]", strings.Join(values, ", "), pythonEnumBaseType(e.Type)) + if e.RequestOnly { + return fmt.Sprintf("%sInput = %s\n", e.Name, alias) + } + buf := new(strings.Builder) + fmt.Fprintf(buf, "%s = %s\n", e.Name, alias) + if e.GenerateInput { + fmt.Fprintf(buf, "%sInput = %s\n", e.Name, e.Name) } - fmt.Fprintf(buf, "], %s]\n", pythonEnumBaseType(e.Type)) return buf.String() } @@ -170,3 +297,66 @@ func pythonEnumBaseType(typeName string) string { return "typing.Any" } } + +func inputTypeName(typeName string) string { + switch { + case strings.HasPrefix(typeName, "list[") && strings.HasSuffix(typeName, "]"): + return "typing.Sequence[" + inputTypeName(typeName[5:len(typeName)-1]) + "]" + case strings.HasPrefix(typeName, "typing.Sequence[") && strings.HasSuffix(typeName, "]"): + return "typing.Sequence[" + inputTypeName(typeName[len("typing.Sequence["):len(typeName)-1]) + "]" + case strings.HasPrefix(typeName, "dict[") && strings.HasSuffix(typeName, "]"): + args := splitTypeArgs(typeName[5 : len(typeName)-1]) + if len(args) == 2 { + return fmt.Sprintf("typing.Mapping[%s, %s]", args[0], inputTypeName(args[1])) + } + case strings.HasPrefix(typeName, "typing.Mapping["), + strings.HasPrefix(typeName, "typing.Literal["), + strings.HasPrefix(typeName, "typing.Any"), + strings.HasPrefix(typeName, "typing_extensions."): + return typeName + case strings.HasPrefix(typeName, "typing.Union[") && strings.HasSuffix(typeName, "]"): + args := splitTypeArgs(typeName[len("typing.Union[") : len(typeName)-1]) + for i := range args { + args[i] = inputTypeName(args[i]) + } + return "typing.Union[" + strings.Join(args, ", ") + "]" + case strings.HasPrefix(typeName, "typing.Optional[") && strings.HasSuffix(typeName, "]"): + return "typing.Optional[" + inputTypeName(typeName[len("typing.Optional["):len(typeName)-1]) + "]" + case isPrimitiveType(typeName): + return typeName + default: + return typeName + "Input" + } + + return typeName +} + +func isPrimitiveType(typeName string) bool { + switch typeName { + case "str", "int", "float", "bool", "object", "datetime.date", "datetime.datetime", "datetime.time", "Secret": + return true + default: + return false + } +} + +func splitTypeArgs(s string) []string { + args := make([]string, 0, 2) + start := 0 + depth := 0 + for i, r := range s { + switch r { + case '[': + depth++ + case ']': + depth-- + case ',': + if depth == 0 { + args = append(args, strings.TrimSpace(s[start:i])) + start = i + 1 + } + } + } + args = append(args, strings.TrimSpace(s[start:])) + return args +} diff --git a/codegen/templates/resource.py.tmpl b/codegen/templates/resource.py.tmpl index f1f334bf..564d2420 100644 --- a/codegen/templates/resource.py.tmpl +++ b/codegen/templates/resource.py.tmpl @@ -1,11 +1,12 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 {{- with .TagDescription }} """ {{ . }} """ {{- end }} from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import Resource, AsyncResource, HeaderTypes, NotGivenType, NOT_GIVEN, serialize_query_params, serialize_request_data from .._exceptions import APIError {{- if .UsesSecret }} from .._secret import Secret @@ -13,6 +14,9 @@ from .._secret import Secret {{- with .TypeNames }} from ..types import {{ join ", " . }} {{- end }} +{{- with .InputTypeNames }} +from ..types import {{ join ", " . }} +{{- end }} import datetime import httpx import typing @@ -40,13 +44,21 @@ class {{.Service}}Resource(Resource): ''' {{ indent 2 . }} ''' +{{- end }} +{{- if .HasFlattenedBody }} +{{ indent 2 .BodyInitString }} + +{{- end }} +{{- if .QueryFields }} +{{ indent 2 .QueryInitString }} + {{- end }} resp = self._client.{{.Method}}({{.Path}}, {{- if .HasBody }} - json=body.model_dump(exclude_unset=True), + json=serialize_request_data({{- if .HasFlattenedBody }}body_data{{- else }}body{{- end }}), {{- end }} -{{- with .QueryParams }} - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, +{{- if .QueryFields }} + params=serialize_query_params(query_data) if query_data else None, {{- end }} headers=headers, ) @@ -90,13 +102,21 @@ class Async{{.Service}}Resource(AsyncResource): ''' {{ indent 2 . }} ''' +{{- end }} +{{- if .HasFlattenedBody }} +{{ indent 2 .BodyInitString }} + +{{- end }} +{{- if .QueryFields }} +{{ indent 2 .QueryInitString }} + {{- end }} resp = await self._client.{{.Method}}({{.Path}}, {{- if .HasBody }} - json=body.model_dump(exclude_unset=True), + json=serialize_request_data({{- if .HasFlattenedBody }}body_data{{- else }}body{{- end }}), {{- end }} -{{- with .QueryParams }} - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, +{{- if .QueryFields }} + params=serialize_query_params(query_data) if query_data else None, {{- end }} headers=headers, ) diff --git a/codegen/templates/types.py.tmpl b/codegen/templates/types.py.tmpl index d2145934..11be376e 100644 --- a/codegen/templates/types.py.tmpl +++ b/codegen/templates/types.py.tmpl @@ -3,6 +3,7 @@ from __future__ import annotations import datetime import typing import pydantic +import typing_extensions {{- if .UsesSecret }} from .._secret import Secret {{- end }} diff --git a/examples/card_reader_checkout.py b/examples/card_reader_checkout.py index fdf1f901..4140fff1 100644 --- a/examples/card_reader_checkout.py +++ b/examples/card_reader_checkout.py @@ -2,7 +2,6 @@ import asyncio from sumup import AsyncSumup -from sumup.readers import CreateReaderCheckoutBody, CreateReaderCheckoutBodyTotalAmount async def main(): @@ -15,14 +14,8 @@ async def main(): checkout = await client.readers.create_checkout( merchant_code=merchant_code, reader_id=reader.id, - body=CreateReaderCheckoutBody( - total_amount=CreateReaderCheckoutBodyTotalAmount( - currency="EUR", - minor_unit=2, - value=1000, - ), - description="sumup-py card reader checkout example", - ), + total_amount={"currency": "EUR", "minor_unit": 2, "value": 1000}, + description="sumup-py card reader checkout example", ) print(checkout) diff --git a/examples/sync.py b/examples/sync.py index 500717ef..6a020e11 100644 --- a/examples/sync.py +++ b/examples/sync.py @@ -1,7 +1,6 @@ import os from sumup import Sumup, APIError -from sumup.checkouts import CreateCheckoutBody, ListCheckoutsParams client = Sumup() @@ -13,19 +12,17 @@ print(readers) transactions = client.checkouts.list( - params=ListCheckoutsParams(checkout_reference="1231"), + checkout_reference="1231", headers={"Foo": "Bar"}, ) print(transactions) try: checkout = client.checkouts.create( - body=CreateCheckoutBody( - merchant_code=merchant.merchant_code, - amount=100.50, - checkout_reference="unique-checkout-ref-123", - currency="EUR", - ) + merchant_code=merchant.merchant_code, + amount=100.50, + checkout_reference="unique-checkout-ref-123", + currency="EUR", ) print(checkout) except APIError as e: diff --git a/sumup/__init__.py b/sumup/__init__.py index 2c7b40f7..55f9ae10 100644 --- a/sumup/__init__.py +++ b/sumup/__init__.py @@ -2,4 +2,4 @@ from sumup._service import Resource, AsyncResource from sumup._exceptions import APIError -__all__ = ["APIError", "AsyncResource", "AsyncSumup", "MerchantAccount", "Resource", "Sumup"] +__all__ = ["APIError", "AsyncResource", "AsyncSumup", "Resource", "Sumup"] diff --git a/sumup/_service.py b/sumup/_service.py index 453b2f4b..de95d08e 100644 --- a/sumup/_service.py +++ b/sumup/_service.py @@ -1,13 +1,26 @@ +import datetime import httpx import platform import sys import typing from functools import lru_cache +from collections.abc import Mapping, Sequence from ._api_version import __api_version__ from ._version import __version__ +from ._secret import Secret HeaderTypes = typing.Mapping[str, str] +PrimitiveQueryValue = typing.Optional[typing.Union[str, int, float]] +QueryValue = typing.Union[PrimitiveQueryValue, typing.Sequence[PrimitiveQueryValue]] +QueryParamTypes = typing.Mapping[str, QueryValue] + + +class NotGivenType: + __slots__ = () + + +NOT_GIVEN = NotGivenType() class BaseResource: @@ -16,6 +29,22 @@ def version() -> str: return f"v{__version__}" +def serialize_request_data(value: object) -> object: + if isinstance(value, Secret): + return value.value() + if isinstance(value, (datetime.datetime, datetime.date, datetime.time)): + return value.isoformat() + if isinstance(value, Mapping): + return {str(key): serialize_request_data(item) for key, item in value.items()} + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + return [serialize_request_data(item) for item in value] + return value + + +def serialize_query_params(value: QueryParamTypes) -> QueryParamTypes: + return typing.cast(QueryParamTypes, serialize_request_data(value)) + + def runtime_headers() -> dict[str, str]: return dict(_runtime_headers()) diff --git a/sumup/checkouts/__init__.py b/sumup/checkouts/__init__.py index b8f1e1c2..44279794 100755 --- a/sumup/checkouts/__init__.py +++ b/sumup/checkouts/__init__.py @@ -2,13 +2,6 @@ from .resource import ( CheckoutsResource, AsyncCheckoutsResource, - CreateCheckoutBodyPurpose, - CreateCheckoutBody, - ProcessCheckoutBodyPaymentType, - ProcessCheckoutBody, - CreateApplePaySessionBody, - GetPaymentMethodsParams, - ListCheckoutsParams, GetPaymentMethods200ResponseAvailablePaymentMethod, GetPaymentMethods200Response, ProcessCheckoutResponse, @@ -41,13 +34,6 @@ __all__ = [ "CheckoutsResource", "AsyncCheckoutsResource", - "CreateCheckoutBodyPurpose", - "CreateCheckoutBody", - "ProcessCheckoutBodyPaymentType", - "ProcessCheckoutBody", - "CreateApplePaySessionBody", - "GetPaymentMethodsParams", - "ListCheckoutsParams", "GetPaymentMethods200ResponseAvailablePaymentMethod", "GetPaymentMethods200Response", "ProcessCheckoutResponse", diff --git a/sumup/checkouts/resource.py b/sumup/checkouts/resource.py index 94c62479..58ff3c9e 100755 --- a/sumup/checkouts/resource.py +++ b/sumup/checkouts/resource.py @@ -1,4 +1,5 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Checkouts represent online payment sessions that you create before attempting to charge a payer. A checkout captures the payment intent, such as the amount, currency, merchant, and optional customer or redirect settings, and then moves through its lifecycle as you process it. @@ -17,186 +18,252 @@ """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from ..types import ( + AddressLegacy, Card, + CardType, Checkout, CheckoutAccepted, + CheckoutCreateRequest, CheckoutSuccess, Currency, + DetailsError, + EntryMode, + Error, + ErrorExtended, + ErrorForbidden, MandatePayload, + MandateResponse, + PaymentType, PersonalDetails, + Problem, + ProcessCheckout, + TransactionBase, + TransactionCheckoutInfo, +) +from ..types import ( + AddressLegacyInput, + CardInput, + CardTypeInput, + CheckoutCreateRequestInput, + CurrencyInput, + MandatePayloadInput, + PersonalDetailsInput, + ProcessCheckoutInput, ) import datetime import httpx import typing import pydantic +import typing_extensions -CreateCheckoutBodyPurpose = typing.Union[typing.Literal["CHECKOUT", "SETUP_RECURRING_PAYMENT"], str] +CreateCheckoutBodyPurposeInput = typing.Union[ + typing.Literal["CHECKOUT", "SETUP_RECURRING_PAYMENT"], str +] -class CreateCheckoutBody(pydantic.BaseModel): +class CreateCheckoutBodyInput(typing_extensions.TypedDict, total=False): """ Request body for creating a checkout before processing payment. Define the payment amount, currency, merchant,and optional customer or redirect behavior here. """ - amount: float - """ - Amount to be charged to the payer, expressed in major units. - """ - - checkout_reference: str - """ - Merchant-defined reference for the new checkout. It should be unique enough for you to identify the payment attemptin your own systems. - Max length: 90 - """ - - currency: Currency - """ - Three-letter [ISO4217](https://en.wikipedia.org/wiki/ISO_4217) code of the currency for the amount. Currently supportedcurrency values are enumerated above. - """ - - merchant_code: str - """ - Merchant account that should receive the payment. - """ - - customer_id: typing.Optional[str] = None - """ - Merchant-scoped customer identifier. Required when setting up recurring payments and useful when the checkoutshould be linked to a returning payer. - """ - - description: typing.Optional[str] = None - """ - Short merchant-defined description shown in SumUp tools and reporting for easier identification of the checkout. - """ - - purpose: typing.Optional[CreateCheckoutBodyPurpose] = None - """ - Business purpose of the checkout. Use `CHECKOUT` for a standard payment and `SETUP_RECURRING_PAYMENT` whencollecting consent and payment details for future recurring charges. - Default: "CHECKOUT" - """ - - redirect_url: typing.Optional[str] = None - """ - URL where the payer should be sent after a redirect-based payment or SCA flow completes. This is required for[APMs](https://developer.sumup.com/online-payments/apm/introduction) and recommended for card checkouts thatmay require [3DS](https://developer.sumup.com/online-payments/features/3ds). If it is omitted, the [Payment Widget](https://developer.sumup.com/online-payments/checkouts)can render the challenge in an iframe instead of using a full-page redirect. - """ - - return_url: typing.Optional[str] = None - """ - Optional backend callback URL used by SumUp to notify your platform about processing updates for the checkout. - Format:uri - """ - - valid_until: typing.Optional[datetime.datetime] = None - """ - Optional expiration timestamp. The checkout must be processed before this moment, otherwise it becomes unusable.If omitted, the checkout does not have an explicit expiry time. - """ - - -ProcessCheckoutBodyPaymentType = typing.Union[ + amount: typing_extensions.Required[ + typing_extensions.Annotated[ + float, + typing_extensions.Doc("Amount to be charged to the payer, expressed in major units."), + ] + ] + checkout_reference: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Merchant-defined reference for the new checkout. It should be unique enough for you to identify the payment attemptin your own systems.\nMax length: 90" + ), + ] + ] + currency: typing_extensions.Required[ + typing_extensions.Annotated[ + CurrencyInput, + typing_extensions.Doc( + "Three-letter [ISO4217](https://en.wikipedia.org/wiki/ISO_4217) code of the currency for the amount. Currently supportedcurrency values are enumerated above." + ), + ] + ] + merchant_code: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("Merchant account that should receive the payment.") + ] + ] + customer_id: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Merchant-scoped customer identifier. Required when setting up recurring payments and useful when the checkoutshould be linked to a returning payer." + ), + ] + ] + description: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Short merchant-defined description shown in SumUp tools and reporting for easier identification of the checkout." + ), + ] + ] + purpose: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateCheckoutBodyPurposeInput, + typing_extensions.Doc( + 'Business purpose of the checkout. Use `CHECKOUT` for a standard payment and `SETUP_RECURRING_PAYMENT` whencollecting consent and payment details for future recurring charges.\nDefault: "CHECKOUT"' + ), + ] + ] + redirect_url: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "URL where the payer should be sent after a redirect-based payment or SCA flow completes. This is required for[APMs](https://developer.sumup.com/online-payments/apm/introduction) and recommended for card checkouts thatmay require [3DS](https://developer.sumup.com/online-payments/features/3ds). If it is omitted, the [Payment Widget](https://developer.sumup.com/online-payments/checkouts)can render the challenge in an iframe instead of using a full-page redirect." + ), + ] + ] + return_url: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Optional backend callback URL used by SumUp to notify your platform about processing updates for the checkout.\nFormat:uri" + ), + ] + ] + valid_until: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + datetime.datetime, + typing_extensions.Doc( + "Optional expiration timestamp. The checkout must be processed before this moment, otherwise it becomes unusable.If omitted, the checkout does not have an explicit expiry time." + ), + ] + ] + + +ProcessCheckoutBodyPaymentTypeInput = typing.Union[ typing.Literal["apple_pay", "bancontact", "blik", "boleto", "card", "google_pay", "ideal"], str ] -ProcessCheckoutBodyGooglePay = dict[str, object] +ProcessCheckoutBodyGooglePayInput = typing.Mapping[str, object] """ Raw `PaymentData` object received from Google Pay. Send the Google Pay response payload as-is. """ -ProcessCheckoutBodyApplePay = dict[str, object] +ProcessCheckoutBodyApplePayInput = typing.Mapping[str, object] """ Raw payment token object received from Apple Pay. Send the Apple Pay response payload as-is. """ -class ProcessCheckoutBody(pydantic.BaseModel): +class ProcessCheckoutBodyInput(typing_extensions.TypedDict, total=False): """ Request body for attempting payment on an existing checkout. The required companion fields depend on theselected `payment_type`, for example card details, saved-card data, or payer information required by aspecific payment method. """ - payment_type: ProcessCheckoutBodyPaymentType - """ - Payment method used for this processing attempt. It determines which additional request fields are required. - """ - - apple_pay: typing.Optional[ProcessCheckoutBodyApplePay] = None - """ - Raw payment token object received from Apple Pay. Send the Apple Pay response payload as-is. - """ - - card: typing.Optional[Card] = None - """ - __Required when payment type is `card`.__ Details of the payment card. - """ - - customer_id: typing.Optional[str] = None - """ - Customer identifier associated with the saved payment instrument. Required when `token` is provided. - """ - - google_pay: typing.Optional[ProcessCheckoutBodyGooglePay] = None - """ - Raw `PaymentData` object received from Google Pay. Send the Google Pay response payload as-is. - """ - - installments: typing.Optional[int] = None - """ - Number of installments for deferred payments. Available only to merchant users in Brazil. - Min: 1 - Max: 12 - """ - - mandate: typing.Optional[MandatePayload] = None - """ - Mandate details used when a checkout should create a reusable card token for future recurring or merchant-initiated payments. - """ - - personal_details: typing.Optional[PersonalDetails] = None - """ - Personal details for the customer. - """ - - token: typing.Optional[str] = None - """ - Saved-card token to use instead of raw card details when processing with a previously stored payment instrument. - """ - - -class CreateApplePaySessionBody(pydantic.BaseModel): + payment_type: typing_extensions.Required[ + typing_extensions.Annotated[ + ProcessCheckoutBodyPaymentTypeInput, + typing_extensions.Doc( + "Payment method used for this processing attempt. It determines which additional request fields are required." + ), + ] + ] + apple_pay: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + ProcessCheckoutBodyApplePayInput, + typing_extensions.Doc( + "Raw payment token object received from Apple Pay. Send the Apple Pay response payload as-is." + ), + ] + ] + card: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CardInput, + typing_extensions.Doc( + "__Required when payment type is `card`.__ Details of the payment card." + ), + ] + ] + customer_id: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Customer identifier associated with the saved payment instrument. Required when `token` is provided." + ), + ] + ] + google_pay: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + ProcessCheckoutBodyGooglePayInput, + typing_extensions.Doc( + "Raw `PaymentData` object received from Google Pay. Send the Google Pay response payload as-is." + ), + ] + ] + installments: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "Number of installments for deferred payments. Available only to merchant users in Brazil.\nMin: 1\nMax: 12" + ), + ] + ] + mandate: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + MandatePayloadInput, + typing_extensions.Doc( + "Mandate details used when a checkout should create a reusable card token for future recurring or merchant-initiated payments." + ), + ] + ] + personal_details: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + PersonalDetailsInput, typing_extensions.Doc("Personal details for the customer.") + ] + ] + token: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Saved-card token to use instead of raw card details when processing with a previously stored payment instrument." + ), + ] + ] + + +class CreateApplePaySessionBodyInput(typing_extensions.TypedDict, total=False): """ CreateApplePaySessionBody is a schema definition. """ - context: str - """ - the context to create this apple pay session. - Format: hostname - """ - - target: str - """ - The target url to create this apple pay session. - Format: uri - """ - - -class GetPaymentMethodsParams(pydantic.BaseModel): - """ - GetPaymentMethodsParams: query parameters for GetPaymentMethods - """ - - amount: typing.Optional[float] = None - - currency: typing.Optional[str] = None - - -class ListCheckoutsParams(pydantic.BaseModel): - """ - ListCheckoutsParams: query parameters for ListCheckouts - """ - - checkout_reference: typing.Optional[str] = None + context: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "the context to create this apple pay session.\nFormat: hostname" + ), + ] + ] + target: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc("The target url to create this apple pay session.\nFormat: uri"), + ] + ] class GetPaymentMethods200ResponseAvailablePaymentMethod(pydantic.BaseModel): @@ -226,6 +293,7 @@ class GetPaymentMethods200Response(pydantic.BaseModel): """ ProcessCheckoutResponse = typing.Union[CheckoutSuccess, CheckoutAccepted] + CreateApplePaySession200Response = dict[str, object] """ CreateApplePaySession200Response is a schema definition. @@ -241,7 +309,9 @@ def __init__(self, client: httpx.Client) -> None: def list_available_payment_methods( self, merchant_code: str, - params: typing.Optional[GetPaymentMethodsParams] = None, + *, + amount: typing.Union[float, NotGivenType] = NOT_GIVEN, + currency: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> GetPaymentMethods200Response: """ @@ -249,9 +319,15 @@ def list_available_payment_methods( Get payment methods available for the given merchant to use with a checkout. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(amount, NotGivenType) and amount is not None: + query_data["amount"] = amount + if not isinstance(currency, NotGivenType) and currency is not None: + query_data["currency"] = currency + resp = self._client.get( f"/v0.1/merchants/{merchant_code}/payment-methods", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -263,10 +339,22 @@ def list_available_payment_methods( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def create( - self, body: CreateCheckoutBody, headers: typing.Optional[HeaderTypes] = None + self, + *, + checkout_reference: str, + amount: float, + currency: CurrencyInput, + merchant_code: str, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + return_url: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + customer_id: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + purpose: typing.Union[CreateCheckoutBodyPurposeInput, None, NotGivenType] = NOT_GIVEN, + valid_until: typing.Union[datetime.datetime, None, NotGivenType] = NOT_GIVEN, + redirect_url: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> Checkout: """ Create a checkout @@ -277,9 +365,27 @@ def create( Follow by processing a checkout to charge the provided payment instrument. """ + body_data: dict[str, typing.Any] = {} + body_data["checkout_reference"] = checkout_reference + body_data["amount"] = amount + body_data["currency"] = currency + body_data["merchant_code"] = merchant_code + if not isinstance(description, NotGivenType): + body_data["description"] = description + if not isinstance(return_url, NotGivenType): + body_data["return_url"] = return_url + if not isinstance(customer_id, NotGivenType): + body_data["customer_id"] = customer_id + if not isinstance(purpose, NotGivenType): + body_data["purpose"] = purpose + if not isinstance(valid_until, NotGivenType): + body_data["valid_until"] = valid_until + if not isinstance(redirect_url, NotGivenType): + body_data["redirect_url"] = redirect_url + resp = self._client.post( - "/v0.1/checkouts", - json=body.model_dump(exclude_unset=True), + f"/v0.1/checkouts", + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -303,11 +409,12 @@ def create( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def list( self, - params: typing.Optional[ListCheckoutsParams] = None, + *, + checkout_reference: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListCheckouts200Response: """ @@ -315,9 +422,13 @@ def list( Lists created checkout resources according to the applied `checkout_reference`. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(checkout_reference, NotGivenType) and checkout_reference is not None: + query_data["checkout_reference"] = checkout_reference + resp = self._client.get( - "/v0.1/checkouts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/checkouts", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -327,7 +438,7 @@ def list( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get(self, id: str, headers: typing.Optional[HeaderTypes] = None) -> CheckoutSuccess: """ @@ -350,10 +461,22 @@ def get(self, id: str, headers: typing.Optional[HeaderTypes] = None) -> Checkout "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def process( - self, id: str, body: ProcessCheckoutBody, headers: typing.Optional[HeaderTypes] = None + self, + id: str, + *, + payment_type: ProcessCheckoutBodyPaymentTypeInput, + installments: typing.Union[int, None, NotGivenType] = NOT_GIVEN, + mandate: typing.Union[MandatePayloadInput, None, NotGivenType] = NOT_GIVEN, + card: typing.Union[CardInput, None, NotGivenType] = NOT_GIVEN, + google_pay: typing.Union[ProcessCheckoutBodyGooglePayInput, None, NotGivenType] = NOT_GIVEN, + apple_pay: typing.Union[ProcessCheckoutBodyApplePayInput, None, NotGivenType] = NOT_GIVEN, + token: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + customer_id: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + personal_details: typing.Union[PersonalDetailsInput, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> ProcessCheckoutResponse: """ Process a checkout @@ -362,9 +485,28 @@ def process( Follow this request with `Retrieve a checkout` to confirm its status. """ + body_data: dict[str, typing.Any] = {} + body_data["payment_type"] = payment_type + if not isinstance(installments, NotGivenType): + body_data["installments"] = installments + if not isinstance(mandate, NotGivenType): + body_data["mandate"] = mandate + if not isinstance(card, NotGivenType): + body_data["card"] = card + if not isinstance(google_pay, NotGivenType): + body_data["google_pay"] = google_pay + if not isinstance(apple_pay, NotGivenType): + body_data["apple_pay"] = apple_pay + if not isinstance(token, NotGivenType): + body_data["token"] = token + if not isinstance(customer_id, NotGivenType): + body_data["customer_id"] = customer_id + if not isinstance(personal_details, NotGivenType): + body_data["personal_details"] = personal_details + resp = self._client.put( f"/v0.1/checkouts/{id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -392,7 +534,7 @@ def process( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def deactivate(self, id: str, headers: typing.Optional[HeaderTypes] = None) -> Checkout: """ @@ -421,10 +563,10 @@ def deactivate(self, id: str, headers: typing.Optional[HeaderTypes] = None) -> C body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def create_apple_pay_session( - self, id: str, body: CreateApplePaySessionBody, headers: typing.Optional[HeaderTypes] = None + self, id: str, *, context: str, target: str, headers: typing.Optional[HeaderTypes] = None ) -> CreateApplePaySession200Response: """ Create an Apple Pay session @@ -436,9 +578,13 @@ def create_apple_pay_session( SumUp validates the merchant session request and returns the Apple Pay session object that your frontend should pass to Apple's JavaScript API. """ + body_data: dict[str, typing.Any] = {} + body_data["context"] = context + body_data["target"] = target + resp = self._client.put( f"/v0.2/checkouts/{id}/apple-pay-session", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -452,7 +598,7 @@ def create_apple_pay_session( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncCheckoutsResource(AsyncResource): @@ -464,7 +610,9 @@ def __init__(self, client: httpx.AsyncClient) -> None: async def list_available_payment_methods( self, merchant_code: str, - params: typing.Optional[GetPaymentMethodsParams] = None, + *, + amount: typing.Union[float, NotGivenType] = NOT_GIVEN, + currency: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> GetPaymentMethods200Response: """ @@ -472,9 +620,15 @@ async def list_available_payment_methods( Get payment methods available for the given merchant to use with a checkout. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(amount, NotGivenType) and amount is not None: + query_data["amount"] = amount + if not isinstance(currency, NotGivenType) and currency is not None: + query_data["currency"] = currency + resp = await self._client.get( f"/v0.1/merchants/{merchant_code}/payment-methods", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -486,10 +640,22 @@ async def list_available_payment_methods( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def create( - self, body: CreateCheckoutBody, headers: typing.Optional[HeaderTypes] = None + self, + *, + checkout_reference: str, + amount: float, + currency: CurrencyInput, + merchant_code: str, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + return_url: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + customer_id: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + purpose: typing.Union[CreateCheckoutBodyPurposeInput, None, NotGivenType] = NOT_GIVEN, + valid_until: typing.Union[datetime.datetime, None, NotGivenType] = NOT_GIVEN, + redirect_url: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> Checkout: """ Create a checkout @@ -500,9 +666,27 @@ async def create( Follow by processing a checkout to charge the provided payment instrument. """ + body_data: dict[str, typing.Any] = {} + body_data["checkout_reference"] = checkout_reference + body_data["amount"] = amount + body_data["currency"] = currency + body_data["merchant_code"] = merchant_code + if not isinstance(description, NotGivenType): + body_data["description"] = description + if not isinstance(return_url, NotGivenType): + body_data["return_url"] = return_url + if not isinstance(customer_id, NotGivenType): + body_data["customer_id"] = customer_id + if not isinstance(purpose, NotGivenType): + body_data["purpose"] = purpose + if not isinstance(valid_until, NotGivenType): + body_data["valid_until"] = valid_until + if not isinstance(redirect_url, NotGivenType): + body_data["redirect_url"] = redirect_url + resp = await self._client.post( - "/v0.1/checkouts", - json=body.model_dump(exclude_unset=True), + f"/v0.1/checkouts", + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -526,11 +710,12 @@ async def create( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def list( self, - params: typing.Optional[ListCheckoutsParams] = None, + *, + checkout_reference: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListCheckouts200Response: """ @@ -538,9 +723,13 @@ async def list( Lists created checkout resources according to the applied `checkout_reference`. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(checkout_reference, NotGivenType) and checkout_reference is not None: + query_data["checkout_reference"] = checkout_reference + resp = await self._client.get( - "/v0.1/checkouts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/checkouts", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -550,7 +739,7 @@ async def list( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get(self, id: str, headers: typing.Optional[HeaderTypes] = None) -> CheckoutSuccess: """ @@ -573,10 +762,22 @@ async def get(self, id: str, headers: typing.Optional[HeaderTypes] = None) -> Ch "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def process( - self, id: str, body: ProcessCheckoutBody, headers: typing.Optional[HeaderTypes] = None + self, + id: str, + *, + payment_type: ProcessCheckoutBodyPaymentTypeInput, + installments: typing.Union[int, None, NotGivenType] = NOT_GIVEN, + mandate: typing.Union[MandatePayloadInput, None, NotGivenType] = NOT_GIVEN, + card: typing.Union[CardInput, None, NotGivenType] = NOT_GIVEN, + google_pay: typing.Union[ProcessCheckoutBodyGooglePayInput, None, NotGivenType] = NOT_GIVEN, + apple_pay: typing.Union[ProcessCheckoutBodyApplePayInput, None, NotGivenType] = NOT_GIVEN, + token: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + customer_id: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + personal_details: typing.Union[PersonalDetailsInput, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> ProcessCheckoutResponse: """ Process a checkout @@ -585,9 +786,28 @@ async def process( Follow this request with `Retrieve a checkout` to confirm its status. """ + body_data: dict[str, typing.Any] = {} + body_data["payment_type"] = payment_type + if not isinstance(installments, NotGivenType): + body_data["installments"] = installments + if not isinstance(mandate, NotGivenType): + body_data["mandate"] = mandate + if not isinstance(card, NotGivenType): + body_data["card"] = card + if not isinstance(google_pay, NotGivenType): + body_data["google_pay"] = google_pay + if not isinstance(apple_pay, NotGivenType): + body_data["apple_pay"] = apple_pay + if not isinstance(token, NotGivenType): + body_data["token"] = token + if not isinstance(customer_id, NotGivenType): + body_data["customer_id"] = customer_id + if not isinstance(personal_details, NotGivenType): + body_data["personal_details"] = personal_details + resp = await self._client.put( f"/v0.1/checkouts/{id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -615,7 +835,7 @@ async def process( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def deactivate(self, id: str, headers: typing.Optional[HeaderTypes] = None) -> Checkout: """ @@ -644,10 +864,10 @@ async def deactivate(self, id: str, headers: typing.Optional[HeaderTypes] = None body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def create_apple_pay_session( - self, id: str, body: CreateApplePaySessionBody, headers: typing.Optional[HeaderTypes] = None + self, id: str, *, context: str, target: str, headers: typing.Optional[HeaderTypes] = None ) -> CreateApplePaySession200Response: """ Create an Apple Pay session @@ -659,9 +879,13 @@ async def create_apple_pay_session( SumUp validates the merchant session request and returns the Apple Pay session object that your frontend should pass to Apple's JavaScript API. """ + body_data: dict[str, typing.Any] = {} + body_data["context"] = context + body_data["target"] = target + resp = await self._client.put( f"/v0.2/checkouts/{id}/apple-pay-session", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -675,4 +899,4 @@ async def create_apple_pay_session( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/customers/__init__.py b/sumup/customers/__init__.py index f2e36468..e530c1cd 100755 --- a/sumup/customers/__init__.py +++ b/sumup/customers/__init__.py @@ -2,8 +2,6 @@ from .resource import ( CustomersResource, AsyncCustomersResource, - CreateCustomerBody, - UpdateCustomerBody, ) from ..types import ( AddressLegacy, @@ -22,8 +20,6 @@ __all__ = [ "CustomersResource", "AsyncCustomersResource", - "CreateCustomerBody", - "UpdateCustomerBody", "AddressLegacy", "CardType", "Customer", diff --git a/sumup/customers/resource.py b/sumup/customers/resource.py index ba4e7310..c3db2553 100755 --- a/sumup/customers/resource.py +++ b/sumup/customers/resource.py @@ -1,4 +1,5 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Allow your regular customers to save their information with the Customers model. @@ -8,43 +9,61 @@ """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from ..types import ( + AddressLegacy, + CardType, Customer, + Error, + ErrorExtended, + ErrorForbidden, + MandateResponse, PaymentInstrumentResponse, PersonalDetails, + Problem, ) +from ..types import AddressLegacyInput, CustomerInput, PersonalDetailsInput +import datetime import httpx import typing import pydantic +import typing_extensions -class CreateCustomerBody(pydantic.BaseModel): +class CreateCustomerBodyInput(typing_extensions.TypedDict, total=False): """ Saved customer details. """ - customer_id: str - """ - Unique ID of the customer. - """ - - personal_details: typing.Optional[PersonalDetails] = None - """ - Personal details for the customer. - """ + customer_id: typing_extensions.Required[ + typing_extensions.Annotated[str, typing_extensions.Doc("Unique ID of the customer.")] + ] + personal_details: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + PersonalDetailsInput, typing_extensions.Doc("Personal details for the customer.") + ] + ] -class UpdateCustomerBody(pydantic.BaseModel): +class UpdateCustomerBodyInput(typing_extensions.TypedDict, total=False): """ UpdateCustomerBody is a schema definition. """ - personal_details: typing.Optional[PersonalDetails] = None - """ - Personal details for the customer. - """ + personal_details: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + PersonalDetailsInput, typing_extensions.Doc("Personal details for the customer.") + ] + ] ListPaymentInstruments200Response = list[PaymentInstrumentResponse] @@ -60,16 +79,25 @@ def __init__(self, client: httpx.Client) -> None: super().__init__(client) def create( - self, body: CreateCustomerBody, headers: typing.Optional[HeaderTypes] = None + self, + *, + customer_id: str, + personal_details: typing.Union[PersonalDetailsInput, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> Customer: """ Create a customer Creates a new saved customer resource which you can later manipulate and save payment instruments to. """ + body_data: dict[str, typing.Any] = {} + body_data["customer_id"] = customer_id + if not isinstance(personal_details, NotGivenType): + body_data["personal_details"] = personal_details + resp = self._client.post( - "/v0.1/customers", - json=body.model_dump(exclude_unset=True), + f"/v0.1/customers", + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -93,7 +121,7 @@ def create( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get(self, customer_id: str, headers: typing.Optional[HeaderTypes] = None) -> Customer: """ @@ -122,12 +150,13 @@ def get(self, customer_id: str, headers: typing.Optional[HeaderTypes] = None) -> "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def update( self, customer_id: str, - body: UpdateCustomerBody, + *, + personal_details: typing.Union[PersonalDetailsInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Customer: """ @@ -137,9 +166,13 @@ def update( The request only overwrites the parameters included in the request, all other parameters will remain with theirinitially assigned values. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(personal_details, NotGivenType): + body_data["personal_details"] = personal_details + resp = self._client.put( f"/v0.1/customers/{customer_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -159,7 +192,7 @@ def update( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def list_payment_instruments( self, customer_id: str, headers: typing.Optional[HeaderTypes] = None @@ -192,7 +225,7 @@ def list_payment_instruments( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def deactivate_payment_instrument( self, customer_id: str, token: str, headers: typing.Optional[HeaderTypes] = None @@ -225,7 +258,7 @@ def deactivate_payment_instrument( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncCustomersResource(AsyncResource): @@ -235,16 +268,25 @@ def __init__(self, client: httpx.AsyncClient) -> None: super().__init__(client) async def create( - self, body: CreateCustomerBody, headers: typing.Optional[HeaderTypes] = None + self, + *, + customer_id: str, + personal_details: typing.Union[PersonalDetailsInput, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> Customer: """ Create a customer Creates a new saved customer resource which you can later manipulate and save payment instruments to. """ + body_data: dict[str, typing.Any] = {} + body_data["customer_id"] = customer_id + if not isinstance(personal_details, NotGivenType): + body_data["personal_details"] = personal_details + resp = await self._client.post( - "/v0.1/customers", - json=body.model_dump(exclude_unset=True), + f"/v0.1/customers", + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -268,7 +310,7 @@ async def create( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get(self, customer_id: str, headers: typing.Optional[HeaderTypes] = None) -> Customer: """ @@ -297,12 +339,13 @@ async def get(self, customer_id: str, headers: typing.Optional[HeaderTypes] = No "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def update( self, customer_id: str, - body: UpdateCustomerBody, + *, + personal_details: typing.Union[PersonalDetailsInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Customer: """ @@ -312,9 +355,13 @@ async def update( The request only overwrites the parameters included in the request, all other parameters will remain with theirinitially assigned values. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(personal_details, NotGivenType): + body_data["personal_details"] = personal_details + resp = await self._client.put( f"/v0.1/customers/{customer_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -334,7 +381,7 @@ async def update( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def list_payment_instruments( self, customer_id: str, headers: typing.Optional[HeaderTypes] = None @@ -367,7 +414,7 @@ async def list_payment_instruments( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def deactivate_payment_instrument( self, customer_id: str, token: str, headers: typing.Optional[HeaderTypes] = None @@ -400,4 +447,4 @@ async def deactivate_payment_instrument( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/members/__init__.py b/sumup/members/__init__.py index 0b9d7496..152cad86 100755 --- a/sumup/members/__init__.py +++ b/sumup/members/__init__.py @@ -2,10 +2,6 @@ from .resource import ( MembersResource, AsyncMembersResource, - CreateMerchantMemberBody, - UpdateMerchantMemberBodyUser, - UpdateMerchantMemberBody, - ListMerchantMembersParams, ListMerchantMembers200Response, ) from ..types import ( @@ -23,10 +19,6 @@ __all__ = [ "MembersResource", "AsyncMembersResource", - "CreateMerchantMemberBody", - "UpdateMerchantMemberBodyUser", - "UpdateMerchantMemberBody", - "ListMerchantMembersParams", "ListMerchantMembers200Response", "Attributes", "Invite", diff --git a/sumup/members/resource.py b/sumup/members/resource.py index 16e1905b..97a5fd7a 100755 --- a/sumup/members/resource.py +++ b/sumup/members/resource.py @@ -1,135 +1,145 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Endpoints to manage account members. Members are users that have membership within merchant accounts. """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from .._secret import Secret from ..types import ( Attributes, + Invite, Member, MembershipStatus, + MembershipUser, + MembershipUserClassic, Metadata, + Problem, ) +from ..types import AttributesInput, MembershipStatusInput, MetadataInput +import datetime import httpx import typing import pydantic +import typing_extensions -class CreateMerchantMemberBody(pydantic.BaseModel): +class CreateMerchantMemberBodyInput(typing_extensions.TypedDict, total=False): """ CreateMerchantMemberBody is a schema definition. """ - email: str - """ - Email address of the member to add. - Format: email - """ - - roles: list[str] - """ - List of roles to assign to the new member. - """ - - attributes: typing.Optional[Attributes] = None - """ - Object attributes that are modifiable only by SumUp applications. - """ - - is_managed_user: typing.Optional[bool] = None - """ - True if the user is managed by the merchant. In this case, we'll created a virtual user with the provided passwordand nickname. - """ - - metadata: typing.Optional[Metadata] = None - """ - Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object. - Max properties: 64 - """ - - nickname: typing.Optional[str] = None - """ - Nickname of the member to add. Only used if `is_managed_user` is true. Used for display purposes only. - """ - - password: typing.Optional[Secret] = None - """ - Password of the member to add. Only used if `is_managed_user` is true. In the case of service accounts, thepassword is not used and can not be defined by the caller. - Format: password - Min length: 8 - """ - - -class UpdateMerchantMemberBodyUser(pydantic.BaseModel): + email: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("Email address of the member to add.\nFormat: email") + ] + ] + roles: typing_extensions.Required[ + typing_extensions.Annotated[ + typing.Sequence[str], + typing_extensions.Doc("List of roles to assign to the new member."), + ] + ] + attributes: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + AttributesInput, + typing_extensions.Doc( + "Object attributes that are modifiable only by SumUp applications." + ), + ] + ] + is_managed_user: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + bool, + typing_extensions.Doc( + "True if the user is managed by the merchant. In this case, we'll created a virtual user with the provided passwordand nickname." + ), + ] + ] + metadata: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + MetadataInput, + typing_extensions.Doc( + "Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object.\nMax properties: 64" + ), + ] + ] + nickname: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Nickname of the member to add. Only used if `is_managed_user` is true. Used for display purposes only." + ), + ] + ] + password: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + Secret, + typing_extensions.Doc( + "Password of the member to add. Only used if `is_managed_user` is true. In the case of service accounts, thepassword is not used and can not be defined by the caller.\nFormat: password\nMin length: 8" + ), + ] + ] + + +class UpdateMerchantMemberBodyUserInput(typing_extensions.TypedDict, total=False): """ Allows you to update user data of managed users. """ - nickname: typing.Optional[str] = None - """ - User's preferred name. Used for display purposes only. - """ - - password: typing.Optional[Secret] = None - """ - Password of the member to add. Only used if `is_managed_user` is true. - Format: password - Min length: 8 - """ - - -class UpdateMerchantMemberBody(pydantic.BaseModel): + nickname: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("User's preferred name. Used for display purposes only.") + ] + ] + password: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + Secret, + typing_extensions.Doc( + "Password of the member to add. Only used if `is_managed_user` is true.\nFormat: password\nMin length: 8" + ), + ] + ] + + +class UpdateMerchantMemberBodyInput(typing_extensions.TypedDict, total=False): """ UpdateMerchantMemberBody is a schema definition. """ - attributes: typing.Optional[Attributes] = None - """ - Object attributes that are modifiable only by SumUp applications. - """ - - metadata: typing.Optional[Metadata] = None - """ - Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object. - Max properties: 64 - """ - - roles: typing.Optional[list[str]] = None - - user: typing.Optional[UpdateMerchantMemberBodyUser] = None - """ - Allows you to update user data of managed users. - """ - - -class ListMerchantMembersParams(pydantic.BaseModel): - """ - ListMerchantMembersParams: query parameters for ListMerchantMembers - """ - - email: typing.Optional[str] = None - - limit: typing.Optional[int] = None - - offset: typing.Optional[int] = None - - roles: typing.Optional[list[str]] = None - - scroll: typing.Optional[bool] = None - - status: typing.Optional[MembershipStatus] = None - """ - The status of the membership. - """ - - user_id: typing.Optional[str] = pydantic.Field( - default=None, - serialization_alias="user.id", - validation_alias=pydantic.AliasChoices("user.id", "user_id"), - ) + attributes: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + AttributesInput, + typing_extensions.Doc( + "Object attributes that are modifiable only by SumUp applications." + ), + ] + ] + metadata: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + MetadataInput, + typing_extensions.Doc( + "Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object.\nMax properties: 64" + ), + ] + ] + roles: typing_extensions.NotRequired[typing.Sequence[str]] + user: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + UpdateMerchantMemberBodyUserInput, + typing_extensions.Doc("Allows you to update user data of managed users."), + ] + ] class ListMerchantMembers200Response(pydantic.BaseModel): @@ -151,7 +161,14 @@ def __init__(self, client: httpx.Client) -> None: def list( self, merchant_code: str, - params: typing.Optional[ListMerchantMembersParams] = None, + *, + offset: typing.Union[int, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + scroll: typing.Union[bool, NotGivenType] = NOT_GIVEN, + email: typing.Union[str, NotGivenType] = NOT_GIVEN, + user_id: typing.Union[str, NotGivenType] = NOT_GIVEN, + status: typing.Union[MembershipStatusInput, NotGivenType] = NOT_GIVEN, + roles: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListMerchantMembers200Response: """ @@ -159,9 +176,25 @@ def list( Lists merchant members. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(offset, NotGivenType) and offset is not None: + query_data["offset"] = offset + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(scroll, NotGivenType) and scroll is not None: + query_data["scroll"] = scroll + if not isinstance(email, NotGivenType) and email is not None: + 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(status, NotGivenType) and status is not None: + query_data["status"] = status + if not isinstance(roles, NotGivenType) and roles is not None: + query_data["roles"] = list(roles) + resp = self._client.get( f"/v0.1/merchants/{merchant_code}/members", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -169,12 +202,19 @@ def list( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def create( self, merchant_code: str, - body: CreateMerchantMemberBody, + *, + is_managed_user: typing.Union[bool, None, NotGivenType] = NOT_GIVEN, + email: str, + password: typing.Union[Secret, None, NotGivenType] = NOT_GIVEN, + nickname: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + roles: typing.Sequence[str], + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, + attributes: typing.Union[AttributesInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Member: """ @@ -182,9 +222,23 @@ def create( Create a merchant member. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(is_managed_user, NotGivenType): + body_data["is_managed_user"] = is_managed_user + body_data["email"] = email + if not isinstance(password, NotGivenType): + body_data["password"] = password + if not isinstance(nickname, NotGivenType): + body_data["nickname"] = nickname + body_data["roles"] = list(roles) + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + if not isinstance(attributes, NotGivenType): + body_data["attributes"] = attributes + resp = self._client.post( f"/v0.1/merchants/{merchant_code}/members", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -200,7 +254,7 @@ def create( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get( self, merchant_code: str, member_id: str, headers: typing.Optional[HeaderTypes] = None @@ -219,13 +273,17 @@ def get( elif resp.status_code == 404: raise APIError("Merchant or member not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def update( self, merchant_code: str, member_id: str, - body: UpdateMerchantMemberBody, + *, + roles: typing.Union[typing.Sequence[str], None, NotGivenType] = NOT_GIVEN, + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, + attributes: typing.Union[AttributesInput, None, NotGivenType] = NOT_GIVEN, + user: typing.Union[UpdateMerchantMemberBodyUserInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Member: """ @@ -233,9 +291,19 @@ def update( Update the merchant member. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(roles, NotGivenType): + body_data["roles"] = list(roles) if roles is not None else None + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + if not isinstance(attributes, NotGivenType): + body_data["attributes"] = attributes + if not isinstance(user, NotGivenType): + body_data["user"] = user + resp = self._client.put( f"/v0.1/merchants/{merchant_code}/members/{member_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -261,7 +329,7 @@ def update( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def delete( self, merchant_code: str, member_id: str, headers: typing.Optional[HeaderTypes] = None @@ -280,7 +348,7 @@ def delete( elif resp.status_code == 404: raise APIError("Merchant or member not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncMembersResource(AsyncResource): @@ -292,7 +360,14 @@ def __init__(self, client: httpx.AsyncClient) -> None: async def list( self, merchant_code: str, - params: typing.Optional[ListMerchantMembersParams] = None, + *, + offset: typing.Union[int, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + scroll: typing.Union[bool, NotGivenType] = NOT_GIVEN, + email: typing.Union[str, NotGivenType] = NOT_GIVEN, + user_id: typing.Union[str, NotGivenType] = NOT_GIVEN, + status: typing.Union[MembershipStatusInput, NotGivenType] = NOT_GIVEN, + roles: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListMerchantMembers200Response: """ @@ -300,9 +375,25 @@ async def list( Lists merchant members. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(offset, NotGivenType) and offset is not None: + query_data["offset"] = offset + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(scroll, NotGivenType) and scroll is not None: + query_data["scroll"] = scroll + if not isinstance(email, NotGivenType) and email is not None: + 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(status, NotGivenType) and status is not None: + query_data["status"] = status + if not isinstance(roles, NotGivenType) and roles is not None: + query_data["roles"] = list(roles) + resp = await self._client.get( f"/v0.1/merchants/{merchant_code}/members", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -310,12 +401,19 @@ async def list( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def create( self, merchant_code: str, - body: CreateMerchantMemberBody, + *, + is_managed_user: typing.Union[bool, None, NotGivenType] = NOT_GIVEN, + email: str, + password: typing.Union[Secret, None, NotGivenType] = NOT_GIVEN, + nickname: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + roles: typing.Sequence[str], + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, + attributes: typing.Union[AttributesInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Member: """ @@ -323,9 +421,23 @@ async def create( Create a merchant member. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(is_managed_user, NotGivenType): + body_data["is_managed_user"] = is_managed_user + body_data["email"] = email + if not isinstance(password, NotGivenType): + body_data["password"] = password + if not isinstance(nickname, NotGivenType): + body_data["nickname"] = nickname + body_data["roles"] = list(roles) + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + if not isinstance(attributes, NotGivenType): + body_data["attributes"] = attributes + resp = await self._client.post( f"/v0.1/merchants/{merchant_code}/members", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -341,7 +453,7 @@ async def create( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get( self, merchant_code: str, member_id: str, headers: typing.Optional[HeaderTypes] = None @@ -360,13 +472,17 @@ async def get( elif resp.status_code == 404: raise APIError("Merchant or member not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def update( self, merchant_code: str, member_id: str, - body: UpdateMerchantMemberBody, + *, + roles: typing.Union[typing.Sequence[str], None, NotGivenType] = NOT_GIVEN, + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, + attributes: typing.Union[AttributesInput, None, NotGivenType] = NOT_GIVEN, + user: typing.Union[UpdateMerchantMemberBodyUserInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Member: """ @@ -374,9 +490,19 @@ async def update( Update the merchant member. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(roles, NotGivenType): + body_data["roles"] = list(roles) if roles is not None else None + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + if not isinstance(attributes, NotGivenType): + body_data["attributes"] = attributes + if not isinstance(user, NotGivenType): + body_data["user"] = user + resp = await self._client.put( f"/v0.1/merchants/{merchant_code}/members/{member_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -402,7 +528,7 @@ async def update( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def delete( self, merchant_code: str, member_id: str, headers: typing.Optional[HeaderTypes] = None @@ -421,4 +547,4 @@ async def delete( elif resp.status_code == 404: raise APIError("Merchant or member not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/memberships/__init__.py b/sumup/memberships/__init__.py index 8cb041ed..90f5c48d 100755 --- a/sumup/memberships/__init__.py +++ b/sumup/memberships/__init__.py @@ -2,8 +2,6 @@ from .resource import ( MembershipsResource, AsyncMembershipsResource, - ListMembershipsParamsResourceParentType, - ListMembershipsParams, ListMemberships200Response, ) from ..types import ( @@ -21,8 +19,6 @@ __all__ = [ "MembershipsResource", "AsyncMembershipsResource", - "ListMembershipsParamsResourceParentType", - "ListMembershipsParams", "ListMemberships200Response", "Attributes", "Invite", diff --git a/sumup/memberships/resource.py b/sumup/memberships/resource.py index 7eab945b..c2f40a7b 100755 --- a/sumup/memberships/resource.py +++ b/sumup/memberships/resource.py @@ -1,88 +1,44 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Endpoints to manage user's memberships. Memberships are used to connect the user to merchant accounts and to grant them access to the merchant's resources via roles. """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from ..types import ( + Attributes, + Invite, Membership, + MembershipResource, MembershipStatus, + Metadata, + Problem, ResourceType, ) +from ..types import MembershipStatusInput, ResourceTypeInput +import datetime import httpx import typing import pydantic +import typing_extensions -class ListMembershipsParamsResourceParentType(pydantic.BaseModel): +class ListMembershipsParamsResourceParentTypeInput(typing_extensions.TypedDict, total=False): """ ListMembershipsParamsResourceParentType is a schema definition. """ - -class ListMembershipsParams(pydantic.BaseModel): - """ - ListMembershipsParams: query parameters for ListMemberships - """ - - kind: typing.Optional[ResourceType] = None - """ - The type of the membership resource. - Possible values are: - * `merchant` - merchant account(s) - * `organization` - organization(s) - """ - - limit: typing.Optional[int] = None - - offset: typing.Optional[int] = None - - resource_attributes_sandbox: typing.Optional[bool] = pydantic.Field( - default=None, - serialization_alias="resource.attributes.sandbox", - validation_alias=pydantic.AliasChoices( - "resource.attributes.sandbox", "resource_attributes_sandbox" - ), - ) - - resource_name: typing.Optional[str] = pydantic.Field( - default=None, - serialization_alias="resource.name", - validation_alias=pydantic.AliasChoices("resource.name", "resource_name"), - ) - - resource_parent_id: typing.Optional[str] = pydantic.Field( - default=None, - serialization_alias="resource.parent.id", - validation_alias=pydantic.AliasChoices("resource.parent.id", "resource_parent_id"), - ) - - 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"), - ) - - resource_type: typing.Optional[ResourceType] = pydantic.Field( - default=None, - serialization_alias="resource.type", - validation_alias=pydantic.AliasChoices("resource.type", "resource_type"), - ) - """ - The type of the membership resource. - Possible values are: - * `merchant` - merchant account(s) - * `organization` - organization(s) - """ - - roles: typing.Optional[list[str]] = None - - status: typing.Optional[MembershipStatus] = None - """ - The status of the membership. - """ + pass class ListMemberships200Response(pydantic.BaseModel): @@ -103,7 +59,19 @@ def __init__(self, client: httpx.Client) -> None: def list( self, - params: typing.Optional[ListMembershipsParams] = None, + *, + offset: typing.Union[int, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + kind: typing.Union[ResourceTypeInput, NotGivenType] = NOT_GIVEN, + status: typing.Union[MembershipStatusInput, NotGivenType] = NOT_GIVEN, + resource_type: typing.Union[ResourceTypeInput, NotGivenType] = NOT_GIVEN, + resource_attributes_sandbox: typing.Union[bool, NotGivenType] = NOT_GIVEN, + resource_name: typing.Union[str, NotGivenType] = NOT_GIVEN, + resource_parent_id: typing.Union[str, NotGivenType] = NOT_GIVEN, + resource_parent_type: typing.Union[ + ListMembershipsParamsResourceParentTypeInput, NotGivenType + ] = NOT_GIVEN, + roles: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListMemberships200Response: """ @@ -111,9 +79,34 @@ def list( List memberships of the current user. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(offset, NotGivenType) and offset is not None: + query_data["offset"] = offset + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(kind, NotGivenType) and kind is not None: + query_data["kind"] = kind + if not isinstance(status, NotGivenType) and status is not None: + query_data["status"] = status + if not isinstance(resource_type, NotGivenType) and resource_type is not None: + query_data["resource.type"] = resource_type + if ( + not isinstance(resource_attributes_sandbox, NotGivenType) + and resource_attributes_sandbox is not None + ): + 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_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: + query_data["resource.parent.type"] = resource_parent_type + if not isinstance(roles, NotGivenType) and roles is not None: + query_data["roles"] = list(roles) + resp = self._client.get( - "/v0.1/memberships", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/memberships", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -129,7 +122,7 @@ def list( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncMembershipsResource(AsyncResource): @@ -140,7 +133,19 @@ def __init__(self, client: httpx.AsyncClient) -> None: async def list( self, - params: typing.Optional[ListMembershipsParams] = None, + *, + offset: typing.Union[int, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + kind: typing.Union[ResourceTypeInput, NotGivenType] = NOT_GIVEN, + status: typing.Union[MembershipStatusInput, NotGivenType] = NOT_GIVEN, + resource_type: typing.Union[ResourceTypeInput, NotGivenType] = NOT_GIVEN, + resource_attributes_sandbox: typing.Union[bool, NotGivenType] = NOT_GIVEN, + resource_name: typing.Union[str, NotGivenType] = NOT_GIVEN, + resource_parent_id: typing.Union[str, NotGivenType] = NOT_GIVEN, + resource_parent_type: typing.Union[ + ListMembershipsParamsResourceParentTypeInput, NotGivenType + ] = NOT_GIVEN, + roles: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListMemberships200Response: """ @@ -148,9 +153,34 @@ async def list( List memberships of the current user. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(offset, NotGivenType) and offset is not None: + query_data["offset"] = offset + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(kind, NotGivenType) and kind is not None: + query_data["kind"] = kind + if not isinstance(status, NotGivenType) and status is not None: + query_data["status"] = status + if not isinstance(resource_type, NotGivenType) and resource_type is not None: + query_data["resource.type"] = resource_type + if ( + not isinstance(resource_attributes_sandbox, NotGivenType) + and resource_attributes_sandbox is not None + ): + 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_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: + query_data["resource.parent.type"] = resource_parent_type + if not isinstance(roles, NotGivenType) and roles is not None: + query_data["roles"] = list(roles) + resp = await self._client.get( - "/v0.1/memberships", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/memberships", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -166,4 +196,4 @@ async def list( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/merchants/__init__.py b/sumup/merchants/__init__.py index 94e5421f..9148f533 100755 --- a/sumup/merchants/__init__.py +++ b/sumup/merchants/__init__.py @@ -2,9 +2,6 @@ from .resource import ( MerchantsResource, AsyncMerchantsResource, - GetMerchantParams, - ListPersonsParams, - GetPersonParams, ) from ..types import ( Address, @@ -35,9 +32,6 @@ __all__ = [ "MerchantsResource", "AsyncMerchantsResource", - "GetMerchantParams", - "ListPersonsParams", - "GetPersonParams", "Address", "Attributes", "BasePerson", diff --git a/sumup/merchants/resource.py b/sumup/merchants/resource.py index 2a909b77..f6da43a9 100755 --- a/sumup/merchants/resource.py +++ b/sumup/merchants/resource.py @@ -1,43 +1,49 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Merchant account represents a single business entity at SumUp. """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from ..types import ( + Address, + Attributes, + BasePerson, + Branding, + BusinessProfile, + ChangeStatus, + ClassicMerchantIdentifiers, + Company, + CompanyIdentifier, + CompanyIdentifiers, + CountryCode, + LegalType, ListPersonsResponseBody, Merchant, + Meta, + Ownership, Person, + PersonalIdentifier, + PhoneNumber, + Problem, + Timestamps, + Version, ) +import datetime import httpx import typing import pydantic - - -class GetMerchantParams(pydantic.BaseModel): - """ - GetMerchantParams: query parameters for GetMerchant - """ - - version: typing.Optional[str] = None - - -class ListPersonsParams(pydantic.BaseModel): - """ - ListPersonsParams: query parameters for ListPersons - """ - - version: typing.Optional[str] = None - - -class GetPersonParams(pydantic.BaseModel): - """ - GetPersonParams: query parameters for GetPerson - """ - - version: typing.Optional[str] = None +import typing_extensions class MerchantsResource(Resource): @@ -49,7 +55,8 @@ def __init__(self, client: httpx.Client) -> None: def get( self, merchant_code: str, - params: typing.Optional[GetMerchantParams] = None, + *, + version: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Merchant: """ @@ -59,9 +66,13 @@ def get( Merchant documentation: https://developer.sumup.com/tools/models/merchant """ + query_data: dict[str, typing.Any] = {} + if not isinstance(version, NotGivenType) and version is not None: + query_data["version"] = version + resp = self._client.get( f"/v1/merchants/{merchant_code}", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -71,12 +82,13 @@ def get( "The requested Merchant does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def list_persons( self, merchant_code: str, - params: typing.Optional[ListPersonsParams] = None, + *, + version: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListPersonsResponseBody: """ @@ -86,9 +98,13 @@ def list_persons( Persons documentation: https://developer.sumup.com/tools/models/merchant#persons """ + query_data: dict[str, typing.Any] = {} + if not isinstance(version, NotGivenType) and version is not None: + query_data["version"] = version + resp = self._client.get( f"/v1/merchants/{merchant_code}/persons", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -98,13 +114,14 @@ def list_persons( "The requested Merchant does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get_person( self, merchant_code: str, person_id: str, - params: typing.Optional[GetPersonParams] = None, + *, + version: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Person: """ @@ -114,9 +131,13 @@ def get_person( Persons documentation: https://developer.sumup.com/tools/models/merchant#persons """ + query_data: dict[str, typing.Any] = {} + if not isinstance(version, NotGivenType) and version is not None: + query_data["version"] = version + resp = self._client.get( f"/v1/merchants/{merchant_code}/persons/{person_id}", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -126,7 +147,7 @@ def get_person( "The requested Person does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncMerchantsResource(AsyncResource): @@ -138,7 +159,8 @@ def __init__(self, client: httpx.AsyncClient) -> None: async def get( self, merchant_code: str, - params: typing.Optional[GetMerchantParams] = None, + *, + version: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Merchant: """ @@ -148,9 +170,13 @@ async def get( Merchant documentation: https://developer.sumup.com/tools/models/merchant """ + query_data: dict[str, typing.Any] = {} + if not isinstance(version, NotGivenType) and version is not None: + query_data["version"] = version + resp = await self._client.get( f"/v1/merchants/{merchant_code}", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -160,12 +186,13 @@ async def get( "The requested Merchant does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def list_persons( self, merchant_code: str, - params: typing.Optional[ListPersonsParams] = None, + *, + version: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListPersonsResponseBody: """ @@ -175,9 +202,13 @@ async def list_persons( Persons documentation: https://developer.sumup.com/tools/models/merchant#persons """ + query_data: dict[str, typing.Any] = {} + if not isinstance(version, NotGivenType) and version is not None: + query_data["version"] = version + resp = await self._client.get( f"/v1/merchants/{merchant_code}/persons", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -187,13 +218,14 @@ async def list_persons( "The requested Merchant does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get_person( self, merchant_code: str, person_id: str, - params: typing.Optional[GetPersonParams] = None, + *, + version: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Person: """ @@ -203,9 +235,13 @@ async def get_person( Persons documentation: https://developer.sumup.com/tools/models/merchant#persons """ + query_data: dict[str, typing.Any] = {} + if not isinstance(version, NotGivenType) and version is not None: + query_data["version"] = version + resp = await self._client.get( f"/v1/merchants/{merchant_code}/persons/{person_id}", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -215,4 +251,4 @@ async def get_person( "The requested Person does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/payouts/__init__.py b/sumup/payouts/__init__.py index d182de3c..246a8308 100755 --- a/sumup/payouts/__init__.py +++ b/sumup/payouts/__init__.py @@ -2,12 +2,6 @@ from .resource import ( PayoutsResource, AsyncPayoutsResource, - ListPayoutsV1ParamsFormat, - ListPayoutsV1ParamsOrder, - ListPayoutsV1Params, - ListPayoutsParamsFormat, - ListPayoutsParamsOrder, - ListPayoutsParams, ) from ..types import ( Error, @@ -20,12 +14,6 @@ __all__ = [ "PayoutsResource", "AsyncPayoutsResource", - "ListPayoutsV1ParamsFormat", - "ListPayoutsV1ParamsOrder", - "ListPayoutsV1Params", - "ListPayoutsParamsFormat", - "ListPayoutsParamsOrder", - "ListPayoutsParams", "Error", "ErrorExtended", "FinancialPayouts", diff --git a/sumup/payouts/resource.py b/sumup/payouts/resource.py index 9179a768..8e033d7b 100755 --- a/sumup/payouts/resource.py +++ b/sumup/payouts/resource.py @@ -1,4 +1,5 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ The Payouts model will allow you to track funds you’ve received from SumUp. @@ -6,55 +7,30 @@ """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError -from ..types import FinancialPayouts +from ..types import Error, ErrorExtended, FinancialPayouts, Problem import datetime import httpx import typing import pydantic import typing_extensions -ListPayoutsV1ParamsFormat = typing.Union[typing.Literal["csv", "json"], str] +ListPayoutsV1ParamsFormatInput = typing.Union[typing.Literal["csv", "json"], str] -ListPayoutsV1ParamsOrder = typing.Union[typing.Literal["asc", "desc"], str] +ListPayoutsV1ParamsOrderInput = typing.Union[typing.Literal["asc", "desc"], str] +ListPayoutsParamsFormatInput = typing.Union[typing.Literal["csv", "json"], str] -class ListPayoutsV1Params(pydantic.BaseModel): - """ - ListPayoutsV1Params: query parameters for ListPayoutsV1 - """ - - end_date: datetime.date - - start_date: datetime.date - - format: typing.Optional[ListPayoutsV1ParamsFormat] = None - - limit: typing.Optional[int] = 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): - """ - ListPayoutsParams: query parameters for ListPayouts - """ - - end_date: datetime.date - - start_date: datetime.date - - format: typing.Optional[ListPayoutsParamsFormat] = None - - limit: typing.Optional[int] = None - - order: typing.Optional[ListPayoutsParamsOrder] = None +ListPayoutsParamsOrderInput = typing.Union[typing.Literal["asc", "desc"], str] class PayoutsResource(Resource): @@ -66,7 +42,12 @@ def __init__(self, client: httpx.Client) -> None: def list( self, merchant_code: str, - params: typing.Optional[ListPayoutsV1Params] = None, + *, + start_date: datetime.date, + end_date: datetime.date, + format: typing.Union[ListPayoutsV1ParamsFormatInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListPayoutsV1ParamsOrderInput, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> FinancialPayouts: """ @@ -74,9 +55,19 @@ def list( Lists ordered payouts for the merchant account. """ + query_data: dict[str, typing.Any] = {} + query_data["start_date"] = start_date + query_data["end_date"] = end_date + if not isinstance(format, NotGivenType) and format is not None: + query_data["format"] = format + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + resp = self._client.get( f"/v1.0/merchants/{merchant_code}/payouts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -92,12 +83,17 @@ def list( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated("This method is deprecated") def list_deprecated( self, - params: typing.Optional[ListPayoutsParams] = None, + *, + start_date: datetime.date, + end_date: datetime.date, + format: typing.Union[ListPayoutsParamsFormatInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListPayoutsParamsOrderInput, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> FinancialPayouts: """ @@ -105,9 +101,19 @@ def list_deprecated( Lists ordered payouts for the merchant account. """ + query_data: dict[str, typing.Any] = {} + query_data["start_date"] = start_date + query_data["end_date"] = end_date + if not isinstance(format, NotGivenType) and format is not None: + query_data["format"] = format + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + resp = self._client.get( - "/v0.1/me/financials/payouts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/financials/payouts", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -123,7 +129,7 @@ def list_deprecated( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncPayoutsResource(AsyncResource): @@ -135,7 +141,12 @@ def __init__(self, client: httpx.AsyncClient) -> None: async def list( self, merchant_code: str, - params: typing.Optional[ListPayoutsV1Params] = None, + *, + start_date: datetime.date, + end_date: datetime.date, + format: typing.Union[ListPayoutsV1ParamsFormatInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListPayoutsV1ParamsOrderInput, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> FinancialPayouts: """ @@ -143,9 +154,19 @@ async def list( Lists ordered payouts for the merchant account. """ + query_data: dict[str, typing.Any] = {} + query_data["start_date"] = start_date + query_data["end_date"] = end_date + if not isinstance(format, NotGivenType) and format is not None: + query_data["format"] = format + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + resp = await self._client.get( f"/v1.0/merchants/{merchant_code}/payouts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -161,12 +182,17 @@ async def list( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated("This method is deprecated") async def list_deprecated( self, - params: typing.Optional[ListPayoutsParams] = None, + *, + start_date: datetime.date, + end_date: datetime.date, + format: typing.Union[ListPayoutsParamsFormatInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListPayoutsParamsOrderInput, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> FinancialPayouts: """ @@ -174,9 +200,19 @@ async def list_deprecated( Lists ordered payouts for the merchant account. """ + query_data: dict[str, typing.Any] = {} + query_data["start_date"] = start_date + query_data["end_date"] = end_date + if not isinstance(format, NotGivenType) and format is not None: + query_data["format"] = format + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + resp = await self._client.get( - "/v0.1/me/financials/payouts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/financials/payouts", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -192,4 +228,4 @@ async def list_deprecated( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/readers/__init__.py b/sumup/readers/__init__.py index 67037c03..cdf0234b 100755 --- a/sumup/readers/__init__.py +++ b/sumup/readers/__init__.py @@ -2,13 +2,6 @@ from .resource import ( ReadersResource, AsyncReadersResource, - CreateReaderBody, - UpdateReaderBody, - CreateReaderCheckoutBodyAade, - CreateReaderCheckoutBodyAffiliate, - CreateReaderCheckoutBodyCardType, - CreateReaderCheckoutBodyTotalAmount, - CreateReaderCheckoutBody, ListReaders200Response, ) from ..types import ( @@ -36,13 +29,6 @@ __all__ = [ "ReadersResource", "AsyncReadersResource", - "CreateReaderBody", - "UpdateReaderBody", - "CreateReaderCheckoutBodyAade", - "CreateReaderCheckoutBodyAffiliate", - "CreateReaderCheckoutBodyCardType", - "CreateReaderCheckoutBodyTotalAmount", - "CreateReaderCheckoutBody", "ListReaders200Response", "BadRequest", "CreateReaderCheckoutError", diff --git a/sumup/readers/resource.py b/sumup/readers/resource.py index d9e52e78..caec0174 100755 --- a/sumup/readers/resource.py +++ b/sumup/readers/resource.py @@ -1,133 +1,182 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ A reader represents a device that accepts payments. You can use the SumUp Solo to accept in-person payments. """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from ..types import ( + BadRequest, + CreateReaderCheckoutError, + CreateReaderCheckoutRequest, CreateReaderCheckoutResponse, + CreateReaderCheckoutUnprocessableEntity, + CreateReaderTerminateError, + CreateReaderTerminateUnprocessableEntity, Metadata, + NotFound, + Problem, Reader, + ReaderDevice, ReaderId, ReaderName, ReaderPairingCode, + ReaderStatus, StatusResponse, + Unauthorized, +) +from ..types import ( + CreateReaderCheckoutRequestInput, + MetadataInput, + ReaderIdInput, + ReaderNameInput, + ReaderPairingCodeInput, ) +import datetime import httpx import typing import pydantic +import typing_extensions -class CreateReaderBody(pydantic.BaseModel): +class CreateReaderBodyInput(typing_extensions.TypedDict, total=False): """ CreateReaderBody is a schema definition. """ - name: ReaderName - """ - Custom human-readable, user-defined name for easier identification of the reader. - Max length: 500 - """ - - pairing_code: ReaderPairingCode - """ - The pairing code is a 8 or 9 character alphanumeric string that is displayed on a SumUp Device after initiatingthe pairing. It is used to link the physical device to the created pairing. - Min length: 8 - Max length: 9 - """ - - metadata: typing.Optional[Metadata] = None - """ - Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object. - Max properties: 64 - """ - - -class UpdateReaderBody(pydantic.BaseModel): + name: typing_extensions.Required[ + typing_extensions.Annotated[ + ReaderNameInput, + typing_extensions.Doc( + "Custom human-readable, user-defined name for easier identification of the reader.\nMax length: 500" + ), + ] + ] + pairing_code: typing_extensions.Required[ + typing_extensions.Annotated[ + ReaderPairingCodeInput, + typing_extensions.Doc( + "The pairing code is a 8 or 9 character alphanumeric string that is displayed on a SumUp Device after initiatingthe pairing. It is used to link the physical device to the created pairing.\nMin length: 8\nMax length: 9" + ), + ] + ] + metadata: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + MetadataInput, + typing_extensions.Doc( + "Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object.\nMax properties: 64" + ), + ] + ] + + +class UpdateReaderBodyInput(typing_extensions.TypedDict, total=False): """ UpdateReaderBody is a schema definition. """ - metadata: typing.Optional[Metadata] = None - """ - Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object. - Max properties: 64 - """ + metadata: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + MetadataInput, + typing_extensions.Doc( + "Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object.\nMax properties: 64" + ), + ] + ] + name: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + ReaderNameInput, + typing_extensions.Doc( + "Custom human-readable, user-defined name for easier identification of the reader.\nMax length: 500" + ), + ] + ] - name: typing.Optional[ReaderName] = None - """ - Custom human-readable, user-defined name for easier identification of the reader. - Max length: 500 - """ - -class CreateReaderCheckoutBodyAade(pydantic.BaseModel): +class CreateReaderCheckoutBodyAadeInput(typing_extensions.TypedDict, total=False): """ Optional object containing data for transactions from ERP integrators in Greece that comply with the AADE1155 protocol. When such regulatory/business requirements apply, this object must be provided and contains the data needed tovalidate the transaction with the AADE signature provider. """ - provider_id: str - """ - The identifier of the AADE signature provider. - """ - - signature: str - """ - The base64 encoded signature of the transaction data. - """ - - signature_data: str - """ - The string containing the signed transaction data. - """ - - -CreateReaderCheckoutBodyAffiliateTags = dict[str, object] + provider_id: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("The identifier of the AADE signature provider.") + ] + ] + signature: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("The base64 encoded signature of the transaction data.") + ] + ] + signature_data: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("The string containing the signed transaction data.") + ] + ] + + +CreateReaderCheckoutBodyAffiliateTagsInput = typing.Mapping[str, object] """ Additional metadata for the transaction. It is key-value object that can be associated with the transaction. """ -class CreateReaderCheckoutBodyAffiliate(pydantic.BaseModel): +class CreateReaderCheckoutBodyAffiliateInput(typing_extensions.TypedDict, total=False): """ Affiliate metadata for the transaction. It is a field that allow for integrators to track the source of the transaction. """ - app_id: str - """ - Application ID of the affiliate. - It is a unique identifier for the application and should be set by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page. - """ - - foreign_transaction_id: str - """ - Foreign transaction ID of the affiliate. - It is a unique identifier for the transaction. - It can be used later to fetch the transaction details via the [Transactions API](https://developer.sumup.com/api/transactions/get). - """ - - key: str - """ - Key of the affiliate. - It is a unique identifier for the key and should be generated by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page. - """ - - tags: typing.Optional[CreateReaderCheckoutBodyAffiliateTags] = None - """ - Additional metadata for the transaction. - It is key-value object that can be associated with the transaction. - """ - - -CreateReaderCheckoutBodyCardType = typing.Union[typing.Literal["credit", "debit"], str] - - -class CreateReaderCheckoutBodyTotalAmount(pydantic.BaseModel): + app_id: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Application ID of the affiliate.\nIt is a unique identifier for the application and should be set by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page." + ), + ] + ] + foreign_transaction_id: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Foreign transaction ID of the affiliate.\nIt is a unique identifier for the transaction.\nIt can be used later to fetch the transaction details via the [Transactions API](https://developer.sumup.com/api/transactions/get)." + ), + ] + ] + key: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Key of the affiliate.\nIt is a unique identifier for the key and should be generated by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page." + ), + ] + ] + tags: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutBodyAffiliateTagsInput, + typing_extensions.Doc( + "Additional metadata for the transaction.\nIt is key-value object that can be associated with the transaction." + ), + ] + ] + + +CreateReaderCheckoutBodyCardTypeInput = typing.Union[typing.Literal["credit", "debit"], str] + + +class CreateReaderCheckoutBodyTotalAmountInput(typing_extensions.TypedDict, total=False): """ Amount structure. @@ -136,100 +185,99 @@ class CreateReaderCheckoutBodyTotalAmount(pydantic.BaseModel): For example, EUR 1.00 is represented as value 100 with minor unit of 2. """ - currency: str - """ - Currency ISO 4217 code - """ - - minor_unit: int - """ - The minor units of the currency. - It represents the number of decimals of the currency. For the currencies CLP, COP and HUF, the minor unit is0. - Min: 0 - """ - - value: int - """ - Integer value of the amount. - Min: 0 - """ + currency: typing_extensions.Required[ + typing_extensions.Annotated[str, typing_extensions.Doc("Currency ISO 4217 code")] + ] + minor_unit: typing_extensions.Required[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "The minor units of the currency.\nIt represents the number of decimals of the currency. For the currencies CLP, COP and HUF, the minor unit is0.\nMin: 0" + ), + ] + ] + value: typing_extensions.Required[ + typing_extensions.Annotated[ + int, typing_extensions.Doc("Integer value of the amount.\nMin: 0") + ] + ] -class CreateReaderCheckoutBody(pydantic.BaseModel): +class CreateReaderCheckoutBodyInput(typing_extensions.TypedDict, total=False): """ Reader Checkout """ - total_amount: CreateReaderCheckoutBodyTotalAmount - """ - Amount structure. - - The amount is represented as an integer value altogether with the currency and the minor unit. - - For example, EUR 1.00 is represented as value 100 with minor unit of 2. - """ - - aade: typing.Optional[CreateReaderCheckoutBodyAade] = None - """ - Optional object containing data for transactions from ERP integrators in Greece that comply with the AADE1155 protocol. - When such regulatory/business requirements apply, this object must be provided and contains the data needed tovalidate the transaction with the AADE signature provider. - """ - - affiliate: typing.Optional[CreateReaderCheckoutBodyAffiliate] = None - """ - Affiliate metadata for the transaction. - It is a field that allow for integrators to track the source of the transaction. - """ - - card_type: typing.Optional[CreateReaderCheckoutBodyCardType] = None - """ - The card type of the card used for the transaction. - Is is required only for some countries (e.g: Brazil). - """ - - description: typing.Optional[str] = None - """ - Description of the checkout to be shown in the Merchant Sales - """ - - installments: typing.Optional[int] = None - """ - Number of installments for the transaction. - It may vary according to the merchant country. - For example, in Brazil, the maximum number of installments is 12. - - Omit if the merchant country does support installments. - Otherwise, the checkout will be rejected. - Min: 1 - """ - - return_url: typing.Optional[str] = None - """ - Webhook URL to which the payment result will be sent. - It must be a HTTPS url. - Format: uri - """ - - tip_rates: typing.Optional[list[float]] = None - """ - List of tipping rates to be displayed to the cardholder. - The rates are in percentage and should be between 0.01 and 0.99. - The list should be sorted in ascending order. - """ - - tip_timeout: typing.Optional[int] = None - """ - Time in seconds the cardholder has to select a tip rate. - If not provided, the default value is 30 seconds. - - It can only be set if `tip_rates` is provided. - - **Note**: If the target device is a Solo, it must be in version 3.3.38.0 or higher. - Default: 30 - - Min: 30 - Max: 120 - """ + total_amount: typing_extensions.Required[ + typing_extensions.Annotated[ + CreateReaderCheckoutBodyTotalAmountInput, + typing_extensions.Doc( + "Amount structure.\n\nThe amount is represented as an integer value altogether with the currency and the minor unit.\n\nFor example, EUR 1.00 is represented as value 100 with minor unit of 2." + ), + ] + ] + aade: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutBodyAadeInput, + typing_extensions.Doc( + "Optional object containing data for transactions from ERP integrators in Greece that comply with the AADE1155 protocol.\nWhen such regulatory/business requirements apply, this object must be provided and contains the data needed tovalidate the transaction with the AADE signature provider." + ), + ] + ] + affiliate: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutBodyAffiliateInput, + typing_extensions.Doc( + "Affiliate metadata for the transaction.\nIt is a field that allow for integrators to track the source of the transaction." + ), + ] + ] + card_type: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutBodyCardTypeInput, + typing_extensions.Doc( + "The card type of the card used for the transaction.\nIs is required only for some countries (e.g: Brazil)." + ), + ] + ] + description: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc("Description of the checkout to be shown in the Merchant Sales"), + ] + ] + installments: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "Number of installments for the transaction.\nIt may vary according to the merchant country.\nFor example, in Brazil, the maximum number of installments is 12.\n\nOmit if the merchant country does support installments.\nOtherwise, the checkout will be rejected.\nMin: 1" + ), + ] + ] + return_url: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Webhook URL to which the payment result will be sent.\nIt must be a HTTPS url.\nFormat: uri" + ), + ] + ] + tip_rates: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + typing.Sequence[float], + typing_extensions.Doc( + "List of tipping rates to be displayed to the cardholder.\nThe rates are in percentage and should be between 0.01 and 0.99.\nThe list should be sorted in ascending order." + ), + ] + ] + tip_timeout: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "Time in seconds the cardholder has to select a tip rate.\nIf not provided, the default value is 30 seconds.\n\nIt can only be set if `tip_rates` is provided.\n\n**Note**: If the target device is a Solo, it must be in version 3.3.38.0 or higher.\nDefault: 30\n\nMin: 30\nMax: 120" + ), + ] + ] class ListReaders200Response(pydantic.BaseModel): @@ -267,12 +315,15 @@ def list( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def create( self, merchant_code: str, - body: CreateReaderBody, + *, + pairing_code: ReaderPairingCodeInput, + name: ReaderNameInput, + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Reader: """ @@ -280,9 +331,15 @@ def create( Create a new Reader for the merchant account. """ + body_data: dict[str, typing.Any] = {} + body_data["pairing_code"] = pairing_code + body_data["name"] = name + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + resp = self._client.post( f"/v0.1/merchants/{merchant_code}/readers", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -300,7 +357,7 @@ def create( "The Reader is not in a pending state.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get( self, merchant_code: str, id: ReaderId, headers: typing.Optional[HeaderTypes] = None @@ -323,7 +380,7 @@ def get( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def delete( self, merchant_code: str, id: ReaderId, headers: typing.Optional[HeaderTypes] = None @@ -346,13 +403,15 @@ def delete( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def update( self, merchant_code: str, id: ReaderId, - body: UpdateReaderBody, + *, + name: typing.Union[ReaderNameInput, None, NotGivenType] = NOT_GIVEN, + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Reader: """ @@ -360,9 +419,15 @@ def update( Update a Reader. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(name, NotGivenType): + body_data["name"] = name + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + resp = self._client.patch( f"/v0.1/merchants/{merchant_code}/readers/{id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -380,13 +445,26 @@ def update( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def create_checkout( self, merchant_code: str, reader_id: str, - body: CreateReaderCheckoutBody, + *, + aade: typing.Union[CreateReaderCheckoutBodyAadeInput, None, NotGivenType] = NOT_GIVEN, + affiliate: typing.Union[ + CreateReaderCheckoutBodyAffiliateInput, None, NotGivenType + ] = NOT_GIVEN, + card_type: typing.Union[ + CreateReaderCheckoutBodyCardTypeInput, None, NotGivenType + ] = NOT_GIVEN, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + installments: typing.Union[int, None, NotGivenType] = NOT_GIVEN, + return_url: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + tip_rates: typing.Union[typing.Sequence[float], None, NotGivenType] = NOT_GIVEN, + tip_timeout: typing.Union[int, None, NotGivenType] = NOT_GIVEN, + total_amount: CreateReaderCheckoutBodyTotalAmountInput, headers: typing.Optional[HeaderTypes] = None, ) -> CreateReaderCheckoutResponse: """ @@ -404,9 +482,28 @@ def create_checkout( **Note**: If the target device is a Solo, it must be in version 3.3.24.3 or higher. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(aade, NotGivenType): + body_data["aade"] = aade + if not isinstance(affiliate, NotGivenType): + body_data["affiliate"] = affiliate + if not isinstance(card_type, NotGivenType): + body_data["card_type"] = card_type + if not isinstance(description, NotGivenType): + body_data["description"] = description + if not isinstance(installments, NotGivenType): + body_data["installments"] = installments + if not isinstance(return_url, NotGivenType): + body_data["return_url"] = return_url + if not isinstance(tip_rates, NotGivenType): + body_data["tip_rates"] = list(tip_rates) if tip_rates is not None else None + if not isinstance(tip_timeout, NotGivenType): + body_data["tip_timeout"] = tip_timeout + body_data["total_amount"] = total_amount + resp = self._client.post( f"/v0.1/merchants/{merchant_code}/readers/{reader_id}/checkout", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -430,7 +527,7 @@ def create_checkout( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get_status( self, merchant_code: str, reader_id: str, headers: typing.Optional[HeaderTypes] = None @@ -481,7 +578,7 @@ def get_status( "Response when given reader is not found", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def terminate_checkout( self, merchant_code: str, reader_id: str, headers: typing.Optional[HeaderTypes] = None @@ -529,7 +626,7 @@ def terminate_checkout( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncReadersResource(AsyncResource): @@ -559,12 +656,15 @@ async def list( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def create( self, merchant_code: str, - body: CreateReaderBody, + *, + pairing_code: ReaderPairingCodeInput, + name: ReaderNameInput, + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Reader: """ @@ -572,9 +672,15 @@ async def create( Create a new Reader for the merchant account. """ + body_data: dict[str, typing.Any] = {} + body_data["pairing_code"] = pairing_code + body_data["name"] = name + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + resp = await self._client.post( f"/v0.1/merchants/{merchant_code}/readers", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -592,7 +698,7 @@ async def create( "The Reader is not in a pending state.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get( self, merchant_code: str, id: ReaderId, headers: typing.Optional[HeaderTypes] = None @@ -615,7 +721,7 @@ async def get( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def delete( self, merchant_code: str, id: ReaderId, headers: typing.Optional[HeaderTypes] = None @@ -638,13 +744,15 @@ async def delete( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def update( self, merchant_code: str, id: ReaderId, - body: UpdateReaderBody, + *, + name: typing.Union[ReaderNameInput, None, NotGivenType] = NOT_GIVEN, + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Reader: """ @@ -652,9 +760,15 @@ async def update( Update a Reader. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(name, NotGivenType): + body_data["name"] = name + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + resp = await self._client.patch( f"/v0.1/merchants/{merchant_code}/readers/{id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -672,13 +786,26 @@ async def update( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def create_checkout( self, merchant_code: str, reader_id: str, - body: CreateReaderCheckoutBody, + *, + aade: typing.Union[CreateReaderCheckoutBodyAadeInput, None, NotGivenType] = NOT_GIVEN, + affiliate: typing.Union[ + CreateReaderCheckoutBodyAffiliateInput, None, NotGivenType + ] = NOT_GIVEN, + card_type: typing.Union[ + CreateReaderCheckoutBodyCardTypeInput, None, NotGivenType + ] = NOT_GIVEN, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + installments: typing.Union[int, None, NotGivenType] = NOT_GIVEN, + return_url: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + tip_rates: typing.Union[typing.Sequence[float], None, NotGivenType] = NOT_GIVEN, + tip_timeout: typing.Union[int, None, NotGivenType] = NOT_GIVEN, + total_amount: CreateReaderCheckoutBodyTotalAmountInput, headers: typing.Optional[HeaderTypes] = None, ) -> CreateReaderCheckoutResponse: """ @@ -696,9 +823,28 @@ async def create_checkout( **Note**: If the target device is a Solo, it must be in version 3.3.24.3 or higher. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(aade, NotGivenType): + body_data["aade"] = aade + if not isinstance(affiliate, NotGivenType): + body_data["affiliate"] = affiliate + if not isinstance(card_type, NotGivenType): + body_data["card_type"] = card_type + if not isinstance(description, NotGivenType): + body_data["description"] = description + if not isinstance(installments, NotGivenType): + body_data["installments"] = installments + if not isinstance(return_url, NotGivenType): + body_data["return_url"] = return_url + if not isinstance(tip_rates, NotGivenType): + body_data["tip_rates"] = list(tip_rates) if tip_rates is not None else None + if not isinstance(tip_timeout, NotGivenType): + body_data["tip_timeout"] = tip_timeout + body_data["total_amount"] = total_amount + resp = await self._client.post( f"/v0.1/merchants/{merchant_code}/readers/{reader_id}/checkout", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -722,7 +868,7 @@ async def create_checkout( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get_status( self, merchant_code: str, reader_id: str, headers: typing.Optional[HeaderTypes] = None @@ -773,7 +919,7 @@ async def get_status( "Response when given reader is not found", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def terminate_checkout( self, merchant_code: str, reader_id: str, headers: typing.Optional[HeaderTypes] = None @@ -821,4 +967,4 @@ async def terminate_checkout( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/receipts/__init__.py b/sumup/receipts/__init__.py index 4c6c29f3..11fbe3b6 100755 --- a/sumup/receipts/__init__.py +++ b/sumup/receipts/__init__.py @@ -2,7 +2,6 @@ from .resource import ( ReceiptsResource, AsyncReceiptsResource, - GetReceiptParams, ) from ..types import ( Error, @@ -23,7 +22,6 @@ __all__ = [ "ReceiptsResource", "AsyncReceiptsResource", - "GetReceiptParams", "Error", "EventId", "EventStatus", diff --git a/sumup/receipts/resource.py b/sumup/receipts/resource.py index 90fe073d..ada863d4 100755 --- a/sumup/receipts/resource.py +++ b/sumup/receipts/resource.py @@ -1,27 +1,39 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ The Receipts model obtains receipt-like details for specific transactions. """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from ..types import ( + Error, + EventId, + EventStatus, + EventType, + Problem, Receipt, + ReceiptCard, + ReceiptEvent, + ReceiptMerchantData, + ReceiptReader, + ReceiptTransaction, + TransactionId, ) +import datetime import httpx import typing import pydantic - - -class GetReceiptParams(pydantic.BaseModel): - """ - GetReceiptParams: query parameters for GetReceipt - """ - - mid: str - - tx_event_id: typing.Optional[int] = None +import typing_extensions class ReceiptsResource(Resource): @@ -33,7 +45,9 @@ def __init__(self, client: httpx.Client) -> None: def get( self, id: str, - params: typing.Optional[GetReceiptParams] = None, + *, + mid: str, + tx_event_id: typing.Union[int, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Receipt: """ @@ -41,9 +55,14 @@ def get( Retrieves receipt specific data for a transaction. """ + query_data: dict[str, typing.Any] = {} + query_data["mid"] = mid + if not isinstance(tx_event_id, NotGivenType) and tx_event_id is not None: + query_data["tx_event_id"] = tx_event_id + resp = self._client.get( f"/v1.1/receipts/{id}", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -65,7 +84,7 @@ def get( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncReceiptsResource(AsyncResource): @@ -77,7 +96,9 @@ def __init__(self, client: httpx.AsyncClient) -> None: async def get( self, id: str, - params: typing.Optional[GetReceiptParams] = None, + *, + mid: str, + tx_event_id: typing.Union[int, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Receipt: """ @@ -85,9 +106,14 @@ async def get( Retrieves receipt specific data for a transaction. """ + query_data: dict[str, typing.Any] = {} + query_data["mid"] = mid + if not isinstance(tx_event_id, NotGivenType) and tx_event_id is not None: + query_data["tx_event_id"] = tx_event_id + resp = await self._client.get( f"/v1.1/receipts/{id}", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -109,4 +135,4 @@ async def get( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/roles/__init__.py b/sumup/roles/__init__.py index a0192e58..427a5c0f 100755 --- a/sumup/roles/__init__.py +++ b/sumup/roles/__init__.py @@ -2,8 +2,6 @@ from .resource import ( RolesResource, AsyncRolesResource, - CreateMerchantRoleBody, - UpdateMerchantRoleBody, ListMerchantRoles200Response, ) from ..types import ( @@ -16,8 +14,6 @@ __all__ = [ "RolesResource", "AsyncRolesResource", - "CreateMerchantRoleBody", - "UpdateMerchantRoleBody", "ListMerchantRoles200Response", "Metadata", "Problem", diff --git a/sumup/roles/resource.py b/sumup/roles/resource.py index f58d5002..82a973c3 100755 --- a/sumup/roles/resource.py +++ b/sumup/roles/resource.py @@ -1,65 +1,75 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Endpoints to manage custom roles. Custom roles allow you to tailor roles from individual permissions to match your needs. Once created, you can assign your custom roles to your merchant account members using the memberships. """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError -from ..types import Metadata, Role +from ..types import Metadata, Problem, Role +from ..types import MetadataInput +import datetime import httpx import typing import pydantic +import typing_extensions -class CreateMerchantRoleBody(pydantic.BaseModel): +class CreateMerchantRoleBodyInput(typing_extensions.TypedDict, total=False): """ CreateMerchantRoleBody is a schema definition. """ - name: str - """ - User-defined name of the role. - """ - - permissions: list[str] - """ - User's permissions. - Max items: 100 - """ - - description: typing.Optional[str] = None - """ - User-defined description of the role. - """ - - metadata: typing.Optional[Metadata] = None - """ - Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object. - Max properties: 64 - """ - - -class UpdateMerchantRoleBody(pydantic.BaseModel): + name: typing_extensions.Required[ + typing_extensions.Annotated[str, typing_extensions.Doc("User-defined name of the role.")] + ] + permissions: typing_extensions.Required[ + typing_extensions.Annotated[ + typing.Sequence[str], typing_extensions.Doc("User's permissions.\nMax items: 100") + ] + ] + description: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("User-defined description of the role.") + ] + ] + metadata: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + MetadataInput, + typing_extensions.Doc( + "Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object.\nMax properties: 64" + ), + ] + ] + + +class UpdateMerchantRoleBodyInput(typing_extensions.TypedDict, total=False): """ UpdateMerchantRoleBody is a schema definition. """ - description: typing.Optional[str] = None - """ - User-defined description of the role. - """ - - name: typing.Optional[str] = None - """ - User-defined name of the role. - """ - - permissions: typing.Optional[list[str]] = None - """ - User's permissions. - Max items: 100 - """ + description: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("User-defined description of the role.") + ] + ] + name: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("User-defined name of the role.")] + ] + permissions: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + typing.Sequence[str], typing_extensions.Doc("User's permissions.\nMax items: 100") + ] + ] class ListMerchantRoles200Response(pydantic.BaseModel): @@ -93,12 +103,16 @@ def list( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def create( self, merchant_code: str, - body: CreateMerchantRoleBody, + *, + name: str, + permissions: typing.Sequence[str], + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Role: """ @@ -106,9 +120,17 @@ def create( Create a custom role for the merchant. Roles are defined by the set of permissions that they grant to the members thatthey are assigned to. """ + body_data: dict[str, typing.Any] = {} + body_data["name"] = name + body_data["permissions"] = list(permissions) + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + if not isinstance(description, NotGivenType): + body_data["description"] = description + resp = self._client.post( f"/v0.1/merchants/{merchant_code}/roles", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -118,7 +140,7 @@ def create( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get( self, merchant_code: str, role_id: str, headers: typing.Optional[HeaderTypes] = None @@ -137,7 +159,7 @@ def get( elif resp.status_code == 404: raise APIError("Merchant or role not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def delete( self, merchant_code: str, role_id: str, headers: typing.Optional[HeaderTypes] = None @@ -158,13 +180,16 @@ def delete( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def update( self, merchant_code: str, role_id: str, - body: UpdateMerchantRoleBody, + *, + name: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + permissions: typing.Union[typing.Sequence[str], None, NotGivenType] = NOT_GIVEN, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Role: """ @@ -172,9 +197,17 @@ def update( Update a custom role. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(name, NotGivenType): + body_data["name"] = name + if not isinstance(permissions, NotGivenType): + body_data["permissions"] = list(permissions) if permissions is not None else None + if not isinstance(description, NotGivenType): + body_data["description"] = description + resp = self._client.patch( f"/v0.1/merchants/{merchant_code}/roles/{role_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -184,7 +217,7 @@ def update( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncRolesResource(AsyncResource): @@ -210,12 +243,16 @@ async def list( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def create( self, merchant_code: str, - body: CreateMerchantRoleBody, + *, + name: str, + permissions: typing.Sequence[str], + metadata: typing.Union[MetadataInput, None, NotGivenType] = NOT_GIVEN, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Role: """ @@ -223,9 +260,17 @@ async def create( Create a custom role for the merchant. Roles are defined by the set of permissions that they grant to the members thatthey are assigned to. """ + body_data: dict[str, typing.Any] = {} + body_data["name"] = name + body_data["permissions"] = list(permissions) + if not isinstance(metadata, NotGivenType): + body_data["metadata"] = metadata + if not isinstance(description, NotGivenType): + body_data["description"] = description + resp = await self._client.post( f"/v0.1/merchants/{merchant_code}/roles", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 201: @@ -235,7 +280,7 @@ async def create( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get( self, merchant_code: str, role_id: str, headers: typing.Optional[HeaderTypes] = None @@ -254,7 +299,7 @@ async def get( elif resp.status_code == 404: raise APIError("Merchant or role not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def delete( self, merchant_code: str, role_id: str, headers: typing.Optional[HeaderTypes] = None @@ -275,13 +320,16 @@ async def delete( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def update( self, merchant_code: str, role_id: str, - body: UpdateMerchantRoleBody, + *, + name: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + permissions: typing.Union[typing.Sequence[str], None, NotGivenType] = NOT_GIVEN, + description: typing.Union[str, None, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Role: """ @@ -289,9 +337,17 @@ async def update( Update a custom role. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(name, NotGivenType): + body_data["name"] = name + if not isinstance(permissions, NotGivenType): + body_data["permissions"] = list(permissions) if permissions is not None else None + if not isinstance(description, NotGivenType): + body_data["description"] = description + resp = await self._client.patch( f"/v0.1/merchants/{merchant_code}/roles/{role_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -301,4 +357,4 @@ async def update( elif resp.status_code == 404: raise APIError("Merchant not found.", status=resp.status_code, body=resp.text) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/subaccounts/__init__.py b/sumup/subaccounts/__init__.py index f9981c2a..e2fd2882 100755 --- a/sumup/subaccounts/__init__.py +++ b/sumup/subaccounts/__init__.py @@ -2,11 +2,6 @@ from .resource import ( SubaccountsResource, AsyncSubaccountsResource, - CreateSubAccountBodyPermissions, - CreateSubAccountBody, - UpdateSubAccountBodyPermissions, - UpdateSubAccountBody, - ListSubAccountsParams, ) from ..types import ( Operator, @@ -18,11 +13,6 @@ __all__ = [ "SubaccountsResource", "AsyncSubaccountsResource", - "CreateSubAccountBodyPermissions", - "CreateSubAccountBody", - "UpdateSubAccountBodyPermissions", - "UpdateSubAccountBody", - "ListSubAccountsParams", "Operator", "Permissions", "Problem", diff --git a/sumup/subaccounts/resource.py b/sumup/subaccounts/resource.py index 0436f4e4..aaebe24b 100755 --- a/sumup/subaccounts/resource.py +++ b/sumup/subaccounts/resource.py @@ -1,97 +1,79 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Endpoints for managing merchant sub-accounts (operators). """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError -from ..types import Operator +from ..types import Operator, Permissions, Problem +import datetime import httpx import typing import pydantic import typing_extensions -class CreateSubAccountBodyPermissions(pydantic.BaseModel): +class CreateSubAccountBodyPermissionsInput(typing_extensions.TypedDict, total=False): """ CreateSubAccountBodyPermissions is a schema definition. """ - create_moto_payments: typing.Optional[bool] = None + create_moto_payments: typing_extensions.NotRequired[bool] + create_referral: typing_extensions.NotRequired[bool] + full_transaction_history_view: typing_extensions.NotRequired[bool] + refund_transactions: typing_extensions.NotRequired[bool] - create_referral: typing.Optional[bool] = None - full_transaction_history_view: typing.Optional[bool] = None - - refund_transactions: typing.Optional[bool] = None - - -class CreateSubAccountBody(pydantic.BaseModel): +class CreateSubAccountBodyInput(typing_extensions.TypedDict, total=False): """ CreateSubAccountBody is a schema definition. """ - password: str - """ - Min length: 8 - """ + password: typing_extensions.Required[ + typing_extensions.Annotated[str, typing_extensions.Doc("Min length: 8")] + ] + username: typing_extensions.Required[ + typing_extensions.Annotated[str, typing_extensions.Doc("Format: email")] + ] + nickname: typing_extensions.NotRequired[str] + permissions: typing_extensions.NotRequired[CreateSubAccountBodyPermissionsInput] - username: str - """ - Format: email - """ - nickname: typing.Optional[str] = None - - permissions: typing.Optional[CreateSubAccountBodyPermissions] = None - - -class UpdateSubAccountBodyPermissions(pydantic.BaseModel): +class UpdateSubAccountBodyPermissionsInput(typing_extensions.TypedDict, total=False): """ UpdateSubAccountBodyPermissions is a schema definition. """ - create_moto_payments: typing.Optional[bool] = None - - create_referral: typing.Optional[bool] = None + create_moto_payments: typing_extensions.NotRequired[bool] + create_referral: typing_extensions.NotRequired[bool] + full_transaction_history_view: typing_extensions.NotRequired[bool] + refund_transactions: typing_extensions.NotRequired[bool] - full_transaction_history_view: typing.Optional[bool] = None - refund_transactions: typing.Optional[bool] = None - - -class UpdateSubAccountBody(pydantic.BaseModel): +class UpdateSubAccountBodyInput(typing_extensions.TypedDict, total=False): """ UpdateSubAccountBody is a schema definition. """ - disabled: typing.Optional[bool] = None - - nickname: typing.Optional[str] = None - - password: typing.Optional[str] = None - """ - Min length: 8 - """ - - permissions: typing.Optional[UpdateSubAccountBodyPermissions] = None - - username: typing.Optional[str] = None - """ - Format: email - Max length: 256 - """ - - -class ListSubAccountsParams(pydantic.BaseModel): - """ - ListSubAccountsParams: query parameters for ListSubAccounts - """ - - include_primary: typing.Optional[bool] = None - - query: typing.Optional[str] = None + disabled: typing_extensions.NotRequired[bool] + nickname: typing_extensions.NotRequired[str] + password: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("Min length: 8")] + ] + permissions: typing_extensions.NotRequired[UpdateSubAccountBodyPermissionsInput] + username: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("Format: email\nMax length: 256")] + ] ListSubAccounts200Response = list[Operator] @@ -111,7 +93,9 @@ def __init__(self, client: httpx.Client) -> None: ) def list_sub_accounts( self, - params: typing.Optional[ListSubAccountsParams] = None, + *, + query: typing.Union[str, NotGivenType] = NOT_GIVEN, + include_primary: typing.Union[bool, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListSubAccounts200Response: """ @@ -119,9 +103,15 @@ def list_sub_accounts( Returns list of operators for currently authorized user's merchant. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(query, NotGivenType) and query is not None: + query_data["query"] = query + if not isinstance(include_primary, NotGivenType) and include_primary is not None: + query_data["include_primary"] = include_primary + resp = self._client.get( - "/v0.1/me/accounts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/accounts", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -133,22 +123,38 @@ def list_sub_accounts( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated( "Subaccounts API is deprecated, to create a user in your merchant account please use [Create member](https://developer.sumup.com/api/members/create) instead." ) def create_sub_account( - self, body: CreateSubAccountBody, headers: typing.Optional[HeaderTypes] = None + self, + *, + username: str, + password: str, + nickname: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + permissions: typing.Union[ + CreateSubAccountBodyPermissionsInput, None, NotGivenType + ] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> Operator: """ Create an operator Creates new operator for currently authorized users' merchant. """ + body_data: dict[str, typing.Any] = {} + body_data["username"] = username + body_data["password"] = password + if not isinstance(nickname, NotGivenType): + body_data["nickname"] = nickname + if not isinstance(permissions, NotGivenType): + body_data["permissions"] = permissions + resp = self._client.post( - "/v0.1/me/accounts", - json=body.model_dump(exclude_unset=True), + f"/v0.1/me/accounts", + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -158,7 +164,7 @@ def create_sub_account( "Operator creation was forbidden.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated( "Subaccounts API is deprecated, to get a user that's a member of your merchant account please use [Get member](https://developer.sumup.com/api/members/get) instead." @@ -184,7 +190,7 @@ def compat_get_operator( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated( "Subaccounts API is deprecated, to update a user that's a member of your merchant account please use [Update member](https://developer.sumup.com/api/members/update) instead." @@ -192,7 +198,14 @@ def compat_get_operator( def update_sub_account( self, operator_id: int, - body: UpdateSubAccountBody, + *, + password: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + username: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + disabled: typing.Union[bool, None, NotGivenType] = NOT_GIVEN, + nickname: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + permissions: typing.Union[ + UpdateSubAccountBodyPermissionsInput, None, NotGivenType + ] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Operator: """ @@ -200,9 +213,21 @@ def update_sub_account( Updates operator. If the operator was disabled and their password is updated they will be unblocked. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(password, NotGivenType): + body_data["password"] = password + if not isinstance(username, NotGivenType): + body_data["username"] = username + if not isinstance(disabled, NotGivenType): + body_data["disabled"] = disabled + if not isinstance(nickname, NotGivenType): + body_data["nickname"] = nickname + if not isinstance(permissions, NotGivenType): + body_data["permissions"] = permissions + resp = self._client.put( f"/v0.1/me/accounts/{operator_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -214,7 +239,7 @@ def update_sub_account( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncSubaccountsResource(AsyncResource): @@ -228,7 +253,9 @@ def __init__(self, client: httpx.AsyncClient) -> None: ) async def list_sub_accounts( self, - params: typing.Optional[ListSubAccountsParams] = None, + *, + query: typing.Union[str, NotGivenType] = NOT_GIVEN, + include_primary: typing.Union[bool, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListSubAccounts200Response: """ @@ -236,9 +263,15 @@ async def list_sub_accounts( Returns list of operators for currently authorized user's merchant. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(query, NotGivenType) and query is not None: + query_data["query"] = query + if not isinstance(include_primary, NotGivenType) and include_primary is not None: + query_data["include_primary"] = include_primary + resp = await self._client.get( - "/v0.1/me/accounts", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/accounts", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -250,22 +283,38 @@ async def list_sub_accounts( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated( "Subaccounts API is deprecated, to create a user in your merchant account please use [Create member](https://developer.sumup.com/api/members/create) instead." ) async def create_sub_account( - self, body: CreateSubAccountBody, headers: typing.Optional[HeaderTypes] = None + self, + *, + username: str, + password: str, + nickname: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + permissions: typing.Union[ + CreateSubAccountBodyPermissionsInput, None, NotGivenType + ] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ) -> Operator: """ Create an operator Creates new operator for currently authorized users' merchant. """ + body_data: dict[str, typing.Any] = {} + body_data["username"] = username + body_data["password"] = password + if not isinstance(nickname, NotGivenType): + body_data["nickname"] = nickname + if not isinstance(permissions, NotGivenType): + body_data["permissions"] = permissions + resp = await self._client.post( - "/v0.1/me/accounts", - json=body.model_dump(exclude_unset=True), + f"/v0.1/me/accounts", + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -275,7 +324,7 @@ async def create_sub_account( "Operator creation was forbidden.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated( "Subaccounts API is deprecated, to get a user that's a member of your merchant account please use [Get member](https://developer.sumup.com/api/members/get) instead." @@ -301,7 +350,7 @@ async def compat_get_operator( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated( "Subaccounts API is deprecated, to update a user that's a member of your merchant account please use [Update member](https://developer.sumup.com/api/members/update) instead." @@ -309,7 +358,14 @@ async def compat_get_operator( async def update_sub_account( self, operator_id: int, - body: UpdateSubAccountBody, + *, + password: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + username: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + disabled: typing.Union[bool, None, NotGivenType] = NOT_GIVEN, + nickname: typing.Union[str, None, NotGivenType] = NOT_GIVEN, + permissions: typing.Union[ + UpdateSubAccountBodyPermissionsInput, None, NotGivenType + ] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> Operator: """ @@ -317,9 +373,21 @@ async def update_sub_account( Updates operator. If the operator was disabled and their password is updated they will be unblocked. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(password, NotGivenType): + body_data["password"] = password + if not isinstance(username, NotGivenType): + body_data["username"] = username + if not isinstance(disabled, NotGivenType): + body_data["disabled"] = disabled + if not isinstance(nickname, NotGivenType): + body_data["nickname"] = nickname + if not isinstance(permissions, NotGivenType): + body_data["permissions"] = permissions + resp = await self._client.put( f"/v0.1/me/accounts/{operator_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 200: @@ -331,4 +399,4 @@ async def update_sub_account( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/transactions/__init__.py b/sumup/transactions/__init__.py index 0372c3e0..d298256e 100755 --- a/sumup/transactions/__init__.py +++ b/sumup/transactions/__init__.py @@ -2,17 +2,6 @@ from .resource import ( TransactionsResource, AsyncTransactionsResource, - RefundTransactionBody, - GetTransactionV21Params, - GetTransactionParams, - ListTransactionsV21ParamsOrder, - ListTransactionsV21ParamsStatuse, - ListTransactionsV21ParamsType, - ListTransactionsV21Params, - ListTransactionsParamsOrder, - ListTransactionsParamsStatuse, - ListTransactionsParamsType, - ListTransactionsParams, ListTransactionsV21200Response, ListTransactions200Response, ) @@ -49,17 +38,6 @@ __all__ = [ "TransactionsResource", "AsyncTransactionsResource", - "RefundTransactionBody", - "GetTransactionV21Params", - "GetTransactionParams", - "ListTransactionsV21ParamsOrder", - "ListTransactionsV21ParamsStatuse", - "ListTransactionsV21ParamsType", - "ListTransactionsV21Params", - "ListTransactionsParamsOrder", - "ListTransactionsParamsStatuse", - "ListTransactionsParamsType", - "ListTransactionsParams", "ListTransactionsV21200Response", "ListTransactions200Response", "CardResponse", diff --git a/sumup/transactions/resource.py b/sumup/transactions/resource.py index d61125e7..6b80c436 100755 --- a/sumup/transactions/resource.py +++ b/sumup/transactions/resource.py @@ -1,4 +1,5 @@ # Code generated by `py-sdk-gen`. DO NOT EDIT. +# ruff: noqa: F401, F541 """ Transactions represent completed or attempted payment operations processed for a merchant account. A transaction contains the core payment result, such as the amount, currency, payment method, creation time, and current high-level status. @@ -21,15 +22,45 @@ """ from __future__ import annotations -from .._service import Resource, AsyncResource, HeaderTypes +from .._service import ( + Resource, + AsyncResource, + HeaderTypes, + NotGivenType, + NOT_GIVEN, + serialize_query_params, + serialize_request_data, +) from .._exceptions import APIError from ..types import ( + CardResponse, + CardType, + Currency, + Device, + ElvCardAccount, EntryMode, + Error, + Event, + EventId, + EventStatus, + EventType, + HorizontalAccuracy, + Lat, + Link, + Lon, PaymentType, + Problem, + Product, + TransactionBase, + TransactionCheckoutInfo, + TransactionEvent, TransactionFull, TransactionHistory, + TransactionId, + TransactionMixinHistory, TransactionsHistoryLink, ) +from ..types import EntryModeInput, PaymentTypeInput import datetime import httpx import typing @@ -37,133 +68,40 @@ import typing_extensions -class RefundTransactionBody(pydantic.BaseModel): +class RefundTransactionBodyInput(typing_extensions.TypedDict, total=False): """ Optional amount for partial refunds of transactions. """ - amount: typing.Optional[float] = None - """ - Amount to be refunded. Eligible amount can't exceed the amount of the transaction and varies based on countryand currency. If you do not specify a value, the system performs a full refund of the transaction. - """ - - -class GetTransactionV21Params(pydantic.BaseModel): - """ - GetTransactionV21Params: query parameters for GetTransactionV2.1 - """ - - client_transaction_id: typing.Optional[str] = None - - foreign_transaction_id: typing.Optional[str] = None - - id: typing.Optional[str] = None + amount: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + float, + typing_extensions.Doc( + "Amount to be refunded. Eligible amount can't exceed the amount of the transaction and varies based on countryand currency. If you do not specify a value, the system performs a full refund of the transaction." + ), + ] + ] - transaction_code: typing.Optional[str] = None +ListTransactionsV21ParamsOrderInput = typing.Union[typing.Literal["ascending", "descending"], str] -class GetTransactionParams(pydantic.BaseModel): - """ - GetTransactionParams: query parameters for GetTransaction - """ - - id: typing.Optional[str] = None - - transaction_code: typing.Optional[str] = None - - -ListTransactionsV21ParamsOrder = typing.Union[typing.Literal["ascending", "descending"], str] - -ListTransactionsV21ParamsStatuse = typing.Union[ +ListTransactionsV21ParamsStatuseInput = typing.Union[ typing.Literal["CANCELLED", "CHARGE_BACK", "FAILED", "REFUNDED", "SUCCESSFUL"], str ] -ListTransactionsV21ParamsType = typing.Union[ +ListTransactionsV21ParamsTypeInput = typing.Union[ typing.Literal["CHARGE_BACK", "PAYMENT", "REFUND"], str ] +ListTransactionsParamsOrderInput = typing.Union[typing.Literal["ascending", "descending"], str] -class ListTransactionsV21Params(pydantic.BaseModel): - """ - ListTransactionsV21Params: query parameters for ListTransactionsV2.1 - """ - - changes_since: typing.Optional[datetime.datetime] = None - - entry_modes: typing.Optional[list[EntryMode]] = pydantic.Field( - default=None, - serialization_alias="entry_modes[]", - validation_alias=pydantic.AliasChoices("entry_modes[]", "entry_modes"), - ) - - limit: typing.Optional[int] = None - - newest_ref: typing.Optional[str] = None - - newest_time: typing.Optional[datetime.datetime] = None - - oldest_ref: typing.Optional[str] = None - - oldest_time: typing.Optional[datetime.datetime] = None - - order: typing.Optional[ListTransactionsV21ParamsOrder] = None - - payment_types: typing.Optional[list[PaymentType]] = None - - statuses: typing.Optional[list[ListTransactionsV21ParamsStatuse]] = pydantic.Field( - default=None, - serialization_alias="statuses[]", - validation_alias=pydantic.AliasChoices("statuses[]", "statuses"), - ) - - transaction_code: typing.Optional[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[ +ListTransactionsParamsStatuseInput = 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 - """ - - changes_since: typing.Optional[datetime.datetime] = None - - limit: typing.Optional[int] = None - - newest_ref: typing.Optional[str] = None - - newest_time: typing.Optional[datetime.datetime] = None - - oldest_ref: typing.Optional[str] = None - - oldest_time: typing.Optional[datetime.datetime] = None - - order: typing.Optional[ListTransactionsParamsOrder] = None - - payment_types: typing.Optional[list[PaymentType]] = None - - statuses: typing.Optional[list[ListTransactionsParamsStatuse]] = pydantic.Field( - default=None, - serialization_alias="statuses[]", - validation_alias=pydantic.AliasChoices("statuses[]", "statuses"), - ) - - transaction_code: typing.Optional[str] = None - - types: typing.Optional[list[ListTransactionsParamsType]] = None - - users: typing.Optional[list[str]] = None +ListTransactionsParamsTypeInput = typing.Union[ + typing.Literal["CHARGE_BACK", "PAYMENT", "REFUND"], str +] class ListTransactionsV21200Response(pydantic.BaseModel): @@ -193,16 +131,24 @@ def __init__(self, client: httpx.Client) -> None: super().__init__(client) def refund( - self, txn_id: str, body: RefundTransactionBody, headers: typing.Optional[HeaderTypes] = None + self, + txn_id: str, + *, + amount: typing.Union[float, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ): """ Refund a transaction Refunds an identified transaction either in full or partially. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(amount, NotGivenType): + body_data["amount"] = amount + resp = self._client.post( f"/v0.1/me/refund/{txn_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 204: @@ -218,12 +164,16 @@ def refund( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def get( self, merchant_code: str, - params: typing.Optional[GetTransactionV21Params] = None, + *, + id: typing.Union[str, NotGivenType] = NOT_GIVEN, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, + foreign_transaction_id: typing.Union[str, NotGivenType] = NOT_GIVEN, + client_transaction_id: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> TransactionFull: """ @@ -235,9 +185,25 @@ def get( - `foreign_transaction_id` - `client_transaction_id` """ + query_data: dict[str, typing.Any] = {} + if not isinstance(id, NotGivenType) and id is not None: + query_data["id"] = id + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + if ( + not isinstance(foreign_transaction_id, NotGivenType) + and foreign_transaction_id is not None + ): + query_data["foreign_transaction_id"] = foreign_transaction_id + if ( + not isinstance(client_transaction_id, NotGivenType) + and client_transaction_id is not None + ): + query_data["client_transaction_id"] = client_transaction_id + resp = self._client.get( f"/v2.1/merchants/{merchant_code}/transactions", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -251,12 +217,14 @@ def get( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated("This method is deprecated") def get_deprecated( self, - params: typing.Optional[GetTransactionParams] = None, + *, + id: typing.Union[str, NotGivenType] = NOT_GIVEN, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> TransactionFull: """ @@ -268,9 +236,15 @@ def get_deprecated( - `foreign_transaction_id` - `client_transaction_id` """ + query_data: dict[str, typing.Any] = {} + if not isinstance(id, NotGivenType) and id is not None: + query_data["id"] = id + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + resp = self._client.get( - "/v0.1/me/transactions", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/transactions", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -284,12 +258,29 @@ def get_deprecated( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) def list( self, merchant_code: str, - params: typing.Optional[ListTransactionsV21Params] = None, + *, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListTransactionsV21ParamsOrderInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + users: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, + statuses: typing.Union[ + typing.Sequence[ListTransactionsV21ParamsStatuseInput], NotGivenType + ] = NOT_GIVEN, + payment_types: typing.Union[typing.Sequence[PaymentTypeInput], NotGivenType] = NOT_GIVEN, + entry_modes: typing.Union[typing.Sequence[EntryModeInput], NotGivenType] = NOT_GIVEN, + types: typing.Union[ + typing.Sequence[ListTransactionsV21ParamsTypeInput], NotGivenType + ] = NOT_GIVEN, + changes_since: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, + oldest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + oldest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListTransactionsV21200Response: """ @@ -297,9 +288,37 @@ def list( Lists detailed history of all transactions associated with the merchant profile. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(users, NotGivenType) and users is not None: + query_data["users"] = list(users) + if not isinstance(statuses, NotGivenType) and statuses is not None: + query_data["statuses[]"] = list(statuses) + if not isinstance(payment_types, NotGivenType) and payment_types is not None: + query_data["payment_types"] = list(payment_types) + if not isinstance(entry_modes, NotGivenType) and entry_modes is not None: + query_data["entry_modes[]"] = list(entry_modes) + if not isinstance(types, NotGivenType) and types is not None: + query_data["types"] = list(types) + if not isinstance(changes_since, NotGivenType) and changes_since is not None: + query_data["changes_since"] = changes_since + if not isinstance(newest_time, NotGivenType) and newest_time is not None: + query_data["newest_time"] = newest_time + if not isinstance(newest_ref, NotGivenType) and newest_ref is not None: + query_data["newest_ref"] = newest_ref + if not isinstance(oldest_time, NotGivenType) and oldest_time is not None: + query_data["oldest_time"] = oldest_time + if not isinstance(oldest_ref, NotGivenType) and oldest_ref is not None: + query_data["oldest_ref"] = oldest_ref + resp = self._client.get( f"/v2.1/merchants/{merchant_code}/transactions/history", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -315,12 +334,28 @@ def list( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated("This method is deprecated") def list_deprecated( self, - params: typing.Optional[ListTransactionsParams] = None, + *, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListTransactionsParamsOrderInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + users: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, + statuses: typing.Union[ + typing.Sequence[ListTransactionsParamsStatuseInput], NotGivenType + ] = NOT_GIVEN, + payment_types: typing.Union[typing.Sequence[PaymentTypeInput], NotGivenType] = NOT_GIVEN, + types: typing.Union[ + typing.Sequence[ListTransactionsParamsTypeInput], NotGivenType + ] = NOT_GIVEN, + changes_since: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, + oldest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + oldest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListTransactions200Response: """ @@ -328,9 +363,35 @@ def list_deprecated( Lists detailed history of all transactions associated with the merchant profile. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(users, NotGivenType) and users is not None: + query_data["users"] = list(users) + if not isinstance(statuses, NotGivenType) and statuses is not None: + query_data["statuses[]"] = list(statuses) + if not isinstance(payment_types, NotGivenType) and payment_types is not None: + query_data["payment_types"] = list(payment_types) + if not isinstance(types, NotGivenType) and types is not None: + query_data["types"] = list(types) + if not isinstance(changes_since, NotGivenType) and changes_since is not None: + query_data["changes_since"] = changes_since + if not isinstance(newest_time, NotGivenType) and newest_time is not None: + query_data["newest_time"] = newest_time + if not isinstance(newest_ref, NotGivenType) and newest_ref is not None: + query_data["newest_ref"] = newest_ref + if not isinstance(oldest_time, NotGivenType) and oldest_time is not None: + query_data["oldest_time"] = oldest_time + if not isinstance(oldest_ref, NotGivenType) and oldest_ref is not None: + query_data["oldest_ref"] = oldest_ref + resp = self._client.get( - "/v0.1/me/transactions/history", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/transactions/history", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -346,7 +407,7 @@ def list_deprecated( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) class AsyncTransactionsResource(AsyncResource): @@ -356,16 +417,24 @@ def __init__(self, client: httpx.AsyncClient) -> None: super().__init__(client) async def refund( - self, txn_id: str, body: RefundTransactionBody, headers: typing.Optional[HeaderTypes] = None + self, + txn_id: str, + *, + amount: typing.Union[float, None, NotGivenType] = NOT_GIVEN, + headers: typing.Optional[HeaderTypes] = None, ): """ Refund a transaction Refunds an identified transaction either in full or partially. """ + body_data: dict[str, typing.Any] = {} + if not isinstance(amount, NotGivenType): + body_data["amount"] = amount + resp = await self._client.post( f"/v0.1/me/refund/{txn_id}", - json=body.model_dump(exclude_unset=True), + json=serialize_request_data(body_data), headers=headers, ) if resp.status_code == 204: @@ -381,12 +450,16 @@ async def refund( body=resp.text, ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def get( self, merchant_code: str, - params: typing.Optional[GetTransactionV21Params] = None, + *, + id: typing.Union[str, NotGivenType] = NOT_GIVEN, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, + foreign_transaction_id: typing.Union[str, NotGivenType] = NOT_GIVEN, + client_transaction_id: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> TransactionFull: """ @@ -398,9 +471,25 @@ async def get( - `foreign_transaction_id` - `client_transaction_id` """ + query_data: dict[str, typing.Any] = {} + if not isinstance(id, NotGivenType) and id is not None: + query_data["id"] = id + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + if ( + not isinstance(foreign_transaction_id, NotGivenType) + and foreign_transaction_id is not None + ): + query_data["foreign_transaction_id"] = foreign_transaction_id + if ( + not isinstance(client_transaction_id, NotGivenType) + and client_transaction_id is not None + ): + query_data["client_transaction_id"] = client_transaction_id + resp = await self._client.get( f"/v2.1/merchants/{merchant_code}/transactions", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -414,12 +503,14 @@ async def get( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated("This method is deprecated") async def get_deprecated( self, - params: typing.Optional[GetTransactionParams] = None, + *, + id: typing.Union[str, NotGivenType] = NOT_GIVEN, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> TransactionFull: """ @@ -431,9 +522,15 @@ async def get_deprecated( - `foreign_transaction_id` - `client_transaction_id` """ + query_data: dict[str, typing.Any] = {} + if not isinstance(id, NotGivenType) and id is not None: + query_data["id"] = id + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + resp = await self._client.get( - "/v0.1/me/transactions", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/transactions", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -447,12 +544,29 @@ async def get_deprecated( "The requested resource does not exist.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) async def list( self, merchant_code: str, - params: typing.Optional[ListTransactionsV21Params] = None, + *, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListTransactionsV21ParamsOrderInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + users: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, + statuses: typing.Union[ + typing.Sequence[ListTransactionsV21ParamsStatuseInput], NotGivenType + ] = NOT_GIVEN, + payment_types: typing.Union[typing.Sequence[PaymentTypeInput], NotGivenType] = NOT_GIVEN, + entry_modes: typing.Union[typing.Sequence[EntryModeInput], NotGivenType] = NOT_GIVEN, + types: typing.Union[ + typing.Sequence[ListTransactionsV21ParamsTypeInput], NotGivenType + ] = NOT_GIVEN, + changes_since: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, + oldest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + oldest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListTransactionsV21200Response: """ @@ -460,9 +574,37 @@ async def list( Lists detailed history of all transactions associated with the merchant profile. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(users, NotGivenType) and users is not None: + query_data["users"] = list(users) + if not isinstance(statuses, NotGivenType) and statuses is not None: + query_data["statuses[]"] = list(statuses) + if not isinstance(payment_types, NotGivenType) and payment_types is not None: + query_data["payment_types"] = list(payment_types) + if not isinstance(entry_modes, NotGivenType) and entry_modes is not None: + query_data["entry_modes[]"] = list(entry_modes) + if not isinstance(types, NotGivenType) and types is not None: + query_data["types"] = list(types) + if not isinstance(changes_since, NotGivenType) and changes_since is not None: + query_data["changes_since"] = changes_since + if not isinstance(newest_time, NotGivenType) and newest_time is not None: + query_data["newest_time"] = newest_time + if not isinstance(newest_ref, NotGivenType) and newest_ref is not None: + query_data["newest_ref"] = newest_ref + if not isinstance(oldest_time, NotGivenType) and oldest_time is not None: + query_data["oldest_time"] = oldest_time + if not isinstance(oldest_ref, NotGivenType) and oldest_ref is not None: + query_data["oldest_ref"] = oldest_ref + resp = await self._client.get( f"/v2.1/merchants/{merchant_code}/transactions/history", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -478,12 +620,28 @@ async def list( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) @typing_extensions.deprecated("This method is deprecated") async def list_deprecated( self, - params: typing.Optional[ListTransactionsParams] = None, + *, + transaction_code: typing.Union[str, NotGivenType] = NOT_GIVEN, + order: typing.Union[ListTransactionsParamsOrderInput, NotGivenType] = NOT_GIVEN, + limit: typing.Union[int, NotGivenType] = NOT_GIVEN, + users: typing.Union[typing.Sequence[str], NotGivenType] = NOT_GIVEN, + statuses: typing.Union[ + typing.Sequence[ListTransactionsParamsStatuseInput], NotGivenType + ] = NOT_GIVEN, + payment_types: typing.Union[typing.Sequence[PaymentTypeInput], NotGivenType] = NOT_GIVEN, + types: typing.Union[ + typing.Sequence[ListTransactionsParamsTypeInput], NotGivenType + ] = NOT_GIVEN, + changes_since: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + newest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, + oldest_time: typing.Union[datetime.datetime, NotGivenType] = NOT_GIVEN, + oldest_ref: typing.Union[str, NotGivenType] = NOT_GIVEN, headers: typing.Optional[HeaderTypes] = None, ) -> ListTransactions200Response: """ @@ -491,9 +649,35 @@ async def list_deprecated( Lists detailed history of all transactions associated with the merchant profile. """ + query_data: dict[str, typing.Any] = {} + if not isinstance(transaction_code, NotGivenType) and transaction_code is not None: + query_data["transaction_code"] = transaction_code + if not isinstance(order, NotGivenType) and order is not None: + query_data["order"] = order + if not isinstance(limit, NotGivenType) and limit is not None: + query_data["limit"] = limit + if not isinstance(users, NotGivenType) and users is not None: + query_data["users"] = list(users) + if not isinstance(statuses, NotGivenType) and statuses is not None: + query_data["statuses[]"] = list(statuses) + if not isinstance(payment_types, NotGivenType) and payment_types is not None: + query_data["payment_types"] = list(payment_types) + if not isinstance(types, NotGivenType) and types is not None: + query_data["types"] = list(types) + if not isinstance(changes_since, NotGivenType) and changes_since is not None: + query_data["changes_since"] = changes_since + if not isinstance(newest_time, NotGivenType) and newest_time is not None: + query_data["newest_time"] = newest_time + if not isinstance(newest_ref, NotGivenType) and newest_ref is not None: + query_data["newest_ref"] = newest_ref + if not isinstance(oldest_time, NotGivenType) and oldest_time is not None: + query_data["oldest_time"] = oldest_time + if not isinstance(oldest_ref, NotGivenType) and oldest_ref is not None: + query_data["oldest_ref"] = oldest_ref + resp = await self._client.get( - "/v0.1/me/transactions/history", - params=params.model_dump(by_alias=True, exclude_none=True) if params else None, + f"/v0.1/me/transactions/history", + params=serialize_query_params(query_data) if query_data else None, headers=headers, ) if resp.status_code == 200: @@ -509,4 +693,4 @@ async def list_deprecated( "The request is not authorized.", status=resp.status_code, body=resp.text ) else: - raise APIError("Unexpected response", status=resp.status_code, body=resp.text) + raise APIError(f"Unexpected response", status=resp.status_code, body=resp.text) diff --git a/sumup/types/__init__.py b/sumup/types/__init__.py index 3212aae8..f0ecac7f 100755 --- a/sumup/types/__init__.py +++ b/sumup/types/__init__.py @@ -3,6 +3,7 @@ import datetime import typing import pydantic +import typing_extensions CountryCode = str """ @@ -164,7 +165,49 @@ class AddressLegacy(pydantic.BaseModel): """ +class AddressLegacyDict(typing_extensions.TypedDict, total=False): + city: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("City name from the address.")] + ] + country: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Two letter country code formatted according to [ISO3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)." + ), + ] + ] + line_1: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "First line of the address with details of the street name and number." + ), + ] + ] + line_2: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Second line of the address with details of the building, unit, apartment, and floor numbers." + ), + ] + ] + postal_code: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("Postal code from the address.")] + ] + state: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("State name or abbreviation from the address.") + ] + ] + + +AddressLegacyInput = AddressLegacyDict + + Attributes = dict[str, object] +AttributesInput = typing.Mapping[str, object] """ Object attributes that are modifiable only by SumUp applications. """ @@ -435,7 +478,7 @@ class BusinessProfile(pydantic.BaseModel): The more recognisable your descriptor is, the less risk you have of receiving disputes (e.g. chargebacks). Min length: 1 Max length: 30 - Pattern: ^[a-zA-Z0-9 \-+\'_.]{0,30}$ + Pattern: ^[a-zA-Z0-9 \\-+\\'_.]{0,30}$ """ email: typing.Optional[str] = None @@ -494,10 +537,12 @@ class BusinessProfile(pydantic.BaseModel): ], str, ] +CardTypeInput = CardType CardExpiryMonth = typing.Union[ typing.Literal["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"], str ] +CardExpiryMonthInput = CardExpiryMonth class Card(pydantic.BaseModel): @@ -553,6 +598,65 @@ class Card(pydantic.BaseModel): """ +class CardDict(typing_extensions.TypedDict, total=False): + cvv: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Three or four-digit card verification value (security code) of the payment card.\nWrite only\nMin length: 3\nMax length: 4" + ), + ] + ] + expiry_month: typing_extensions.Required[ + typing_extensions.Annotated[ + CardExpiryMonthInput, + typing_extensions.Doc( + "Month from the expiration time of the payment card. Accepted format is `MM`.\nWrite only" + ), + ] + ] + expiry_year: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Year from the expiration time of the payment card. Accepted formats are `YY` and `YYYY`.\nWrite only\nMin length: 2\nMax length: 4" + ), + ] + ] + name: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Name of the cardholder as it appears on the payment card.\nWrite only" + ), + ] + ] + number: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("Number of the payment card (without spaces).\nWrite only") + ] + ] + type: typing_extensions.Required[ + typing_extensions.Annotated[ + CardTypeInput, + typing_extensions.Doc( + "Issuing card network of the payment card used for the transaction." + ), + ] + ] + zip_code: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Required five-digit ZIP code. Applicable only to merchant users in the USA.\nWrite only\nMin length: 5\nMax length: 5" + ), + ] + ] + + +CardInput = CardDict + + class CardResponse(pydantic.BaseModel): """ Details of the payment card. @@ -593,6 +697,7 @@ class CardResponse(pydantic.BaseModel): ], str, ] +CurrencyInput = Currency EntryMode = typing.Union[ typing.Literal[ @@ -626,6 +731,7 @@ class CardResponse(pydantic.BaseModel): ], str, ] +EntryModeInput = EntryMode MandateResponseStatus = typing.Union[typing.Literal["active", "inactive"], str] @@ -667,6 +773,7 @@ class MandateResponse(pydantic.BaseModel): ], str, ] +PaymentTypeInput = PaymentType TransactionBaseStatus = typing.Union[ typing.Literal["CANCELLED", "FAILED", "PENDING", "SUCCESSFUL"], str @@ -958,6 +1065,7 @@ class CheckoutAccepted(pydantic.BaseModel): CheckoutCreateRequestPurpose = typing.Union[ typing.Literal["CHECKOUT", "SETUP_RECURRING_PAYMENT"], str ] +CheckoutCreateRequestPurposeInput = CheckoutCreateRequestPurpose class CheckoutCreateRequest(pydantic.BaseModel): @@ -1019,6 +1127,87 @@ class CheckoutCreateRequest(pydantic.BaseModel): """ +class CheckoutCreateRequestDict(typing_extensions.TypedDict, total=False): + amount: typing_extensions.Required[ + typing_extensions.Annotated[ + float, + typing_extensions.Doc("Amount to be charged to the payer, expressed in major units."), + ] + ] + checkout_reference: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Merchant-defined reference for the new checkout. It should be unique enough for you to identify the payment attemptin your own systems.\nMax length: 90" + ), + ] + ] + currency: typing_extensions.Required[ + typing_extensions.Annotated[ + CurrencyInput, + typing_extensions.Doc( + "Three-letter [ISO4217](https://en.wikipedia.org/wiki/ISO_4217) code of the currency for the amount. Currently supportedcurrency values are enumerated above." + ), + ] + ] + merchant_code: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("Merchant account that should receive the payment.") + ] + ] + customer_id: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Merchant-scoped customer identifier. Required when setting up recurring payments and useful when the checkoutshould be linked to a returning payer." + ), + ] + ] + description: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Short merchant-defined description shown in SumUp tools and reporting for easier identification of the checkout." + ), + ] + ] + purpose: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CheckoutCreateRequestPurposeInput, + typing_extensions.Doc( + 'Business purpose of the checkout. Use `CHECKOUT` for a standard payment and `SETUP_RECURRING_PAYMENT` whencollecting consent and payment details for future recurring charges.\nDefault: "CHECKOUT"' + ), + ] + ] + redirect_url: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "URL where the payer should be sent after a redirect-based payment or SCA flow completes. This is required for[APMs](https://developer.sumup.com/online-payments/apm/introduction) and recommended for card checkouts thatmay require [3DS](https://developer.sumup.com/online-payments/features/3ds). If it is omitted, the [Payment Widget](https://developer.sumup.com/online-payments/checkouts)can render the challenge in an iframe instead of using a full-page redirect." + ), + ] + ] + return_url: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Optional backend callback URL used by SumUp to notify your platform about processing updates for the checkout.\nFormat:uri" + ), + ] + ] + valid_until: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + datetime.datetime, + typing_extensions.Doc( + "Optional expiration timestamp. The checkout must be processed before this moment, otherwise it becomes unusable.If omitted, the checkout does not have an explicit expiry time." + ), + ] + ] + + +CheckoutCreateRequestInput = CheckoutCreateRequestDict + + CheckoutSuccessStatus = typing.Union[typing.Literal["EXPIRED", "FAILED", "PAID", "PENDING"], str] CheckoutSuccessTransactionStatus = typing.Union[ @@ -1369,7 +1558,29 @@ class CreateReaderCheckoutRequestAade(pydantic.BaseModel): """ +class CreateReaderCheckoutRequestAadeDict(typing_extensions.TypedDict, total=False): + provider_id: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("The identifier of the AADE signature provider.") + ] + ] + signature: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("The base64 encoded signature of the transaction data.") + ] + ] + signature_data: typing_extensions.Required[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("The string containing the signed transaction data.") + ] + ] + + +CreateReaderCheckoutRequestAadeInput = CreateReaderCheckoutRequestAadeDict + + CreateReaderCheckoutRequestAffiliateTags = dict[str, object] +CreateReaderCheckoutRequestAffiliateTagsInput = typing.Mapping[str, object] """ Additional metadata for the transaction. It is key-value object that can be associated with the transaction. @@ -1408,7 +1619,46 @@ class CreateReaderCheckoutRequestAffiliate(pydantic.BaseModel): """ +class CreateReaderCheckoutRequestAffiliateDict(typing_extensions.TypedDict, total=False): + app_id: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Application ID of the affiliate.\nIt is a unique identifier for the application and should be set by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page." + ), + ] + ] + foreign_transaction_id: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Foreign transaction ID of the affiliate.\nIt is a unique identifier for the transaction.\nIt can be used later to fetch the transaction details via the [Transactions API](https://developer.sumup.com/api/transactions/get)." + ), + ] + ] + key: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Key of the affiliate.\nIt is a unique identifier for the key and should be generated by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page." + ), + ] + ] + tags: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutRequestAffiliateTagsInput, + typing_extensions.Doc( + "Additional metadata for the transaction.\nIt is key-value object that can be associated with the transaction." + ), + ] + ] + + +CreateReaderCheckoutRequestAffiliateInput = CreateReaderCheckoutRequestAffiliateDict + + CreateReaderCheckoutRequestCardType = typing.Union[typing.Literal["credit", "debit"], str] +CreateReaderCheckoutRequestCardTypeInput = CreateReaderCheckoutRequestCardType class CreateReaderCheckoutRequestTotalAmount(pydantic.BaseModel): @@ -1439,6 +1689,28 @@ class CreateReaderCheckoutRequestTotalAmount(pydantic.BaseModel): """ +class CreateReaderCheckoutRequestTotalAmountDict(typing_extensions.TypedDict, total=False): + currency: typing_extensions.Required[ + typing_extensions.Annotated[str, typing_extensions.Doc("Currency ISO 4217 code")] + ] + minor_unit: typing_extensions.Required[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "The minor units of the currency.\nIt represents the number of decimals of the currency. For the currencies CLP, COP and HUF, the minor unit is0.\nMin: 0" + ), + ] + ] + value: typing_extensions.Required[ + typing_extensions.Annotated[ + int, typing_extensions.Doc("Integer value of the amount.\nMin: 0") + ] + ] + + +CreateReaderCheckoutRequestTotalAmountInput = CreateReaderCheckoutRequestTotalAmountDict + + class CreateReaderCheckoutRequest(pydantic.BaseModel): """ Reader Checkout @@ -1516,6 +1788,82 @@ class CreateReaderCheckoutRequest(pydantic.BaseModel): """ +class CreateReaderCheckoutRequestDict(typing_extensions.TypedDict, total=False): + total_amount: typing_extensions.Required[ + typing_extensions.Annotated[ + CreateReaderCheckoutRequestTotalAmountInput, + typing_extensions.Doc( + "Amount structure.\n\nThe amount is represented as an integer value altogether with the currency and the minor unit.\n\nFor example, EUR 1.00 is represented as value 100 with minor unit of 2." + ), + ] + ] + aade: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutRequestAadeInput, + typing_extensions.Doc( + "Optional object containing data for transactions from ERP integrators in Greece that comply with the AADE1155 protocol.\nWhen such regulatory/business requirements apply, this object must be provided and contains the data needed tovalidate the transaction with the AADE signature provider." + ), + ] + ] + affiliate: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutRequestAffiliateInput, + typing_extensions.Doc( + "Affiliate metadata for the transaction.\nIt is a field that allow for integrators to track the source of the transaction." + ), + ] + ] + card_type: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CreateReaderCheckoutRequestCardTypeInput, + typing_extensions.Doc( + "The card type of the card used for the transaction.\nIs is required only for some countries (e.g: Brazil)." + ), + ] + ] + description: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc("Description of the checkout to be shown in the Merchant Sales"), + ] + ] + installments: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "Number of installments for the transaction.\nIt may vary according to the merchant country.\nFor example, in Brazil, the maximum number of installments is 12.\n\nOmit if the merchant country does support installments.\nOtherwise, the checkout will be rejected.\nMin: 1" + ), + ] + ] + return_url: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Webhook URL to which the payment result will be sent.\nIt must be a HTTPS url.\nFormat: uri" + ), + ] + ] + tip_rates: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + typing.Sequence[float], + typing_extensions.Doc( + "List of tipping rates to be displayed to the cardholder.\nThe rates are in percentage and should be between 0.01 and 0.99.\nThe list should be sorted in ascending order." + ), + ] + ] + tip_timeout: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "Time in seconds the cardholder has to select a tip rate.\nIf not provided, the default value is 30 seconds.\n\nIt can only be set if `tip_rates` is provided.\n\n**Note**: If the target device is a Solo, it must be in version 3.3.38.0 or higher.\nDefault: 30\n\nMin: 30\nMax: 120" + ), + ] + ] + + +CreateReaderCheckoutRequestInput = CreateReaderCheckoutRequestDict + + class CreateReaderCheckoutResponseData(pydantic.BaseModel): """ CreateReaderCheckoutResponseData is a schema definition. @@ -1632,6 +1980,42 @@ class PersonalDetails(pydantic.BaseModel): """ +class PersonalDetailsDict(typing_extensions.TypedDict, total=False): + address: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + AddressLegacyInput, typing_extensions.Doc("Profile's personal address information.") + ] + ] + birth_date: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + datetime.date, typing_extensions.Doc("Date of birth of the customer.\nFormat: date") + ] + ] + email: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("Email address of the customer.")] + ] + first_name: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("First name of the customer.")] + ] + last_name: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("Last name of the customer.")] + ] + phone: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, typing_extensions.Doc("Phone number of the customer.")] + ] + tax_id: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "An identification number user for tax purposes (e.g. CPF)\nMax length: 255" + ), + ] + ] + + +PersonalDetailsInput = PersonalDetailsDict + + class Customer(pydantic.BaseModel): """ Saved customer details. @@ -1648,6 +2032,20 @@ class Customer(pydantic.BaseModel): """ +class CustomerDict(typing_extensions.TypedDict, total=False): + customer_id: typing_extensions.Required[ + typing_extensions.Annotated[str, typing_extensions.Doc("Unique ID of the customer.")] + ] + personal_details: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + PersonalDetailsInput, typing_extensions.Doc("Personal details for the customer.") + ] + ] + + +CustomerInput = CustomerDict + + class DetailsErrorFailedConstraint(pydantic.BaseModel): """ DetailsErrorFailedConstraint is a schema definition. @@ -2112,6 +2510,7 @@ class ListPersonsResponseBody(pydantic.BaseModel): """ MandatePayloadType = typing.Union[typing.Literal["recurrent"], str] +MandatePayloadTypeInput = MandatePayloadType class MandatePayload(pydantic.BaseModel): @@ -2135,9 +2534,35 @@ class MandatePayload(pydantic.BaseModel): """ +class MandatePayloadDict(typing_extensions.TypedDict, total=False): + type: typing_extensions.Required[ + typing_extensions.Annotated[ + MandatePayloadTypeInput, + typing_extensions.Doc("Type of mandate to create for the saved payment instrument."), + ] + ] + user_agent: typing_extensions.Required[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Browser or client user agent observed when consent was collected." + ), + ] + ] + user_ip: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, typing_extensions.Doc("IP address of the payer when the mandate was accepted.") + ] + ] + + +MandatePayloadInput = MandatePayloadDict + + MembershipStatus = typing.Union[ typing.Literal["accepted", "disabled", "expired", "pending", "unknown"], str ] +MembershipStatusInput = MembershipStatus class MembershipUserClassic(pydantic.BaseModel): @@ -2206,6 +2631,7 @@ class MembershipUser(pydantic.BaseModel): Metadata = dict[str, object] +MetadataInput = typing.Mapping[str, object] """ Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, alwayssubmit whole metadata. Maximum of 64 parameters are allowed in the object. Max properties: 64 @@ -2271,6 +2697,7 @@ class Member(pydantic.BaseModel): ResourceType = str +ResourceTypeInput = str """ The type of the membership resource. Possible values are: @@ -2737,13 +3164,16 @@ def additional_properties(self, value: dict[str, object]) -> None: ProcessCheckoutPaymentType = typing.Union[ typing.Literal["apple_pay", "bancontact", "blik", "boleto", "card", "google_pay", "ideal"], str ] +ProcessCheckoutPaymentTypeInput = ProcessCheckoutPaymentType ProcessCheckoutGooglePay = dict[str, object] +ProcessCheckoutGooglePayInput = typing.Mapping[str, object] """ Raw `PaymentData` object received from Google Pay. Send the Google Pay response payload as-is. """ ProcessCheckoutApplePay = dict[str, object] +ProcessCheckoutApplePayInput = typing.Mapping[str, object] """ Raw payment token object received from Apple Pay. Send the Apple Pay response payload as-is. """ @@ -2802,6 +3232,81 @@ class ProcessCheckout(pydantic.BaseModel): """ +class ProcessCheckoutDict(typing_extensions.TypedDict, total=False): + payment_type: typing_extensions.Required[ + typing_extensions.Annotated[ + ProcessCheckoutPaymentTypeInput, + typing_extensions.Doc( + "Payment method used for this processing attempt. It determines which additional request fields are required." + ), + ] + ] + apple_pay: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + ProcessCheckoutApplePayInput, + typing_extensions.Doc( + "Raw payment token object received from Apple Pay. Send the Apple Pay response payload as-is." + ), + ] + ] + card: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + CardInput, + typing_extensions.Doc( + "__Required when payment type is `card`.__ Details of the payment card." + ), + ] + ] + customer_id: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Customer identifier associated with the saved payment instrument. Required when `token` is provided." + ), + ] + ] + google_pay: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + ProcessCheckoutGooglePayInput, + typing_extensions.Doc( + "Raw `PaymentData` object received from Google Pay. Send the Google Pay response payload as-is." + ), + ] + ] + installments: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + int, + typing_extensions.Doc( + "Number of installments for deferred payments. Available only to merchant users in Brazil.\nMin: 1\nMax: 12" + ), + ] + ] + mandate: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + MandatePayloadInput, + typing_extensions.Doc( + "Mandate details used when a checkout should create a reusable card token for future recurring or merchant-initiated payments." + ), + ] + ] + personal_details: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + PersonalDetailsInput, typing_extensions.Doc("Personal details for the customer.") + ] + ] + token: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + str, + typing_extensions.Doc( + "Saved-card token to use instead of raw card details when processing with a previously stored payment instrument." + ), + ] + ] + + +ProcessCheckoutInput = ProcessCheckoutDict + + class Product(pydantic.BaseModel): """ Purchase product. @@ -2885,6 +3390,7 @@ class ReaderDevice(pydantic.BaseModel): ReaderId = str +ReaderIdInput = str """ Unique identifier of the object. @@ -2894,6 +3400,7 @@ class ReaderDevice(pydantic.BaseModel): """ ReaderName = str +ReaderNameInput = str """ Custom human-readable, user-defined name for easier identification of the reader. Max length: 500 @@ -3024,6 +3531,7 @@ class ReaderCheckoutStatusChange(pydantic.BaseModel): ReaderPairingCode = str +ReaderPairingCodeInput = str """ The pairing code is a 8 or 9 character alphanumeric string that is displayed on a SumUp Device after initiatingthe pairing. It is used to link the physical device to the created pairing. Min length: 8 diff --git a/tests/test_query_params.py b/tests/test_query_params.py index 17f4fab9..7b0ba00a 100644 --- a/tests/test_query_params.py +++ b/tests/test_query_params.py @@ -2,21 +2,21 @@ import pytest import typing -from sumup.transactions import ListTransactionsV21Params +from sumup._service import NotGivenType @pytest.mark.parametrize( - ("params", "expected_query_items"), + ("kwargs", "expected_query_items"), [ - (ListTransactionsV21Params(limit=10), [("limit", "10")]), - (None, []), + ({"limit": 10}, [("limit", "10")]), + ({}, []), ( - ListTransactionsV21Params(statuses=["SUCCESSFUL", "FAILED"]), + {"statuses": ["SUCCESSFUL", "FAILED"]}, [("statuses[]", "SUCCESSFUL"), ("statuses[]", "FAILED")], ), ], ) -def test_transactions_list_query_params(params, expected_query_items, sdk_factory): +def test_transactions_list_query_params(kwargs, expected_query_items, sdk_factory): captured_request: dict[str, httpx.Request] = {} def handler(request: httpx.Request) -> httpx.Response: @@ -24,10 +24,6 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json={"items": []}) sdk = sdk_factory(handler) - kwargs = {} - if params is not None: - kwargs["params"] = params - response = sdk.transactions.list("merchant-123", **kwargs) assert response.items == [] @@ -38,11 +34,18 @@ def handler(request: httpx.Request) -> httpx.Response: def test_transactions_list_query_param_enums_are_typed(): - order_annotation = ListTransactionsV21Params.model_fields["order"].annotation + annotations = sdk_annotations() + order_annotation = annotations["order"] assert typing.get_origin(order_annotation) is typing.Union assert typing.get_args(order_annotation) == ( typing.Literal["ascending", "descending"], str, - type(None), + NotGivenType, ) + + +def sdk_annotations(): + from sumup.transactions.resource import TransactionsResource + + return typing.get_type_hints(TransactionsResource.list) diff --git a/tests/test_request_bodies.py b/tests/test_request_bodies.py index ae4a084a..abd6987c 100644 --- a/tests/test_request_bodies.py +++ b/tests/test_request_bodies.py @@ -1,10 +1,9 @@ +import datetime import json +import warnings import httpx -from sumup.readers import CreateReaderCheckoutBody, CreateReaderCheckoutBodyTotalAmount -from sumup.subaccounts import UpdateSubAccountBody - def test_create_reader_checkout_omits_null_fields(sdk_factory): captured_request: dict[str, httpx.Request] = {} @@ -14,12 +13,12 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(201, json={"data": {"client_transaction_id": "txn-123"}}) sdk = sdk_factory(handler) - body = CreateReaderCheckoutBody( - total_amount=CreateReaderCheckoutBodyTotalAmount(currency="EUR", minor_unit=2, value=1500) + response = sdk.readers.create_checkout( + "merchant-123", + "reader-456", + total_amount={"currency": "EUR", "minor_unit": 2, "value": 1500}, ) - response = sdk.readers.create_checkout("merchant-123", "reader-456", body) - assert response.data.client_transaction_id == "txn-123" assert "request" in captured_request request = captured_request["request"] @@ -29,6 +28,34 @@ def handler(request: httpx.Request) -> httpx.Response: } +def test_create_checkout_serializes_datetime_fields(sdk_factory): + captured_request: dict[str, httpx.Request] = {} + + def handler(request: httpx.Request) -> httpx.Response: + captured_request["request"] = request + return httpx.Response(201, json={"id": "checkout-123"}) + + sdk = sdk_factory(handler) + response = sdk.checkouts.create( + amount=15.0, + checkout_reference="checkout-ref", + currency="EUR", + merchant_code="merchant-123", + valid_until=datetime.datetime(2024, 1, 2, 3, 4, 5), + ) + + assert response.id == "checkout-123" + assert "request" in captured_request + request = captured_request["request"] + assert json.loads(request.content) == { + "amount": 15.0, + "checkout_reference": "checkout-ref", + "currency": "EUR", + "merchant_code": "merchant-123", + "valid_until": "2024-01-02T03:04:05", + } + + def test_update_subaccount_accepts_explicit_null_fields(sdk_factory): captured_request: dict[str, httpx.Request] = {} @@ -55,9 +82,9 @@ def handler(request: httpx.Request) -> httpx.Response: ) sdk = sdk_factory(handler) - body = UpdateSubAccountBody(nickname=None) - - response = sdk.subaccounts.update_sub_account(1, body) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + response = sdk.subaccounts.update_sub_account(1, nickname=None) assert response.nickname is None assert "request" in captured_request diff --git a/tests/test_secret.py b/tests/test_secret.py index 4ea59d80..5533af2c 100644 --- a/tests/test_secret.py +++ b/tests/test_secret.py @@ -1,5 +1,5 @@ from sumup._secret import Secret -from sumup.members.resource import CreateMerchantMemberBody +from sumup._service import serialize_request_data def test_secret_masks_repr_but_preserves_value() -> None: @@ -11,17 +11,17 @@ def test_secret_masks_repr_but_preserves_value() -> None: assert secret == "super-secret" -def test_password_fields_use_secret_and_dump_plain_text() -> None: - body = CreateMerchantMemberBody( - email="user@example.com", - roles=["role_manager"], - is_managed_user=True, - password=Secret("super-secret"), - ) +def test_request_serialization_dumps_secret_plain_text() -> None: + payload = { + "email": "user@example.com", + "password": Secret("super-secret"), + "nested": {"password": Secret("super-secret")}, + } - assert isinstance(body.password, Secret) - dumped = body.model_dump() - assert dumped["password"] == "super-secret" + dumped = serialize_request_data(payload) - body_repr = repr(body) - assert "super-secret" not in body_repr + assert dumped == { + "email": "user@example.com", + "password": "super-secret", + "nested": {"password": "super-secret"}, + }