Search code examples
pythonswaggerswagger-codegen

How to sind a binary string with a python swagger client


From an OpenAPI definition file I generated a Swagger client for Python using Swagger-UI. I'm having trouble using a POST function that requires a request body. What would be the correct approach here?

The definition defines the following:

"/xyz" : {
  "post" : {
    "tags" : [ "xyz" ],
    "operationId" : "xyz",
    "requestBody" : {
      "content" : {
        "multipart/form-data" : {
          "schema" : {
            "required" : [ "d1", "d2" ],
            "type" : "object",
            "properties" : {
              "d1" : {
                "format" : "binary",
                "type" : "string"
              },
              "d2" : {
                "format" : "email",
                "type" : "string",
                "nullable" : false
              }
            }
          }
        }
      },
      "required" : true
    },

 

The method exists:

my_api.xyz(...)

But I dont know how to use the paramters. I found out, the the method needs a body parameter:

my_api.xyz(body)

d1 have to be the content of a xml-file.


Solution

  • I pasted your example schema into https://editor-next.swagger.io/ and edited enough to generate a Python client, by adding the wrapper object like:

    {
      "openapi": "3.0.3",
      "paths": {
        "/xyz": {
          "post": {
            "tags": [ "xyz" ],
            "operationId": "xyz",
            "requestBody": {
              "content": {
                "multipart/form-data": {
                  "schema": {
                    "required": [ "d1", "d2" ],
                    "type": "object",
                    "properties": {
                      "d1": {
                        "format": "binary",
                        "type": "string"
                      },
                      "d2": {
                        "format": "email",
                        "type": "string",
                        "nullable": false
                      }
                    }
                  }
                }
              },
              "required" : true
            }
          }
        }
      }
    }
    

    In the zip of files that it generates is swagger_client/api/xyz_api.py

    from __future__ import absolute_import
    
    import re  # noqa: F401
    
    # python 2 and python 3 compatibility library
    import six
    
    from swagger_client.api_client import ApiClient
    
    
    class XyzApi(object):
        """NOTE: This class is auto generated by the swagger code generator program.
    
        Do not edit the class manually.
        Ref: https://github.com/swagger-api/swagger-codegen
        """
    
        def __init__(self, api_client=None):
            if api_client is None:
                api_client = ApiClient()
            self.api_client = api_client
    
        def xyz(self, d1, d2, **kwargs):  # noqa: E501
            """xyz  # noqa: E501
    
            This method makes a synchronous HTTP request by default. To make an
            asynchronous HTTP request, please pass async_req=True
            >>> thread = api.xyz(d1, d2, async_req=True)
            >>> result = thread.get()
    
            :param async_req bool
            :param str d1: (required)
            :param str d2: (required)
            :return: None
                     If the method is called asynchronously,
                     returns the request thread.
            """
            kwargs['_return_http_data_only'] = True
            if kwargs.get('async_req'):
                return self.xyz_with_http_info(d1, d2, **kwargs)  # noqa: E501
            else:
                (data) = self.xyz_with_http_info(d1, d2, **kwargs)  # noqa: E501
                return data
    
        def xyz_with_http_info(self, d1, d2, **kwargs):  # noqa: E501
            """xyz  # noqa: E501
    
            This method makes a synchronous HTTP request by default. To make an
            asynchronous HTTP request, please pass async_req=True
            >>> thread = api.xyz_with_http_info(d1, d2, async_req=True)
            >>> result = thread.get()
    
            :param async_req bool
            :param str d1: (required)
            :param str d2: (required)
            :return: None
                     If the method is called asynchronously,
                     returns the request thread.
            """
    
            all_params = ['d1', 'd2']  # noqa: E501
            all_params.append('async_req')
            all_params.append('_return_http_data_only')
            all_params.append('_preload_content')
            all_params.append('_request_timeout')
    
            params = locals()
            for key, val in six.iteritems(params['kwargs']):
                if key not in all_params:
                    raise TypeError(
                        "Got an unexpected keyword argument '%s'"
                        " to method xyz" % key
                    )
                params[key] = val
            del params['kwargs']
            # verify the required parameter 'd1' is set
            if ('d1' not in params or
                    params['d1'] is None):
                raise ValueError("Missing the required parameter `d1` when calling `xyz`")  # noqa: E501
            # verify the required parameter 'd2' is set
            if ('d2' not in params or
                    params['d2'] is None):
                raise ValueError("Missing the required parameter `d2` when calling `xyz`")  # noqa: E501
    
            collection_formats = {}
    
            path_params = {}
    
            query_params = []
    
            header_params = {}
    
            form_params = []
            local_var_files = {}
            if 'd1' in params:
                local_var_files['d1'] = params['d1']  # noqa: E501
            if 'd2' in params:
                form_params.append(('d2', params['d2']))  # noqa: E501
    
            body_params = None
            # HTTP header `Content-Type`
            header_params['Content-Type'] = self.api_client.select_header_content_type(  # noqa: E501
                ['multipart/form-data'])  # noqa: E501
    
            # Authentication setting
            auth_settings = []  # noqa: E501
    
            return self.api_client.call_api(
                '/xyz', 'POST',
                path_params,
                query_params,
                header_params,
                body=body_params,
                post_params=form_params,
                files=local_var_files,
                response_type=None,  # noqa: E501
                auth_settings=auth_settings,
                async_req=params.get('async_req'),
                _return_http_data_only=params.get('_return_http_data_only'),
                _preload_content=params.get('_preload_content', True),
                _request_timeout=params.get('_request_timeout'),
                collection_formats=collection_formats)
    

    We can see that the d1 param is treated as a file, as we'd expect for "format" : "binary" in the schema

    If we look in the generic api client implementation at swagger_client/api_client.py we can see how file params are handled:

        def __call_api(
                self, resource_path, method, path_params=None,
                query_params=None, header_params=None, body=None, post_params=None,
                files=None, response_type=None, auth_settings=None,
                _return_http_data_only=None, collection_formats=None,
                _preload_content=True, _request_timeout=None):
    
            ...
    
            # post parameters
            if post_params or files:
                post_params = self.prepare_post_parameters(post_params, files)
    

    and:

        def prepare_post_parameters(self, post_params=None, files=None):
            """Builds form parameters.
    
            :param post_params: Normal form parameters.
            :param files: File parameters.
            :return: Form parameters with files.
            """
            params = []
    
            if post_params:
                params = post_params
    
            if files:
                for k, v in six.iteritems(files):
                    if not v:
                        continue
                    file_names = v if type(v) is list else [v]
                    for n in file_names:
                        with open(n, 'rb') as f:
                            filename = os.path.basename(f.name)
                            filedata = f.read()
                            mimetype = (mimetypes.guess_type(filename)[0] or
                                        'application/octet-stream')
                            params.append(
                                tuple([k, tuple([filename, filedata, mimetype])]))
    
            return params
    

    So it appears to expect the d1 param value to be a filename (or list of filenames), i.e. path to a file on the local file system where the client is running, it then reads the content of the file and sends it in the POST body of the request.