Repository: minio/minio-py

Issue:
Part order error with fput_object
When trying to upload a 200MB file to minio server with minio-py I'm getting an error.

minio-py  version 2.2.5
minio server RELEASE.2017-09-29T19-16-56Z

```
File "/usr/local/lib/python3.6/dist-packages/minio/api.py", line 561, in fput_object
    content_type, metadata)
  File "/usr/local/lib/python3.6/dist-packages/minio/api.py", line 767, in put_object
    data, length, metadata=metadata)
  File "/usr/local/lib/python3.6/dist-packages/minio/api.py", line 1561, in _stream_put_object
    metadata=metadata)
  File "/usr/local/lib/python3.6/dist-packages/minio/api.py", line 1632, in _complete_multipart_upload
    content_sha256=sha256_hex)
  File "/usr/local/lib/python3.6/dist-packages/minio/api.py", line 1781, in _url_open
    object_name).get_exception()
minio.error.InvalidPartOrder: InvalidPartOrder: message: The list of parts was not in ascending order.Parts list must specified in order by part number.
```

Maybe related to this commit?
https://github.com/minio/minio-py/pull/563

Discussion:
poornas: @vadmeste, appveyor and travis are both failing.
vadmeste: Fixed @poornas 

Patch:
diff --git a/minio/api.py b/minio/api.py
index cb7e207..3c29b3a 100644
--- a/minio/api.py
+++ b/minio/api.py
@@ -1605,7 +1605,12 @@ class Minio(object):
         is_non_empty_string(object_name)
         is_non_empty_string(upload_id)
 
-        data = xml_marshal_complete_multipart_upload(uploaded_parts)
+        # Order uploaded parts as required by S3 specification
+        ordered_parts = []
+        for part in sorted(uploaded_parts.keys()):
+            ordered_parts.append(uploaded_parts[part])
+
+        data = xml_marshal_complete_multipart_upload(ordered_parts)
         sha256_hex = get_sha256_hexdigest(data)
         md5_base64 = get_md5_base64digest(data)
 
diff --git a/minio/xml_marshal.py b/minio/xml_marshal.py
index e450d6b..9c62aa0 100644
--- a/minio/xml_marshal.py
+++ b/minio/xml_marshal.py
@@ -51,17 +51,17 @@ def xml_marshal_complete_multipart_upload(uploaded_parts):
     """
     Marshal's complete multipart upload request based on *uploaded_parts*.
 
-    :param uploaded_parts: List of all uploaded parts ordered in the
-           way they were uploaded.
+    :param uploaded_parts: List of all uploaded parts, ordered by part number.
     :return: Marshalled XML data.
     """
     root = s3_xml.Element('CompleteMultipartUpload', {'xmlns': _S3_NAMESPACE})
-    for part_number in uploaded_parts.keys():
+    for uploaded_part in uploaded_parts:
+        part_number = uploaded_part.part_number
         part = s3_xml.SubElement(root, 'Part')
         part_num = s3_xml.SubElement(part, 'PartNumber')
         part_num.text = str(part_number)
         etag = s3_xml.SubElement(part, 'ETag')
-        etag.text = '"' + uploaded_parts[part_number].etag + '"'
+        etag.text = '"' + uploaded_part.etag + '"'
         data = io.BytesIO()
         s3_xml.ElementTree(root).write(data, encoding=None,
                                        xml_declaration=False)


Test patch:
diff --git a/tests/unit/generate_xml_test.py b/tests/unit/generate_xml_test.py
index 18706b9..bdd1dfd 100644
--- a/tests/unit/generate_xml_test.py
+++ b/tests/unit/generate_xml_test.py
@@ -33,17 +33,16 @@ class GenerateRequestTest(TestCase):
                           b'<Part><PartNumber>2</PartNumber><ETag>"0c78aef83f66abc1fa1e8477f296d394"</ETag>' \
                           b'</Part><Part><PartNumber>3</PartNumber><ETag>"acbd18db4cc2f85cedef654fccc4a4d8"' \
                           b'</ETag></Part></CompleteMultipartUpload>'
-        etags = {}
-        etags = {
-            1: UploadPart('bucket', 'object', 'upload_id', 1,
+        etags = [
+            UploadPart('bucket', 'object', 'upload_id', 1,
                             'a54357aff0632cce46d942af68356b38',
                             None, 0),
-            2: UploadPart('bucket', 'object', 'upload_id', 2,
+            UploadPart('bucket', 'object', 'upload_id', 2,
                             '0c78aef83f66abc1fa1e8477f296d394',
                             None, 0),
-            3: UploadPart('bucket', 'object', 'upload_id', 3,
+            UploadPart('bucket', 'object', 'upload_id', 3,
                             'acbd18db4cc2f85cedef654fccc4a4d8',
                             None, 0),
-        }
+        ]
         actual_string = xml_marshal_complete_multipart_upload(etags)
         eq_(expected_string, actual_string)