|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
|
|
|
|
requests_toolbelt.multipart.decoder
|
|
|
|
===================================
|
|
|
|
|
|
|
|
This holds all the implementation details of the MultipartDecoder
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import email.parser
|
|
|
|
from .encoder import encode_with
|
|
|
|
from requests.structures import CaseInsensitiveDict
|
|
|
|
|
|
|
|
|
|
|
|
def _split_on_find(content, bound):
|
|
|
|
point = content.find(bound)
|
|
|
|
return content[:point], content[point + len(bound):]
|
|
|
|
|
|
|
|
|
|
|
|
class ImproperBodyPartContentException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class NonMultipartContentTypeException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def _header_parser(string, encoding):
|
|
|
|
major = sys.version_info[0]
|
|
|
|
if major == 3:
|
|
|
|
string = string.decode(encoding)
|
|
|
|
headers = email.parser.HeaderParser().parsestr(string).items()
|
|
|
|
return (
|
|
|
|
(encode_with(k, encoding), encode_with(v, encoding))
|
|
|
|
for k, v in headers
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class BodyPart(object):
|
|
|
|
"""
|
|
|
|
|
|
|
|
The ``BodyPart`` object is a ``Response``-like interface to an individual
|
|
|
|
subpart of a multipart response. It is expected that these will
|
|
|
|
generally be created by objects of the ``MultipartDecoder`` class.
|
|
|
|
|
|
|
|
Like ``Response``, there is a ``CaseInsensitiveDict`` object named headers,
|
|
|
|
``content`` to access bytes, ``text`` to access unicode, and ``encoding``
|
|
|
|
to access the unicode codec.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, content, encoding):
|
|
|
|
self.encoding = encoding
|
|
|
|
headers = {}
|
|
|
|
# Split into header section (if any) and the content
|
|
|
|
if b'\r\n\r\n' in content:
|
|
|
|
first, self.content = _split_on_find(content, b'\r\n\r\n')
|
|
|
|
if first != b'':
|
|
|
|
headers = _header_parser(first.lstrip(), encoding)
|
|
|
|
else:
|
|
|
|
raise ImproperBodyPartContentException(
|
|
|
|
'content does not contain CR-LF-CR-LF'
|
|
|
|
)
|
|
|
|
self.headers = CaseInsensitiveDict(headers)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def text(self):
|
|
|
|
"""Content of the ``BodyPart`` in unicode."""
|
|
|
|
return self.content.decode(self.encoding)
|
|
|
|
|
|
|
|
|
|
|
|
class MultipartDecoder(object):
|
|
|
|
"""
|
|
|
|
|
|
|
|
The ``MultipartDecoder`` object parses the multipart payload of
|
|
|
|
a bytestring into a tuple of ``Response``-like ``BodyPart`` objects.
|
|
|
|
|
|
|
|
The basic usage is::
|
|
|
|
|
|
|
|
import requests
|
|
|
|
from requests_toolbelt import MultipartDecoder
|
|
|
|
|
|
|
|
response = request.get(url)
|
|
|
|
decoder = MultipartDecoder.from_response(response)
|
|
|
|
for part in decoder.parts:
|
|
|
|
print(part.headers['content-type'])
|
|
|
|
|
|
|
|
If the multipart content is not from a response, basic usage is::
|
|
|
|
|
|
|
|
from requests_toolbelt import MultipartDecoder
|
|
|
|
|
|
|
|
decoder = MultipartDecoder(content, content_type)
|
|
|
|
for part in decoder.parts:
|
|
|
|
print(part.headers['content-type'])
|
|
|
|
|
|
|
|
For both these usages, there is an optional ``encoding`` parameter. This is
|
|
|
|
a string, which is the name of the unicode codec to use (default is
|
|
|
|
``'utf-8'``).
|
|
|
|
|
|
|
|
"""
|
|
|
|
def __init__(self, content, content_type, encoding='utf-8'):
|
|
|
|
#: Original Content-Type header
|
|
|
|
self.content_type = content_type
|
|
|
|
#: Response body encoding
|
|
|
|
self.encoding = encoding
|
|
|
|
#: Parsed parts of the multipart response body
|
|
|
|
self.parts = tuple()
|
|
|
|
self._find_boundary()
|
|
|
|
self._parse_body(content)
|
|
|
|
|
|
|
|
def _find_boundary(self):
|
|
|
|
ct_info = tuple(x.strip() for x in self.content_type.split(';'))
|
|
|
|
mimetype = ct_info[0]
|
|
|
|
if mimetype.split('/')[0].lower() != 'multipart':
|
|
|
|
raise NonMultipartContentTypeException(
|
|
|
|
"Unexpected mimetype in content-type: '{0}'".format(mimetype)
|
|
|
|
)
|
|
|
|
for item in ct_info[1:]:
|
|
|
|
attr, value = _split_on_find(
|
|
|
|
item,
|
|
|
|
'='
|
|
|
|
)
|
|
|
|
if attr.lower() == 'boundary':
|
|
|
|
self.boundary = encode_with(value.strip('"'), self.encoding)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _fix_first_part(part, boundary_marker):
|
|
|
|
bm_len = len(boundary_marker)
|
|
|
|
if boundary_marker == part[:bm_len]:
|
|
|
|
return part[bm_len:]
|
|
|
|
else:
|
|
|
|
return part
|
|
|
|
|
|
|
|
def _parse_body(self, content):
|
|
|
|
boundary = b''.join((b'--', self.boundary))
|
|
|
|
|
|
|
|
def body_part(part):
|
|
|
|
fixed = MultipartDecoder._fix_first_part(part, boundary)
|
|
|
|
return BodyPart(fixed, self.encoding)
|
|
|
|
|
|
|
|
def test_part(part):
|
|
|
|
return (part != b'' and
|
|
|
|
part != b'\r\n' and
|
|
|
|
part[:4] != b'--\r\n' and
|
|
|
|
part != b'--')
|
|
|
|
|
|
|
|
parts = content.split(b''.join((b'\r\n', boundary)))
|
|
|
|
self.parts = tuple(body_part(x) for x in parts if test_part(x))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_response(cls, response, encoding='utf-8'):
|
|
|
|
content = response.content
|
|
|
|
content_type = response.headers.get('content-type', None)
|
|
|
|
return cls(content, content_type, encoding)
|