From 9ad8589efd625fe125345a6e1508ee5be78fd547 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 03:58:26 -0500 Subject: [PATCH 01/19] ISC --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9b3bec0c..66845871 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ setup( # 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Natural Language :: English', - 'License :: OSI Approved :: MIT License', + 'License :: OSI Approved :: ISC License (ISCL)', 'Programming Language :: Python', # 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', From d961000b96d2f67de97ccee6d150c30353c9b3c4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 05:15:47 -0500 Subject: [PATCH 02/19] missed one --- requests/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/requests/core.py b/requests/core.py index 8ac748ec..e22d73cf 100644 --- a/requests/core.py +++ b/requests/core.py @@ -342,7 +342,6 @@ def delete(url, params={}, headers={}, auth=None): r.url = url r.method = 'DELETE' - # return response object r.headers = headers r.auth = _detect_auth(url, auth) From 8e8604d88bb6dc98a1fa5b0f040ba1a5b6dbc775 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 08:55:24 -0500 Subject: [PATCH 03/19] Added NOTICE for Poster vendorization --- NOTICE | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..08ba2bc6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,12 @@ +Request includes some vendorized python libraries to ease installation. + +Poster License +============== + +Copyright (c) 2010 Chris AtLee + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From df419fa6fa5db2c539145cc823c7106c00bb2b13 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 08:55:55 -0500 Subject: [PATCH 04/19] vendorized poster --- requests/packages/__init__.py | 0 requests/packages/poster/__init__.py | 32 ++ requests/packages/poster/encode.py | 414 ++++++++++++++++++++++ requests/packages/poster/streaminghttp.py | 197 ++++++++++ 4 files changed, 643 insertions(+) create mode 100644 requests/packages/__init__.py create mode 100644 requests/packages/poster/__init__.py create mode 100644 requests/packages/poster/encode.py create mode 100644 requests/packages/poster/streaminghttp.py diff --git a/requests/packages/__init__.py b/requests/packages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/requests/packages/poster/__init__.py b/requests/packages/poster/__init__.py new file mode 100644 index 00000000..857eb015 --- /dev/null +++ b/requests/packages/poster/__init__.py @@ -0,0 +1,32 @@ +# Copyright (c) 2010 Chris AtLee +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""poster module + +Support for streaming HTTP uploads, and multipart/form-data encoding + +```poster.version``` is a 3-tuple of integers representing the version number. +New releases of poster will always have a version number that compares greater +than an older version of poster. +New in version 0.6.""" + +import poster.streaminghttp +import poster.encode + +version = (0, 8, 0) # Thanks JP! diff --git a/requests/packages/poster/encode.py b/requests/packages/poster/encode.py new file mode 100644 index 00000000..cf2298d7 --- /dev/null +++ b/requests/packages/poster/encode.py @@ -0,0 +1,414 @@ +"""multipart/form-data encoding module + +This module provides functions that faciliate encoding name/value pairs +as multipart/form-data suitable for a HTTP POST or PUT request. + +multipart/form-data is the standard way to upload files over HTTP""" + +__all__ = ['gen_boundary', 'encode_and_quote', 'MultipartParam', + 'encode_string', 'encode_file_header', 'get_body_size', 'get_headers', + 'multipart_encode'] + +try: + import uuid + def gen_boundary(): + """Returns a random string to use as the boundary for a message""" + return uuid.uuid4().hex +except ImportError: + import random, sha + def gen_boundary(): + """Returns a random string to use as the boundary for a message""" + bits = random.getrandbits(160) + return sha.new(str(bits)).hexdigest() + +import urllib, re, os, mimetypes +try: + from email.header import Header +except ImportError: + # Python 2.4 + from email.Header import Header + +def encode_and_quote(data): + """If ``data`` is unicode, return urllib.quote_plus(data.encode("utf-8")) + otherwise return urllib.quote_plus(data)""" + if data is None: + return None + + if isinstance(data, unicode): + data = data.encode("utf-8") + return urllib.quote_plus(data) + +def _strify(s): + """If s is a unicode string, encode it to UTF-8 and return the results, + otherwise return str(s), or None if s is None""" + if s is None: + return None + if isinstance(s, unicode): + return s.encode("utf-8") + return str(s) + +class MultipartParam(object): + """Represents a single parameter in a multipart/form-data request + + ``name`` is the name of this parameter. + + If ``value`` is set, it must be a string or unicode object to use as the + data for this parameter. + + If ``filename`` is set, it is what to say that this parameter's filename + is. Note that this does not have to be the actual filename any local file. + + If ``filetype`` is set, it is used as the Content-Type for this parameter. + If unset it defaults to "text/plain; charset=utf8" + + If ``filesize`` is set, it specifies the length of the file ``fileobj`` + + If ``fileobj`` is set, it must be a file-like object that supports + .read(). + + Both ``value`` and ``fileobj`` must not be set, doing so will + raise a ValueError assertion. + + If ``fileobj`` is set, and ``filesize`` is not specified, then + the file's size will be determined first by stat'ing ``fileobj``'s + file descriptor, and if that fails, by seeking to the end of the file, + recording the current position as the size, and then by seeking back to the + beginning of the file. + + ``cb`` is a callable which will be called from iter_encode with (self, + current, total), representing the current parameter, current amount + transferred, and the total size. + """ + def __init__(self, name, value=None, filename=None, filetype=None, + filesize=None, fileobj=None, cb=None): + self.name = Header(name).encode() + self.value = _strify(value) + if filename is None: + self.filename = None + else: + if isinstance(filename, unicode): + # Encode with XML entities + self.filename = filename.encode("ascii", "xmlcharrefreplace") + else: + self.filename = str(filename) + self.filename = self.filename.encode("string_escape").\ + replace('"', '\\"') + self.filetype = _strify(filetype) + + self.filesize = filesize + self.fileobj = fileobj + self.cb = cb + + if self.value is not None and self.fileobj is not None: + raise ValueError("Only one of value or fileobj may be specified") + + if fileobj is not None and filesize is None: + # Try and determine the file size + try: + self.filesize = os.fstat(fileobj.fileno()).st_size + except (OSError, AttributeError): + try: + fileobj.seek(0, 2) + self.filesize = fileobj.tell() + fileobj.seek(0) + except: + raise ValueError("Could not determine filesize") + + def __cmp__(self, other): + attrs = ['name', 'value', 'filename', 'filetype', 'filesize', 'fileobj'] + myattrs = [getattr(self, a) for a in attrs] + oattrs = [getattr(other, a) for a in attrs] + return cmp(myattrs, oattrs) + + def reset(self): + if self.fileobj is not None: + self.fileobj.seek(0) + elif self.value is None: + raise ValueError("Don't know how to reset this parameter") + + @classmethod + def from_file(cls, paramname, filename): + """Returns a new MultipartParam object constructed from the local + file at ``filename``. + + ``filesize`` is determined by os.path.getsize(``filename``) + + ``filetype`` is determined by mimetypes.guess_type(``filename``)[0] + + ``filename`` is set to os.path.basename(``filename``) + """ + + return cls(paramname, filename=os.path.basename(filename), + filetype=mimetypes.guess_type(filename)[0], + filesize=os.path.getsize(filename), + fileobj=open(filename, "rb")) + + @classmethod + def from_params(cls, params): + """Returns a list of MultipartParam objects from a sequence of + name, value pairs, MultipartParam instances, + or from a mapping of names to values + + The values may be strings or file objects, or MultipartParam objects. + MultipartParam object names must match the given names in the + name,value pairs or mapping, if applicable.""" + if hasattr(params, 'items'): + params = params.items() + + retval = [] + for item in params: + if isinstance(item, cls): + retval.append(item) + continue + name, value = item + if isinstance(value, cls): + assert value.name == name + retval.append(value) + continue + if hasattr(value, 'read'): + # Looks like a file object + filename = getattr(value, 'name', None) + if filename is not None: + filetype = mimetypes.guess_type(filename)[0] + else: + filetype = None + + retval.append(cls(name=name, filename=filename, + filetype=filetype, fileobj=value)) + else: + retval.append(cls(name, value)) + return retval + + def encode_hdr(self, boundary): + """Returns the header of the encoding of this parameter""" + boundary = encode_and_quote(boundary) + + headers = ["--%s" % boundary] + + if self.filename: + disposition = 'form-data; name="%s"; filename="%s"' % (self.name, + self.filename) + else: + disposition = 'form-data; name="%s"' % self.name + + headers.append("Content-Disposition: %s" % disposition) + + if self.filetype: + filetype = self.filetype + else: + filetype = "text/plain; charset=utf-8" + + headers.append("Content-Type: %s" % filetype) + + headers.append("") + headers.append("") + + return "\r\n".join(headers) + + def encode(self, boundary): + """Returns the string encoding of this parameter""" + if self.value is None: + value = self.fileobj.read() + else: + value = self.value + + if re.search("^--%s$" % re.escape(boundary), value, re.M): + raise ValueError("boundary found in encoded string") + + return "%s%s\r\n" % (self.encode_hdr(boundary), value) + + def iter_encode(self, boundary, blocksize=4096): + """Yields the encoding of this parameter + If self.fileobj is set, then blocks of ``blocksize`` bytes are read and + yielded.""" + total = self.get_size(boundary) + current = 0 + if self.value is not None: + block = self.encode(boundary) + current += len(block) + yield block + if self.cb: + self.cb(self, current, total) + else: + block = self.encode_hdr(boundary) + current += len(block) + yield block + if self.cb: + self.cb(self, current, total) + last_block = "" + encoded_boundary = "--%s" % encode_and_quote(boundary) + boundary_exp = re.compile("^%s$" % re.escape(encoded_boundary), + re.M) + while True: + block = self.fileobj.read(blocksize) + if not block: + current += 2 + yield "\r\n" + if self.cb: + self.cb(self, current, total) + break + last_block += block + if boundary_exp.search(last_block): + raise ValueError("boundary found in file data") + last_block = last_block[-len(encoded_boundary)-2:] + current += len(block) + yield block + if self.cb: + self.cb(self, current, total) + + def get_size(self, boundary): + """Returns the size in bytes that this param will be when encoded + with the given boundary.""" + if self.filesize is not None: + valuesize = self.filesize + else: + valuesize = len(self.value) + + return len(self.encode_hdr(boundary)) + 2 + valuesize + +def encode_string(boundary, name, value): + """Returns ``name`` and ``value`` encoded as a multipart/form-data + variable. ``boundary`` is the boundary string used throughout + a single request to separate variables.""" + + return MultipartParam(name, value).encode(boundary) + +def encode_file_header(boundary, paramname, filesize, filename=None, + filetype=None): + """Returns the leading data for a multipart/form-data field that contains + file data. + + ``boundary`` is the boundary string used throughout a single request to + separate variables. + + ``paramname`` is the name of the variable in this request. + + ``filesize`` is the size of the file data. + + ``filename`` if specified is the filename to give to this field. This + field is only useful to the server for determining the original filename. + + ``filetype`` if specified is the MIME type of this file. + + The actual file data should be sent after this header has been sent. + """ + + return MultipartParam(paramname, filesize=filesize, filename=filename, + filetype=filetype).encode_hdr(boundary) + +def get_body_size(params, boundary): + """Returns the number of bytes that the multipart/form-data encoding + of ``params`` will be.""" + size = sum(p.get_size(boundary) for p in MultipartParam.from_params(params)) + return size + len(boundary) + 6 + +def get_headers(params, boundary): + """Returns a dictionary with Content-Type and Content-Length headers + for the multipart/form-data encoding of ``params``.""" + headers = {} + boundary = urllib.quote_plus(boundary) + headers['Content-Type'] = "multipart/form-data; boundary=%s" % boundary + headers['Content-Length'] = str(get_body_size(params, boundary)) + return headers + +class multipart_yielder: + def __init__(self, params, boundary, cb): + self.params = params + self.boundary = boundary + self.cb = cb + + self.i = 0 + self.p = None + self.param_iter = None + self.current = 0 + self.total = get_body_size(params, boundary) + + def __iter__(self): + return self + + def next(self): + """generator function to yield multipart/form-data representation + of parameters""" + if self.param_iter is not None: + try: + block = self.param_iter.next() + self.current += len(block) + if self.cb: + self.cb(self.p, self.current, self.total) + return block + except StopIteration: + self.p = None + self.param_iter = None + + if self.i is None: + raise StopIteration + elif self.i >= len(self.params): + self.param_iter = None + self.p = None + self.i = None + block = "--%s--\r\n" % self.boundary + self.current += len(block) + if self.cb: + self.cb(self.p, self.current, self.total) + return block + + self.p = self.params[self.i] + self.param_iter = self.p.iter_encode(self.boundary) + self.i += 1 + return self.next() + + def reset(self): + self.i = 0 + self.current = 0 + for param in self.params: + param.reset() + +def multipart_encode(params, boundary=None, cb=None): + """Encode ``params`` as multipart/form-data. + + ``params`` should be a sequence of (name, value) pairs or MultipartParam + objects, or a mapping of names to values. + Values are either strings parameter values, or file-like objects to use as + the parameter value. The file-like objects must support .read() and either + .fileno() or both .seek() and .tell(). + + If ``boundary`` is set, then it as used as the MIME boundary. Otherwise + a randomly generated boundary will be used. In either case, if the + boundary string appears in the parameter values a ValueError will be + raised. + + If ``cb`` is set, it should be a callback which will get called as blocks + of data are encoded. It will be called with (param, current, total), + indicating the current parameter being encoded, the current amount encoded, + and the total amount to encode. + + Returns a tuple of `datagen`, `headers`, where `datagen` is a + generator that will yield blocks of data that make up the encoded + parameters, and `headers` is a dictionary with the assoicated + Content-Type and Content-Length headers. + + Examples: + + >>> datagen, headers = multipart_encode( [("key", "value1"), ("key", "value2")] ) + >>> s = "".join(datagen) + >>> assert "value2" in s and "value1" in s + + >>> p = MultipartParam("key", "value2") + >>> datagen, headers = multipart_encode( [("key", "value1"), p] ) + >>> s = "".join(datagen) + >>> assert "value2" in s and "value1" in s + + >>> datagen, headers = multipart_encode( {"key": "value1"} ) + >>> s = "".join(datagen) + >>> assert "value2" not in s and "value1" in s + + """ + if boundary is None: + boundary = gen_boundary() + else: + boundary = urllib.quote_plus(boundary) + + headers = get_headers(params, boundary) + params = MultipartParam.from_params(params) + + return multipart_yielder(params, boundary, cb), headers diff --git a/requests/packages/poster/streaminghttp.py b/requests/packages/poster/streaminghttp.py new file mode 100644 index 00000000..379d769d --- /dev/null +++ b/requests/packages/poster/streaminghttp.py @@ -0,0 +1,197 @@ +"""Streaming HTTP uploads module. + +This module extends the standard httplib and urllib2 objects so that +iterable objects can be used in the body of HTTP requests. + +In most cases all one should have to do is call :func:`register_openers()` +to register the new streaming http handlers which will take priority over +the default handlers, and then you can use iterable objects in the body +of HTTP requests. + +**N.B.** You must specify a Content-Length header if using an iterable object +since there is no way to determine in advance the total size that will be +yielded, and there is no way to reset an interator. + +Example usage: + +>>> from StringIO import StringIO +>>> import urllib2, poster.streaminghttp + +>>> opener = poster.streaminghttp.register_openers() + +>>> s = "Test file data" +>>> f = StringIO(s) + +>>> req = urllib2.Request("http://localhost:5000", f, +... {'Content-Length': str(len(s))}) +""" + +import httplib, urllib2, socket +from httplib import NotConnected + +__all__ = ['StreamingHTTPConnection', 'StreamingHTTPRedirectHandler', + 'StreamingHTTPHandler', 'register_openers'] + +if hasattr(httplib, 'HTTPS'): + __all__.extend(['StreamingHTTPSHandler', 'StreamingHTTPSConnection']) + +class _StreamingHTTPMixin: + """Mixin class for HTTP and HTTPS connections that implements a streaming + send method.""" + def send(self, value): + """Send ``value`` to the server. + + ``value`` can be a string object, a file-like object that supports + a .read() method, or an iterable object that supports a .next() + method. + """ + # Based on python 2.6's httplib.HTTPConnection.send() + if self.sock is None: + if self.auto_open: + self.connect() + else: + raise NotConnected() + + # send the data to the server. if we get a broken pipe, then close + # the socket. we want to reconnect when somebody tries to send again. + # + # NOTE: we DO propagate the error, though, because we cannot simply + # ignore the error... the caller will know if they can retry. + if self.debuglevel > 0: + print "send:", repr(value) + try: + blocksize = 8192 + if hasattr(value, 'read') : + if hasattr(value, 'seek'): + value.seek(0) + if self.debuglevel > 0: + print "sendIng a read()able" + data = value.read(blocksize) + while data: + self.sock.sendall(data) + data = value.read(blocksize) + elif hasattr(value, 'next'): + if hasattr(value, 'reset'): + value.reset() + if self.debuglevel > 0: + print "sendIng an iterable" + for data in value: + self.sock.sendall(data) + else: + self.sock.sendall(value) + except socket.error, v: + if v[0] == 32: # Broken pipe + self.close() + raise + +class StreamingHTTPConnection(_StreamingHTTPMixin, httplib.HTTPConnection): + """Subclass of `httplib.HTTPConnection` that overrides the `send()` method + to support iterable body objects""" + +class StreamingHTTPRedirectHandler(urllib2.HTTPRedirectHandler): + """Subclass of `urllib2.HTTPRedirectHandler` that overrides the + `redirect_request` method to properly handle redirected POST requests + + This class is required because python 2.5's HTTPRedirectHandler does + not remove the Content-Type or Content-Length headers when requesting + the new resource, but the body of the original request is not preserved. + """ + + handler_order = urllib2.HTTPRedirectHandler.handler_order - 1 + + # From python2.6 urllib2's HTTPRedirectHandler + def redirect_request(self, req, fp, code, msg, headers, newurl): + """Return a Request or None in response to a redirect. + + This is called by the http_error_30x methods when a + redirection response is received. If a redirection should + take place, return a new Request to allow http_error_30x to + perform the redirect. Otherwise, raise HTTPError if no-one + else should try to handle this url. Return None if you can't + but another Handler might. + """ + m = req.get_method() + if (code in (301, 302, 303, 307) and m in ("GET", "HEAD") + or code in (301, 302, 303) and m == "POST"): + # Strictly (according to RFC 2616), 301 or 302 in response + # to a POST MUST NOT cause a redirection without confirmation + # from the user (of urllib2, in this case). In practice, + # essentially all clients do redirect in this case, so we + # do the same. + # be conciliant with URIs containing a space + newurl = newurl.replace(' ', '%20') + newheaders = dict((k, v) for k, v in req.headers.items() + if k.lower() not in ( + "content-length", "content-type") + ) + return urllib2.Request(newurl, + headers=newheaders, + origin_req_host=req.get_origin_req_host(), + unverifiable=True) + else: + raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) + +class StreamingHTTPHandler(urllib2.HTTPHandler): + """Subclass of `urllib2.HTTPHandler` that uses + StreamingHTTPConnection as its http connection class.""" + + handler_order = urllib2.HTTPHandler.handler_order - 1 + + def http_open(self, req): + """Open a StreamingHTTPConnection for the given request""" + return self.do_open(StreamingHTTPConnection, req) + + def http_request(self, req): + """Handle a HTTP request. Make sure that Content-Length is specified + if we're using an interable value""" + # Make sure that if we're using an iterable object as the request + # body, that we've also specified Content-Length + if req.has_data(): + data = req.get_data() + if hasattr(data, 'read') or hasattr(data, 'next'): + if not req.has_header('Content-length'): + raise ValueError( + "No Content-Length specified for iterable body") + return urllib2.HTTPHandler.do_request_(self, req) + +if hasattr(httplib, 'HTTPS'): + class StreamingHTTPSConnection(_StreamingHTTPMixin, + httplib.HTTPSConnection): + """Subclass of `httplib.HTTSConnection` that overrides the `send()` + method to support iterable body objects""" + + class StreamingHTTPSHandler(urllib2.HTTPSHandler): + """Subclass of `urllib2.HTTPSHandler` that uses + StreamingHTTPSConnection as its http connection class.""" + + handler_order = urllib2.HTTPSHandler.handler_order - 1 + + def https_open(self, req): + return self.do_open(StreamingHTTPSConnection, req) + + def https_request(self, req): + # Make sure that if we're using an iterable object as the request + # body, that we've also specified Content-Length + if req.has_data(): + data = req.get_data() + if hasattr(data, 'read') or hasattr(data, 'next'): + if not req.has_header('Content-length'): + raise ValueError( + "No Content-Length specified for iterable body") + return urllib2.HTTPSHandler.do_request_(self, req) + + +def register_openers(): + """Register the streaming http handlers in the global urllib2 default + opener object. + + Returns the created OpenerDirector object.""" + handlers = [StreamingHTTPHandler, StreamingHTTPRedirectHandler] + if hasattr(httplib, "HTTPS"): + handlers.append(StreamingHTTPSHandler) + + opener = urllib2.build_opener(*handlers) + + urllib2.install_opener(opener) + + return opener From e6d5e5319ecba82021143624c1040d0ad01bce41 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:01:09 -0500 Subject: [PATCH 05/19] added multipart_file syntax --- requests/core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/requests/core.py b/requests/core.py index e22d73cf..9f94bbba 100644 --- a/requests/core.py +++ b/requests/core.py @@ -283,12 +283,13 @@ def head(url, params={}, headers={}, auth=None): return r.response -def post(url, data={}, headers={}, auth=None): +def post(url, data={}, headers={}, multipart_files={}, auth=None): """Sends a POST request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. :param data: (optional) Dictionary of POST Data to send with the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. + :param multipart_files: (optional) Dictoinary of 'filename': file-like-objects for multipart encoding upload. :param auth: (optional) AuthObject to enable Basic HTTP Auth. """ @@ -306,12 +307,13 @@ def post(url, data={}, headers={}, auth=None): return r.response -def put(url, data='', headers={}, auth=None): +def put(url, data='', headers={}, multipart_files={}, auth=None): """Sends a PUT request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. :param data: (optional) Bytes of PUT Data to send with the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. + :param multipart_files: (optional) Dictoinary of 'filename': file-like-objects for multipart encoding upload. :param auth: (optional) AuthObject to enable Basic HTTP Auth. """ From 221bed27dda5e3c2c051de72b5553c73d6183985 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:04:35 -0500 Subject: [PATCH 06/19] import poster from site-packages for now --- requests/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requests/core.py b/requests/core.py index 9f94bbba..8c77b7cf 100644 --- a/requests/core.py +++ b/requests/core.py @@ -13,6 +13,9 @@ import urllib import urllib2 +from poster.encode import multipart_encode +from poster.streaminghttp import register_openers + __title__ = 'requests' __version__ = '0.2.0' From 127bbbb18dccc0bb0522377c7b81b99b63b84a68 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:04:41 -0500 Subject: [PATCH 07/19] Worth a shot --- requests/packages/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/requests/packages/__init__.py b/requests/packages/__init__.py index e69de29b..00ce6e24 100644 --- a/requests/packages/__init__.py +++ b/requests/packages/__init__.py @@ -0,0 +1 @@ +import poster \ No newline at end of file From 7dd37d5ea91cd26c869a39b5a200136305c6f3bb Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:27:47 -0500 Subject: [PATCH 08/19] proper poster imports --- requests/__init__.py | 1 + requests/packages/poster/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/requests/__init__.py b/requests/__init__.py index ef1b3728..0378ff46 100644 --- a/requests/__init__.py +++ b/requests/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- +import packages from .core import * \ No newline at end of file diff --git a/requests/packages/poster/__init__.py b/requests/packages/poster/__init__.py index 857eb015..8d7e879c 100644 --- a/requests/packages/poster/__init__.py +++ b/requests/packages/poster/__init__.py @@ -26,7 +26,7 @@ New releases of poster will always have a version number that compares greater than an older version of poster. New in version 0.6.""" -import poster.streaminghttp -import poster.encode +import streaminghttp +import encode version = (0, 8, 0) # Thanks JP! From f7abfa8a1426b018b99c9ac3237331b55bf7103c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:27:57 -0500 Subject: [PATCH 09/19] multipart_files support! --- requests/core.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/requests/core.py b/requests/core.py index 8c77b7cf..600472bf 100644 --- a/requests/core.py +++ b/requests/core.py @@ -13,8 +13,8 @@ import urllib import urllib2 -from poster.encode import multipart_encode -from poster.streaminghttp import register_openers +from .packages.poster.encode import multipart_encode +from .packages.poster.streaminghttp import register_openers __title__ = 'requests' @@ -54,6 +54,7 @@ class Request(object): def __init__(self): self.url = None self.headers = dict() + self.multipart_files = None self.method = None self.params = {} self.data = {} @@ -175,16 +176,25 @@ class Request(object): elif self.method == 'POST': if (not self.sent) or anyway: - req = _Request(self.url, method='POST') + if self.multipart_files: + register_openers() + datagen, headers = multipart_encode(self.multipart_files) + req = _Request(self.url, data=datagen, headers=headers, method='POST') - if self.headers: + if self.headers: + req.headers.update(self.headers) + + else: + req = _Request(self.url, method='POST') req.headers = self.headers + if isinstance(self.data, dict): + req.data = urllib.urlencode(self.data) + else: + req.data = self.data + # url encode form data if it's a dict - if isinstance(self.data, dict): - req.data = urllib.urlencode(self.data) - else: - req.data = self.data + try: @@ -286,7 +296,7 @@ def head(url, params={}, headers={}, auth=None): return r.response -def post(url, data={}, headers={}, multipart_files={}, auth=None): +def post(url, data={}, headers={}, multipart_files=None, auth=None): """Sends a POST request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -297,11 +307,14 @@ def post(url, data={}, headers={}, multipart_files={}, auth=None): """ r = Request() - + r.url = url r.method = 'POST' r.data = data + if multipart_files: + r.multipart_files = multipart_files + r.headers = headers r.auth = _detect_auth(url, auth) From a16278e85dc1dda84e55f2f63e4d56cb60329601 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:30:37 -0500 Subject: [PATCH 10/19] string formatting supports None. Who knew. --- requests/core.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/requests/core.py b/requests/core.py index 600472bf..43d09cfc 100644 --- a/requests/core.py +++ b/requests/core.py @@ -64,11 +64,7 @@ class Request(object): def __repr__(self): - try: - repr = '' % (self.method) - except: - repr = '' - return repr + return '' % (self.method) def __setattr__(self, name, value): @@ -207,7 +203,7 @@ class Request(object): success = True - except urllib2.HTTPError, why: + except(urllib2.HTTPError, why): self.response.status_code = why.code @@ -228,11 +224,8 @@ class Response(object): self.headers = dict() def __repr__(self): - try: - repr = '' % (self.status_code) - except: - repr = '' - return repr + return '' % (self.status_code) + class AuthObject(object): From 4c8b428bbd3800cafa0e3764512eb719f67315f0 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:34:36 -0500 Subject: [PATCH 11/19] except clause fix for python3 --- requests/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requests/core.py b/requests/core.py index 43d09cfc..e63dc224 100644 --- a/requests/core.py +++ b/requests/core.py @@ -141,7 +141,7 @@ class Request(object): self.response.content = resp.read() success = True - except urllib2.HTTPError, why: + except urllib2.HTTPError as why: self.response.status_code = why.code @@ -165,7 +165,7 @@ class Request(object): success = True - except urllib2.HTTPError, why: + except urllib2.HTTPError as why: self.response.status_code = why.code @@ -203,7 +203,7 @@ class Request(object): success = True - except(urllib2.HTTPError, why): + except urllib2.HTTPError as why: self.response.status_code = why.code @@ -225,7 +225,7 @@ class Response(object): def __repr__(self): return '' % (self.status_code) - + class AuthObject(object): From 4c192ec5acd4417fe60b5a63ea3c6a5c034c1d45 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:36:14 -0500 Subject: [PATCH 12/19] absolute imports --- requests/__init__.py | 4 ++-- requests/packages/__init__.py | 2 +- requests/packages/poster/__init__.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requests/__init__.py b/requests/__init__.py index 0378ff46..b6e3841f 100644 --- a/requests/__init__.py +++ b/requests/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -import packages -from .core import * \ No newline at end of file +from . import packages +from .core import * diff --git a/requests/packages/__init__.py b/requests/packages/__init__.py index 00ce6e24..5d44dd4d 100644 --- a/requests/packages/__init__.py +++ b/requests/packages/__init__.py @@ -1 +1 @@ -import poster \ No newline at end of file +from . import poster diff --git a/requests/packages/poster/__init__.py b/requests/packages/poster/__init__.py index 8d7e879c..8f26770b 100644 --- a/requests/packages/poster/__init__.py +++ b/requests/packages/poster/__init__.py @@ -26,7 +26,7 @@ New releases of poster will always have a version number that compares greater than an older version of poster. New in version 0.6.""" -import streaminghttp -import encode +from . import streaminghttp +from . import encode version = (0, 8, 0) # Thanks JP! From 3216bda17432dcb0e7480ba0068b927f7ff746b5 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:45:12 -0500 Subject: [PATCH 13/19] Added multipart_files support for PUT --- requests/core.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/requests/core.py b/requests/core.py index e63dc224..5d9e5b74 100644 --- a/requests/core.py +++ b/requests/core.py @@ -148,12 +148,22 @@ class Request(object): elif self.method == 'PUT': if (not self.sent) or anyway: - req = _Request(self.url, method='PUT') + if self.multipart_files: + register_openers() + datagen, headers = multipart_encode(self.multipart_files) + req = _Request(self.url, data=datagen, headers=headers, method='PUT') - if self.headers: - req.headers = self.headers + if self.headers: + req.headers.update(self.headers) - req.data = self.data + else: + + req = _Request(self.url, method='PUT') + + if self.headers: + req.headers = self.headers + + req.data = self.data try: opener = self._get_opener() @@ -184,13 +194,11 @@ class Request(object): req = _Request(self.url, method='POST') req.headers = self.headers + # url encode form data if it's a dict if isinstance(self.data, dict): req.data = urllib.urlencode(self.data) else: req.data = self.data - - # url encode form data if it's a dict - try: From e89eba79dfb51e7d88de1fe85ba2b61ed92a8dac Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:53:56 -0500 Subject: [PATCH 14/19] added response.url support for 301's and the like. --- README.rst | 3 +++ requests/core.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9b3cfb68..1c0289a1 100644 --- a/README.rst +++ b/README.rst @@ -88,6 +88,9 @@ All request functions return a Response object (see below). Request.content: (Bytes) Received Content + Request.url + (String) URL of response. Useful for detecting redirects. + **HTTP Authentication Registry:** diff --git a/requests/core.py b/requests/core.py index 5d9e5b74..71f69638 100644 --- a/requests/core.py +++ b/requests/core.py @@ -137,8 +137,9 @@ class Request(object): resp = opener(req) self.response.status_code = resp.code self.response.headers = resp.info().dict - if self.method.lower() == 'get': + if self.method == 'GET': self.response.content = resp.read() + self.response.url = resp.url success = True except urllib2.HTTPError as why: @@ -172,6 +173,7 @@ class Request(object): self.response.status_code = resp.code self.response.headers = resp.info().dict self.response.content = resp.read() + self.response.url = resp.url success = True @@ -208,6 +210,7 @@ class Request(object): self.response.status_code = resp.code self.response.headers = resp.info().dict self.response.content = resp.read() + self.response.url = resp.url success = True @@ -230,6 +233,7 @@ class Response(object): self.content = None self.status_code = None self.headers = dict() + self.url = None def __repr__(self): return '' % (self.status_code) From 324c572b6496f2f39cf0f266012df1f9f4930568 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 09:59:42 -0500 Subject: [PATCH 15/19] renamed multipart_files to files --- requests/core.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/requests/core.py b/requests/core.py index 71f69638..f299f610 100644 --- a/requests/core.py +++ b/requests/core.py @@ -54,7 +54,7 @@ class Request(object): def __init__(self): self.url = None self.headers = dict() - self.multipart_files = None + self.files = None self.method = None self.params = {} self.data = {} @@ -149,9 +149,9 @@ class Request(object): elif self.method == 'PUT': if (not self.sent) or anyway: - if self.multipart_files: + if self.files: register_openers() - datagen, headers = multipart_encode(self.multipart_files) + datagen, headers = multipart_encode(self.files) req = _Request(self.url, data=datagen, headers=headers, method='PUT') if self.headers: @@ -184,9 +184,9 @@ class Request(object): elif self.method == 'POST': if (not self.sent) or anyway: - if self.multipart_files: + if self.files: register_openers() - datagen, headers = multipart_encode(self.multipart_files) + datagen, headers = multipart_encode(self.files) req = _Request(self.url, data=datagen, headers=headers, method='POST') if self.headers: @@ -301,13 +301,13 @@ def head(url, params={}, headers={}, auth=None): return r.response -def post(url, data={}, headers={}, multipart_files=None, auth=None): +def post(url, data={}, headers={}, files=None, auth=None): """Sends a POST request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. :param data: (optional) Dictionary of POST Data to send with the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. - :param multipart_files: (optional) Dictoinary of 'filename': file-like-objects for multipart encoding upload. + :param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. :param auth: (optional) AuthObject to enable Basic HTTP Auth. """ @@ -317,8 +317,8 @@ def post(url, data={}, headers={}, multipart_files=None, auth=None): r.method = 'POST' r.data = data - if multipart_files: - r.multipart_files = multipart_files + if files: + r.files = files r.headers = headers r.auth = _detect_auth(url, auth) @@ -328,13 +328,13 @@ def post(url, data={}, headers={}, multipart_files=None, auth=None): return r.response -def put(url, data='', headers={}, multipart_files={}, auth=None): +def put(url, data='', headers={}, files={}, auth=None): """Sends a PUT request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. :param data: (optional) Bytes of PUT Data to send with the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. - :param multipart_files: (optional) Dictoinary of 'filename': file-like-objects for multipart encoding upload. + :param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. :param auth: (optional) AuthObject to enable Basic HTTP Auth. """ @@ -343,7 +343,7 @@ def put(url, data='', headers={}, multipart_files={}, auth=None): r.url = url r.method = 'PUT' r.data = data - + r.files = files r.headers = headers r.auth = _detect_auth(url, auth) From 02834e353d7a9d9a6dff5a2c2ee5ba477f83d46c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 10:00:05 -0500 Subject: [PATCH 16/19] added simple test fixture for Postbin and posting both files and data --- test_requests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test_requests.py b/test_requests.py index 0bd6c965..35bb9f65 100644 --- a/test_requests.py +++ b/test_requests.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + import unittest +from cStringIO import StringIO import requests @@ -42,6 +44,18 @@ class RequestsTestSuite(unittest.TestCase): self.assertEqual(r.status_code, 200) + def test_POSTBIN_GET_POST_FILES(self): + + bin = requests.post('http://www.postbin.org/') + self.assertEqual(bin.status_code, 200) + + post = requests.post(bin.url, data={'some': 'data'}) + self.assertEqual(post.status_code, 201) + + post2 = requests.post(bin.url, files={'some': StringIO('data')}) + self.assertEqual(post2.status_code, 201) + + if __name__ == '__main__': unittest.main() From 9f9ebecec27108db292a31397e44d447f4ebfeb9 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 10:35:15 -0500 Subject: [PATCH 17/19] v0.2.1 --- HISTORY.rst | 7 +++++++ requests/core.py | 4 ++-- setup.py | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7da0986a..41118bbe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,13 @@ History ------- +0.2.1 (2011-02-14) +++++++++++++++++++ + +* Added file attribute to POST and PUT requests for multipart-encode file uploads. +* Added Request.url attribute for context and redirects + + 0.2.0 (2011-02-14) ++++++++++++++++++ diff --git a/requests/core.py b/requests/core.py index f299f610..c99de577 100644 --- a/requests/core.py +++ b/requests/core.py @@ -18,8 +18,8 @@ from .packages.poster.streaminghttp import register_openers __title__ = 'requests' -__version__ = '0.2.0' -__build__ = 0x000200 +__version__ = '0.2.1' +__build__ = 0x000201 __author__ = 'Kenneth Reitz' __license__ = 'ISC' __copyright__ = 'Copyright 2011 Kenneth Reitz' diff --git a/setup.py b/setup.py index 66845871..b7e1d9c7 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ required = [] setup( name='requests', - version='0.2.0', + version='0.2.1', description='Python HTTP Library that\'s actually usable.', long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), @@ -30,6 +30,8 @@ setup( url='https://github.com/kennethreitz/requests', packages= [ 'requests', + 'requests.packages', + 'requests.packages.poster' ], install_requires=required, license='ISC', From e1659ed7afb674006f6ce07f9908a7bccb7a8474 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 10:37:38 -0500 Subject: [PATCH 18/19] added setup.py test --- setup.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b7e1d9c7..725ece6a 100644 --- a/setup.py +++ b/setup.py @@ -7,14 +7,15 @@ import sys from distutils.core import setup -def publish(): - """Publish to PyPi""" - os.system("python setup.py sdist upload") - + if sys.argv[-1] == "publish": - publish() + os.system("python setup.py sdist upload") sys.exit() +if sys.argv[-1] == "test": + os.system("python test_requests.py") + sys.exit() + required = [] # if python > 2.6, require simplejson @@ -22,7 +23,7 @@ required = [] setup( name='requests', version='0.2.1', - description='Python HTTP Library that\'s actually usable.', + description='Awesome Python HTTP Library that\'s actually usable.', long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), author='Kenneth Reitz', From 43790c4d3212afec36e6a5dc303546dc6705db7b Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 14 Feb 2011 10:43:54 -0500 Subject: [PATCH 19/19] updated multipart-upload documentation --- README.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1c0289a1..09d551ad 100644 --- a/README.rst +++ b/README.rst @@ -13,6 +13,7 @@ Features - Extremely simple GET, HEAD, POST, PUT, DELETE Requests + Simple HTTP Header Request Attachment + Simple Data/Params Request Attachment + + Simple Multipart File Uploads - Simple Basic HTTP Authentication + Simple URL + HTTP Auth Registry @@ -65,12 +66,14 @@ All request functions return a Response object (see below). PUT Requests - >>> request.put(url, data='', headers={}, auth=None) + >>> request.put(url, data='', headers={}, files={}, auth=None) + # If files dictionary ( {filename: fileobject, ...} ) is given, a multi-part upload is performed. POST Requests - >>> request.post(url, data={}, headers={}, auth=None) + >>> request.post(url, data={}, headers={}, files={}, auth=None) + # If files dictionary ( {filename: fileobject, ...} ) is given, a multi-part upload is performed. DELETE Requests >>> request.delete(url, params={}, headers={}, auth=None)