Skip to content

Commit

Permalink
[python] Fixes file upload + download, adds tests (#8437)
Browse files Browse the repository at this point in the history
* Adds tests for file upload and files upload

* Adds test_download_attachment

* Fixes test_upload_file

* Adds download_attachment endpoint

* Adds test_download_attachment

* Updates assert_request_called_with signature

* Samples regen

* Adds upload download file spec route and sample gen

* Fixes file upload for application/octet-stream, writes test_upload_download_file

* Changes if into elif

* Improves python code in api_client
  • Loading branch information
spacether authored Jan 15, 2021
1 parent 9914425 commit c4dbd2c
Show file tree
Hide file tree
Showing 14 changed files with 1,154 additions and 580 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,6 @@ class ApiClient(object):
e.body = e.body.decode('utf-8')
raise e

content_type = response_data.getheader('content-type')

self.last_response = response_data

return_data = response_data
Expand All @@ -214,15 +212,17 @@ class ApiClient(object):
{{/tornado}}
return return_data

if response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)

# deserialize response data
if response_type:
if response_type != (file_type,):
encoding = "utf-8"
content_type = response_data.getheader('content-type')
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
if match:
encoding = match.group(1)
response_data.data = response_data.data.decode(encoding)

return_data = self.deserialize(
response_data,
response_type,
Expand Down Expand Up @@ -268,21 +268,24 @@ class ApiClient(object):

@classmethod
def sanitize_for_serialization(cls, obj):
"""Builds a JSON POST object.
"""Prepares data for transmission before it is sent with the rest client
If obj is None, return None.
If obj is str, int, long, float, bool, return directly.
If obj is datetime.datetime, datetime.date
convert to string in iso8601 format.
If obj is list, sanitize each element in the list.
If obj is dict, return the dict.
If obj is OpenAPI model, return the properties dict.
If obj is io.IOBase, return the bytes
:param obj: The data to serialize.
:return: The serialized form of data.
"""
if isinstance(obj, (ModelNormal, ModelComposed)):
return {
key: cls.sanitize_for_serialization(val) for key, val in model_to_dict(obj, serialize=True).items()
}
elif isinstance(obj, io.IOBase):
return cls.get_file_data_and_close_file(obj)
elif isinstance(obj, (str, int, float, none_type, bool)):
return obj
elif isinstance(obj, (datetime, date)):
Expand Down Expand Up @@ -526,6 +529,12 @@ class ApiClient(object):
new_params.append((k, v))
return new_params

@staticmethod
def get_file_data_and_close_file(file_instance: io.IOBase) -> bytes:
file_data = file_instance.read()
file_instance.close()
return file_data

def files_parameters(self, files: typing.Optional[typing.Dict[str, typing.List[io.IOBase]]] = None):
"""Builds form parameters.

Expand All @@ -551,12 +560,11 @@ class ApiClient(object):
"for %s must be open." % param_name
)
filename = os.path.basename(file_instance.name)
filedata = file_instance.read()
filedata = self.get_file_data_and_close_file(file_instance)
mimetype = (mimetypes.guess_type(filename)[0] or
'application/octet-stream')
params.append(
tuple([param_name, tuple([filename, filedata, mimetype])]))
file_instance.close()

return params

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ tags:
description: Access to Petstore orders
- name: user
description: Operations about user
- name: fake
description: Fake api used for feature testing
paths:
/foo:
get:
Expand Down Expand Up @@ -246,45 +248,6 @@ paths:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}/uploadImage':
post:
tags:
- pet
summary: uploads an image
description: ''
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
additionalMetadata:
description: Additional data to pass to server
type: string
file:
description: file to upload
type: string
format: binary
/store/inventory:
get:
tags:
Expand Down Expand Up @@ -1196,32 +1159,48 @@ paths:
responses:
"200":
description: Success
'/fake/{petId}/uploadImageWithRequiredFile':
post:
/{fileName}:
get:
servers:
- url: http://www.jtricks.com
tags:
- pet
summary: uploads an image (required)
description: ''
operationId: uploadFileWithRequiredFile
- fake
summary: downloads a file using Content-Disposition
operationId: downloadAttachment
parameters:
- name: petId
- name: fileName
in: path
description: ID of pet to update
description: file name
required: true
schema:
type: integer
format: int64
type: string
responses:
200:
description: successful operation
content:
'text/plain':
schema:
type: string
format: binary
headers:
Content-Disposition:
schema:
type: string
description: "describes the received file. Looks like: 'attachment; filename=fileName.txt'"
/fake/uploadFile:
post:
tags:
- fake
summary: uploads a file using multipart/form-data
description: ''
operationId: uploadFile
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
multipart/form-data:
Expand All @@ -1231,12 +1210,61 @@ paths:
additionalMetadata:
description: Additional data to pass to server
type: string
requiredFile:
file:
description: file to upload
type: string
format: binary
required:
- requiredFile
- file
/fake/uploadFiles:
post:
tags:
- fake
summary: uploads files using multipart/form-data
description: ''
operationId: uploadFiles
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
files:
type: array
items:
type: string
format: binary
/fake/uploadDownloadFile:
post:
tags:
- fake
summary: uploads a file and downloads a file using application/octet-stream
description: ''
operationId: uploadDownloadFile
responses:
'200':
description: successful operation
content:
application/octet-stream:
schema:
type: string
format: binary
description: file to download
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
description: file to upload
/fake/health:
get:
tags:
Expand Down
32 changes: 20 additions & 12 deletions samples/client/petstore/python/petstore_api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,6 @@ def __call_api(
e.body = e.body.decode('utf-8')
raise e

content_type = response_data.getheader('content-type')

self.last_response = response_data

return_data = response_data
Expand All @@ -211,15 +209,17 @@ def __call_api(
return (return_data)
return return_data

if response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)

# deserialize response data
if response_type:
if response_type != (file_type,):
encoding = "utf-8"
content_type = response_data.getheader('content-type')
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
if match:
encoding = match.group(1)
response_data.data = response_data.data.decode(encoding)

return_data = self.deserialize(
response_data,
response_type,
Expand Down Expand Up @@ -256,21 +256,24 @@ def parameters_to_multipart(self, params, collection_types):

@classmethod
def sanitize_for_serialization(cls, obj):
"""Builds a JSON POST object.
"""Prepares data for transmission before it is sent with the rest client
If obj is None, return None.
If obj is str, int, long, float, bool, return directly.
If obj is datetime.datetime, datetime.date
convert to string in iso8601 format.
If obj is list, sanitize each element in the list.
If obj is dict, return the dict.
If obj is OpenAPI model, return the properties dict.
If obj is io.IOBase, return the bytes
:param obj: The data to serialize.
:return: The serialized form of data.
"""
if isinstance(obj, (ModelNormal, ModelComposed)):
return {
key: cls.sanitize_for_serialization(val) for key, val in model_to_dict(obj, serialize=True).items()
}
elif isinstance(obj, io.IOBase):
return cls.get_file_data_and_close_file(obj)
elif isinstance(obj, (str, int, float, none_type, bool)):
return obj
elif isinstance(obj, (datetime, date)):
Expand Down Expand Up @@ -514,6 +517,12 @@ def parameters_to_tuples(self, params, collection_formats):
new_params.append((k, v))
return new_params

@staticmethod
def get_file_data_and_close_file(file_instance: io.IOBase) -> bytes:
file_data = file_instance.read()
file_instance.close()
return file_data

def files_parameters(self, files: typing.Optional[typing.Dict[str, typing.List[io.IOBase]]] = None):
"""Builds form parameters.
Expand All @@ -539,12 +548,11 @@ def files_parameters(self, files: typing.Optional[typing.Dict[str, typing.List[i
"for %s must be open." % param_name
)
filename = os.path.basename(file_instance.name)
filedata = file_instance.read()
filedata = self.get_file_data_and_close_file(file_instance)
mimetype = (mimetypes.guess_type(filename)[0] or
'application/octet-stream')
params.append(
tuple([param_name, tuple([filename, filedata, mimetype])]))
file_instance.close()

return params

Expand Down
Loading

0 comments on commit c4dbd2c

Please sign in to comment.