one name to catch them all

today we were trying to do a multipart/form-data POST to one of our webservices using the usually rather usable python requests. the spec of the webservice required that multiple files were to be POSTed as multiparts with the name files and individual filename attributes — a task that python request according to its doc did not really seem to support:

>>> url = 'http://httpbin.org/post'
>>> files = {'file': ('report.xls', open('report.xls', 'rb'))}

>>> r = requests.post(url, files=files)
>>> r.text
{
  ...
  "files": {
    "file": "<censored...binary...data>"
  },
  ...
}

since the files parameter is a dict, it’s not really possible to add the files keyword twice. several searches and attempts (tried listing tuples as value of the key, didn’t really work) later we resorted to tracing into the requests.post() call to see what was going on down in the engine room of requests. lo and behold, we did come across the following inspiring piece of code in requests’s _encode_files method:

for (k, v) in files:
            # support for explicit filename
            ft = None
            fh = None
            if isinstance(v, (tuple, list)):
                if len(v) == 2:
                    fn, fp = v
                elif len(v) == 3:
                    fn, fp, ft = v
                else:
                    fn, fp, ft, fh = v
            else:
                fn = guess_filename(v) or k
                fp = v
            if isinstance(fp, str):
                fp = StringIO(fp)
            if isinstance(fp, bytes):
                fp = BytesIO(fp)

            rf = RequestField(name=k, data=fp.read(),
                              filename=fn, headers=fh)
            rf.make_multipart(content_type=ft)
            new_fields.append(rf)

the inspiring bit is the for (k, v) in files: part — this is iterating over a dict (as illustrated by the doc), but that should also work with a list of tuples of (name, file object)!

sure enough: this piece of code:

import requests
r = requests.post('http://127.0.0.1:8008/service/recorddata/write', 
                  files=[('files', open('/tmp/hurz.txt')),
                         ('files', open('/tmp/wuff.txt'))])

yields the desired result:

POST /service/recorddata/write HTTP/1.1
Host: 127.0.0.1:8008
Content-Length: 286
Content-Type: multipart/form-data; boundary=c282f7a56d3941c1bcfc2c43cf991f67
Accept-Encoding: gzip, deflate, compress
Accept: */*User-Agent: python-requests/2.2.0 CPython/2.7.3 Linux/3.2.0-58-generic-pae
Connection: keep-alive

--c282f7a56d3941c1bcfc2c43cf991f67Content-Disposition: form-data; name="files"; filename="hurz.txt"

HurzHurzHurzHurz

--c282f7a56d3941c1bcfc2c43cf991f67
Content-Disposition: form-data; name="files"; filename="wuff.txt"

WuffWuffWuffWuff

--c282f7a56d3941c1bcfc2c43cf991f67-

Voila!