From 23ee58d6ec47a06358008cb7a4ab4f9556edc285 Mon Sep 17 00:00:00 2001 From: Johannes Gorset Date: Thu, 12 May 2011 10:16:21 +0200 Subject: [PATCH 01/10] Encode both keys and values for incoming unicode data as UTF-8, coincidentally fixing a bug that caused UTF-8 encoded byte strings to be encoded twice and causing an UnicodeDecodeError. --- requests/core.py | 11 +++++++---- test_requests.py | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/requests/core.py b/requests/core.py index b2bad895..f12797ee 100644 --- a/requests/core.py +++ b/requests/core.py @@ -70,13 +70,16 @@ class Request(object): self.headers = headers self.files = files self.method = method - self.data = data + + self.data = {} + for (k, v) in data.items(): + self.data.update({ + k.encode('utf-8') if k.__class__ is unicode else k: \ + v.encode('utf-8') if v.__class__ is unicode else v + }) socket.setdefaulttimeout(timeout) - for (k, v) in self.data.iteritems(): - self.data[k] = v.encode('utf-8') - # url encode data if it's a dict if hasattr(data, 'items'): self._enc_data = urllib.urlencode(self.data) diff --git a/test_requests.py b/test_requests.py index c73803c0..89b69acd 100755 --- a/test_requests.py +++ b/test_requests.py @@ -141,6 +141,8 @@ class RequestsTestSuite(unittest.TestCase): def test_unicode_get(self): requests.get('http://google.com', params={'foo': u'føø'}) + requests.get('http://google.com', params={u'føø': u'føø'}) + requests.get('http://google.com', params={'føø': 'føø'}) requests.get('http://google.com', params={'foo': u'foo'}) requests.get('http://google.com/ø', params={'foo': u'foo'}) From 912f48eececb6355b3511416d4d5361aedbd5d08 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Thu, 12 May 2011 10:12:04 -0400 Subject: [PATCH 02/10] slight refactor --- requests/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requests/core.py b/requests/core.py index e7e013e6..40f4c8e0 100644 --- a/requests/core.py +++ b/requests/core.py @@ -69,12 +69,12 @@ class Request(object): self.headers = headers self.files = files self.method = method - + self.data = {} for (k, v) in data.items(): self.data.update({ - k.encode('utf-8') if k.__class__ is unicode else k: \ - v.encode('utf-8') if v.__class__ is unicode else v + k.encode('utf-8') if isinstance(k, unicode) else k: + v.encode('utf-8') if isinstance(v, unicode) else v }) socket.setdefaulttimeout(timeout) From 1971bdbe6726af224d92723b3f5f12f26aa4277f Mon Sep 17 00:00:00 2001 From: megane murayama Date: Thu, 12 May 2011 12:50:09 -0400 Subject: [PATCH 03/10] fix for post non-dict data --- requests/core.py | 13 ++++++------- test_requests.py | 3 +++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/requests/core.py b/requests/core.py index 40f4c8e0..69d0f3f7 100644 --- a/requests/core.py +++ b/requests/core.py @@ -69,18 +69,17 @@ class Request(object): self.headers = headers self.files = files self.method = method - self.data = {} - for (k, v) in data.items(): - self.data.update({ - k.encode('utf-8') if isinstance(k, unicode) else k: - v.encode('utf-8') if isinstance(v, unicode) else v - }) socket.setdefaulttimeout(timeout) - # url encode data if it's a dict if hasattr(data, 'items'): + for (k, v) in data.items(): + self.data.update({ + k.encode('utf-8') if isinstance(k, unicode) else k: + v.encode('utf-8') if isinstance(v, unicode) else v + }) + # url encode data if it's a dict self._enc_data = urllib.urlencode(self.data) else: self._enc_data = self.data diff --git a/test_requests.py b/test_requests.py index 89b69acd..ecd01772 100755 --- a/test_requests.py +++ b/test_requests.py @@ -79,6 +79,9 @@ class RequestsTestSuite(unittest.TestCase): post2 = requests.post(bin.url, files={'some': open('test_requests.py')}) self.assertEqual(post2.status_code, 201) + post3 = requests.post(bin.url, data='[{"some": "json"}]') + self.assertEqual(post.status_code, 201) + def test_POSTBIN_GET_POST_FILES_WITH_PARAMS(self): bin = requests.post('http://www.postbin.org/') From b1e28c7fd8a9fe15d16a9636950559869b46ea96 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 13:04:49 -0400 Subject: [PATCH 04/10] Added Megan Emurayama to authors --- AUTHORS | 1 + requests/core.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index e09b3668..d136e02c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,3 +18,4 @@ Patches and Suggestions - Rob Madole - Aram Dulyan - Johannes Gorset +- 村山めがね (Megan Emurayama) \ No newline at end of file diff --git a/requests/core.py b/requests/core.py index 69d0f3f7..a90cdd7d 100644 --- a/requests/core.py +++ b/requests/core.py @@ -65,24 +65,28 @@ class Request(object): def __init__(self, url=None, headers=dict(), files=None, method=None, data=dict(), auth=None, cookiejar=None, timeout=None): + socket.setdefaulttimeout(timeout) + self.url = url self.headers = headers self.files = files self.method = method self.data = {} - socket.setdefaulttimeout(timeout) - + # self.data = {} if hasattr(data, 'items'): for (k, v) in data.items(): self.data.update({ k.encode('utf-8') if isinstance(k, unicode) else k: v.encode('utf-8') if isinstance(v, unicode) else v }) - # url encode data if it's a dict + + # url encode data if it's a dict + if hasattr(data, 'items'): self._enc_data = urllib.urlencode(self.data) else: - self._enc_data = self.data + self._enc_data = data + self.response = Response() @@ -210,8 +214,6 @@ class Request(object): if not self.sent or anyway: - - try: opener = self._get_opener() resp = opener(req) From 62931b60f8c6da1a1f1a3bd472c8d6ce0570c27c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 13:11:08 -0400 Subject: [PATCH 05/10] history for v0.3.4 --- HISTORY.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 0eaf42ab..c579f8a1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ History ------- +0.3.4 ++++++ + +* Bytes data upload Bugfix + + 0.3.3 (2011-05-12) ++++++++++++++++++ From 1421ffa32ab4709c9a7634493803bd55a3c6615f Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 14:02:36 -0400 Subject: [PATCH 06/10] HTTP Basic recursion. Fixes #31 --- requests/core.py | 35 ++++++++++++++++++++++++++++++++--- test_requests.py | 7 +++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/requests/core.py b/requests/core.py index a90cdd7d..7a92c5fa 100644 --- a/requests/core.py +++ b/requests/core.py @@ -55,6 +55,28 @@ class _Request(urllib2.Request): return urllib2.Request.get_method(self) +class _HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler): + # from mercurial + + def __init__(self, *args, **kwargs): + urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs) + self.retried_req = None + + def reset_retry_count(self): + # Python 2.6.5 will call this on 401 or 407 errors and thus loop + # forever. We disable reset_retry_count completely and reset in + # http_error_auth_reqed instead. + pass + + def http_error_auth_reqed(self, auth_header, host, req, headers): + # Reset the retry counter once for each request. + if req is not self.retried_req: + self.retried_req = req + self.retried = 0 + return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed( + self, auth_header, host, req, headers) + + class Request(object): """The :class:`Request` object. It carries out all functionality of Requests. Recommended interface is with the Requests functions. @@ -153,10 +175,17 @@ class Request(object): def _build_response(self, resp): """Build internal Response object from given response.""" + if isinstance(resp, HTTPError): + # print resp.__dict__ + pass self.response.status_code = getattr(resp, 'code', None) - self.response.headers = getattr(resp.info(), 'dict', None) - self.response.content = resp.read() + + try: + self.response.headers = getattr(resp.info(), 'dict', None) + self.response.content = resp.read() + except AttributeError, why: + pass if self.response.headers.get('content-encoding', None) == 'gzip': try: @@ -431,7 +460,7 @@ class AuthObject(object): """ _handlers = { - 'basic': urllib2.HTTPBasicAuthHandler, + 'basic': _HTTPBasicAuthHandler, 'digest': urllib2.HTTPDigestAuthHandler, 'proxy_basic': urllib2.ProxyBasicAuthHandler, 'proxy_digest': urllib2.ProxyDigestAuthHandler diff --git a/test_requests.py b/test_requests.py index ecd01772..3bc2e092 100755 --- a/test_requests.py +++ b/test_requests.py @@ -149,6 +149,13 @@ class RequestsTestSuite(unittest.TestCase): requests.get('http://google.com', params={'foo': u'foo'}) requests.get('http://google.com/ø', params={'foo': u'foo'}) + def test_httpauth_recursion(self): + conv_auth = ('requeststest', 'bad_password') + + r = requests.get('https://convore.com/api/account/verify.json', auth=conv_auth) + self.assertEquals(r.status_code, 401) + print r.__dict__ + if __name__ == '__main__': unittest.main() From 14ef4622634ae8746bce600523cbed15a119d15a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 14:21:42 -0400 Subject: [PATCH 07/10] package refactor --- requests/api.py | 115 +++++++++ requests/core.py | 583 +------------------------------------------- requests/models.py | 435 +++++++++++++++++++++++++++++++++ requests/monkeys.py | 85 +++++++ requests/patches.py | 5 + test_requests.py | 2 +- 6 files changed, 649 insertions(+), 576 deletions(-) create mode 100644 requests/api.py create mode 100644 requests/models.py create mode 100644 requests/monkeys.py create mode 100644 requests/patches.py diff --git a/requests/api.py b/requests/api.py new file mode 100644 index 00000000..26335e5d --- /dev/null +++ b/requests/api.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + +""" +requests.api +~~~~~~~~~~~~ + +This module impliments the Requests API. + +:copyright: (c) 2011 by Kenneth Reitz. +:license: ISC, see LICENSE for more details. + +""" + +import requests +from .models import Request, Response, AuthManager, AuthObject, auth_manager + + +__all__ = ('request', 'get', 'head', 'post', 'put', 'delete') + + + +def request(method, url, **kwargs): + """Sends a `method` request. Returns :class:`Response` object. + + :param method: method for the new :class:`Request` object. + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary of GET/HEAD/DELETE Parameters to send with the :class:`Request`. + :param data: (optional) Bytes/Dictionary of PUT/POST Data to send with the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) CookieJar object to send with the :class:`Request`. + :param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. + :param auth: (optional) AuthObject to enable Basic HTTP Auth. + :param timeout: (optional) Float describing the timeout of the request. + """ + data = kwargs.pop('data', dict()) or kwargs.pop('params', dict()) + + r = Request(method=method, url=url, data=data, headers=kwargs.pop('headers', {}), + cookiejar=kwargs.pop('cookies', None), files=kwargs.pop('files', None), + auth=kwargs.pop('auth', auth_manager.get_auth(url)), + timeout=kwargs.pop('timeout', requests.timeout)) + r.send() + + return r.response + + +def get(url, params={}, headers={}, cookies=None, auth=None, **kwargs): + """Sends a GET request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary of GET Parameters to send with the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) CookieJar object to send with the :class:`Request`. + :param auth: (optional) AuthObject to enable Basic HTTP Auth. + :param timeout: (optional) Float describing the timeout of the request. + """ + + return request('GET', url, params=params, headers=headers, cookies=cookies, auth=auth, **kwargs) + + +def head(url, params={}, headers={}, cookies=None, auth=None, **kwargs): + """Sends a HEAD request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary of GET Parameters to send with the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. + :param cookies: (optional) CookieJar object to send with the :class:`Request`. + :param auth: (optional) AuthObject to enable Basic HTTP Auth. + :param timeout: (optional) Float describing the timeout of the request. + """ + + return request('HEAD', url, params=params, headers=headers, cookies=cookies, auth=auth, **kwargs) + + +def post(url, data={}, headers={}, files=None, cookies=None, auth=None, **kwargs): + """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 files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. + :param cookies: (optional) CookieJar object to send with the :class:`Request`. + :param auth: (optional) AuthObject to enable Basic HTTP Auth. + :param timeout: (optional) Float describing the timeout of the request. + """ + + return request('POST', url, data=data, headers=headers, files=files, cookies=cookies, auth=auth, **kwargs) + + +def put(url, data='', headers={}, files={}, cookies=None, auth=None, **kwargs): + """Sends a PUT request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param params: (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 files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. + :param cookies: (optional) CookieJar object to send with the :class:`Request`. + :param auth: (optional) AuthObject to enable Basic HTTP Auth. + :param timeout: (optional) Float describing the timeout of the request. + """ + + return request('PUT', url, data=data, headers=headers, files=files, cookies=cookies, auth=auth, **kwargs) + + +def delete(url, params={}, headers={}, cookies=None, auth=None, **kwargs): + """Sends a DELETE request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary of DELETE Parameters to send with the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. + :param cookies: (optional) CookieJar object to send with the :class:`Request`. + :param auth: (optional) AuthObject to enable Basic HTTP Auth. + :param timeout: (optional) Float describing the timeout of the request. + """ + + return request('DELETE', url, params=params, headers=headers, cookies=cookies, auth=auth, **kwargs) diff --git a/requests/core.py b/requests/core.py index 7a92c5fa..a07fca2c 100644 --- a/requests/core.py +++ b/requests/core.py @@ -1,30 +1,16 @@ # -*- coding: utf-8 -*- """ - requests.core - ~~~~~~~~~~~~~ +requests.core +~~~~~~~~~~~~~ - This module implements the main Requests system. +This module implements the main Requests system. + +:copyright: (c) 2011 by Kenneth Reitz. +:license: ISC, see LICENSE for more details. - :copyright: (c) 2011 by Kenneth Reitz. - :license: ISC, see LICENSE for more details. """ -from __future__ import absolute_import - -import requests -import urllib -import urllib2 -import socket -import zlib - -from urllib2 import HTTPError -from urlparse import urlparse - -from .packages.poster.encode import multipart_encode -from .packages.poster.streaminghttp import register_openers, get_handlers - - __title__ = 'requests' __version__ = '0.3.3' __build__ = 0x000303 @@ -32,559 +18,6 @@ __author__ = 'Kenneth Reitz' __license__ = 'ISC' __copyright__ = 'Copyright 2011 Kenneth Reitz' -__all__ = [ - 'Request', 'Response', 'request', 'get', 'head', 'post', 'put', 'delete', - 'auth_manager', 'AuthObject','RequestException', 'AuthenticationError', - 'URLRequired', 'InvalidMethod', 'HTTPError' -] - -class _Request(urllib2.Request): - """Hidden wrapper around the urllib2.Request object. Allows for manual - setting of HTTP methods. - """ - - def __init__(self, url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None): - urllib2.Request.__init__(self, url, data, headers, origin_req_host, unverifiable) - self.method = method - - def get_method(self): - if self.method: - return self.method - - return urllib2.Request.get_method(self) - - -class _HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler): - # from mercurial - - def __init__(self, *args, **kwargs): - urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs) - self.retried_req = None - - def reset_retry_count(self): - # Python 2.6.5 will call this on 401 or 407 errors and thus loop - # forever. We disable reset_retry_count completely and reset in - # http_error_auth_reqed instead. - pass - - def http_error_auth_reqed(self, auth_header, host, req, headers): - # Reset the retry counter once for each request. - if req is not self.retried_req: - self.retried_req = req - self.retried = 0 - return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed( - self, auth_header, host, req, headers) - - -class Request(object): - """The :class:`Request` object. It carries out all functionality of - Requests. Recommended interface is with the Requests functions. - """ - - _METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE') - - def __init__(self, url=None, headers=dict(), files=None, method=None, - data=dict(), auth=None, cookiejar=None, timeout=None): - - socket.setdefaulttimeout(timeout) - - self.url = url - self.headers = headers - self.files = files - self.method = method - self.data = {} - - # self.data = {} - if hasattr(data, 'items'): - for (k, v) in data.items(): - self.data.update({ - k.encode('utf-8') if isinstance(k, unicode) else k: - v.encode('utf-8') if isinstance(v, unicode) else v - }) - - # url encode data if it's a dict - if hasattr(data, 'items'): - self._enc_data = urllib.urlencode(self.data) - else: - self._enc_data = data - - - self.response = Response() - - if isinstance(auth, (list, tuple)): - auth = AuthObject(*auth) - if not auth: - auth = auth_manager.get_auth(self.url) - self.auth = auth - self.cookiejar = cookiejar - self.sent = False - - - def __repr__(self): - return '' % (self.method) - - - def __setattr__(self, name, value): - if (name == 'method') and (value): - if not value in self._METHODS: - raise InvalidMethod() - - object.__setattr__(self, name, value) - - - def _checks(self): - """Deterministic checks for consistency.""" - - if not self.url: - raise URLRequired - - - def _get_opener(self): - """Creates appropriate opener object for urllib2.""" - - _handlers = [] - - if self.cookiejar is not None: - _handlers.append(urllib2.HTTPCookieProcessor(self.cookiejar)) - - if self.auth: - if not isinstance(self.auth.handler, (urllib2.AbstractBasicAuthHandler, urllib2.AbstractDigestAuthHandler)): - auth_manager.add_password(self.auth.realm, self.url, self.auth.username, self.auth.password) - self.auth.handler = self.auth.handler(auth_manager) - auth_manager.add_auth(self.url, self.auth) - - _handlers.append(self.auth.handler) - - if not _handlers: - return urllib2.urlopen - - _handlers.extend(get_handlers()) - opener = urllib2.build_opener(*_handlers) - - if self.headers: - # Allow default headers in the opener to be overloaded - normal_keys = [k.capitalize() for k in self.headers] - for key, val in opener.addheaders[:]: - if key not in normal_keys: - continue - # Remove it, we have a value to take its place - opener.addheaders.remove((key, val)) - - return opener.open - - def _build_response(self, resp): - """Build internal Response object from given response.""" - if isinstance(resp, HTTPError): - # print resp.__dict__ - pass - - self.response.status_code = getattr(resp, 'code', None) - - try: - self.response.headers = getattr(resp.info(), 'dict', None) - self.response.content = resp.read() - except AttributeError, why: - pass - - if self.response.headers.get('content-encoding', None) == 'gzip': - try: - self.response.content = zlib.decompress(self.response.content, 16+zlib.MAX_WBITS) - except zlib.error: - pass - - self.response.url = getattr(resp, 'url', None) - - - @staticmethod - def _build_url(url, data=None): - """Build URLs.""" - - if urlparse(url).query: - return '%s&%s' % (url, data) - else: - if data: - return '%s?%s' % (url, data) - else: - return url - - - def send(self, anyway=False): - """Sends the request. Returns True of successful, false if not. - If there was an HTTPError during transmission, - self.response.status_code will contain the HTTPError code. - - Once a request is successfully sent, `sent` will equal True. - - :param anyway: If True, request will be sent, even if it has - already been sent. - """ - self._checks() - success = False - - if self.method in ('GET', 'HEAD', 'DELETE'): - req = _Request(self._build_url(self.url, self._enc_data), method=self.method) - else: - - if self.files: - register_openers() - - if self.data: - self.files.update(self.data) - - datagen, headers = multipart_encode(self.files) - req = _Request(self.url, data=datagen, headers=headers, method=self.method) - - else: - req = _Request(self.url, data=self._enc_data, method=self.method) - - if self.headers: - req.headers.update(self.headers) - - if not self.sent or anyway: - - try: - opener = self._get_opener() - resp = opener(req) - - if self.cookiejar is not None: - self.cookiejar.extract_cookies(resp, req) - - except urllib2.HTTPError, why: - self._build_response(why) - self.response.error = why - else: - self._build_response(resp) - self.response.ok = True - - self.response.cached = False - else: - self.response.cached = True - - self.sent = self.response.ok - - return self.sent - - - def read(self, *args): - return self.response.read() - - - -class Response(object): - """The :class:`Request` object. All :class:`Request` objects contain a - :class:`Request.response ` attribute, which is an instance of - this class. - """ - - def __init__(self): - self.content = None - self.status_code = None - self.headers = dict() - self.url = None - self.ok = False - self.error = None - self.cached = False - - - def __repr__(self): - return '' % (self.status_code) - - - def __nonzero__(self): - """Returns true if status_code is 'OK'.""" - return not self.error - - - def raise_for_status(self): - """Raises stored HTTPError if one exists.""" - if self.error: - raise self.error - - def read(self, *args): - return self.content - - - -class AuthManager(object): - """Authentication Manager.""" - - def __new__(cls): - singleton = cls.__dict__.get('__singleton__') - if singleton is not None: - return singleton - - cls.__singleton__ = singleton = object.__new__(cls) - - return singleton - - - def __init__(self): - self.passwd = {} - self._auth = {} - - - def __repr__(self): - return '' % (self.method) - - - def add_auth(self, uri, auth): - """Registers AuthObject to AuthManager.""" - - uri = self.reduce_uri(uri, False) - - # try to make it an AuthObject - if not isinstance(auth, AuthObject): - try: - auth = AuthObject(*auth) - except TypeError: - pass - - self._auth[uri] = auth - - - def add_password(self, realm, uri, user, passwd): - """Adds password to AuthManager.""" - # uri could be a single URI or a sequence - if isinstance(uri, basestring): - uri = [uri] - - reduced_uri = tuple([self.reduce_uri(u, False) for u in uri]) - - if reduced_uri not in self.passwd: - self.passwd[reduced_uri] = {} - self.passwd[reduced_uri] = (user, passwd) - - - def find_user_password(self, realm, authuri): - for uris, authinfo in self.passwd.iteritems(): - reduced_authuri = self.reduce_uri(authuri, False) - for uri in uris: - if self.is_suburi(uri, reduced_authuri): - return authinfo - - return (None, None) - - - def get_auth(self, uri): - (in_domain, in_path) = self.reduce_uri(uri, False) - - for domain, path, authority in ( - (i[0][0], i[0][1], i[1]) for i in self._auth.iteritems() - ): - if in_domain == domain: - if path in in_path: - return authority - - - def reduce_uri(self, uri, default_port=True): - """Accept authority or URI and extract only the authority and path.""" - # note HTTP URLs do not have a userinfo component - parts = urllib2.urlparse.urlsplit(uri) - if parts[1]: - # URI - scheme = parts[0] - authority = parts[1] - path = parts[2] or '/' - else: - # host or host:port - scheme = None - authority = uri - path = '/' - host, port = urllib2.splitport(authority) - if default_port and port is None and scheme is not None: - dport = {"http": 80, - "https": 443, - }.get(scheme) - if dport is not None: - authority = "%s:%d" % (host, dport) - - return authority, path - - - def is_suburi(self, base, test): - """Check if test is below base in a URI tree - - Both args must be URIs in reduced form. - """ - if base == test: - return True - if base[0] != test[0]: - return False - common = urllib2.posixpath.commonprefix((base[1], test[1])) - if len(common) == len(base[1]): - return True - return False - - - def empty(self): - self.passwd = {} - - - def remove(self, uri, realm=None): - # uri could be a single URI or a sequence - if isinstance(uri, basestring): - uri = [uri] - - for default_port in True, False: - reduced_uri = tuple([self.reduce_uri(u, default_port) for u in uri]) - del self.passwd[reduced_uri][realm] - - - def __contains__(self, uri): - # uri could be a single URI or a sequence - if isinstance(uri, basestring): - uri = [uri] - - uri = tuple([self.reduce_uri(u, False) for u in uri]) - - if uri in self.passwd: - return True - - return False - -auth_manager = AuthManager() - - - -class AuthObject(object): - """The :class:`AuthObject` is a simple HTTP Authentication token. When - given to a Requests function, it enables Basic HTTP Authentication for that - Request. You can also enable Authorization for domain realms with AutoAuth. - See AutoAuth for more details. - - :param username: Username to authenticate with. - :param password: Password for given username. - :param realm: (optional) the realm this auth applies to - :param handler: (optional) basic || digest || proxy_basic || proxy_digest - """ - - _handlers = { - 'basic': _HTTPBasicAuthHandler, - 'digest': urllib2.HTTPDigestAuthHandler, - 'proxy_basic': urllib2.ProxyBasicAuthHandler, - 'proxy_digest': urllib2.ProxyDigestAuthHandler - } - - def __init__(self, username, password, handler='basic', realm=None): - self.username = username - self.password = password - self.realm = realm - - if isinstance(handler, basestring): - self.handler = self._handlers.get(handler.lower(), urllib2.HTTPBasicAuthHandler) - else: - self.handler = handler - - - - -def request(method, url, **kwargs): - """Sends a `method` request. Returns :class:`Response` object. - - :param method: method for the new :class:`Request` object. - :param url: URL for the new :class:`Request` object. - :param params: (optional) Dictionary of GET/HEAD/DELETE Parameters to send with the :class:`Request`. - :param data: (optional) Bytes/Dictionary of PUT/POST Data to send with the :class:`Request`. - :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. - :param cookies: (optional) CookieJar object to send with the :class:`Request`. - :param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. - :param auth: (optional) AuthObject to enable Basic HTTP Auth. - :param timeout: (optional) Float describing the timeout of the request. - """ - data = kwargs.pop('data', dict()) or kwargs.pop('params', dict()) - - r = Request(method=method, url=url, data=data, headers=kwargs.pop('headers', {}), - cookiejar=kwargs.pop('cookies', None), files=kwargs.pop('files', None), - auth=kwargs.pop('auth', auth_manager.get_auth(url)), - timeout=kwargs.pop('timeout', requests.timeout)) - r.send() - - return r.response - - -def get(url, params={}, headers={}, cookies=None, auth=None, **kwargs): - """Sends a GET request. Returns :class:`Response` object. - - :param url: URL for the new :class:`Request` object. - :param params: (optional) Dictionary of GET Parameters to send with the :class:`Request`. - :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. - :param cookies: (optional) CookieJar object to send with the :class:`Request`. - :param auth: (optional) AuthObject to enable Basic HTTP Auth. - :param timeout: (optional) Float describing the timeout of the request. - """ - - return request('GET', url, params=params, headers=headers, cookies=cookies, auth=auth, **kwargs) - - -def head(url, params={}, headers={}, cookies=None, auth=None, **kwargs): - """Sends a HEAD request. Returns :class:`Response` object. - - :param url: URL for the new :class:`Request` object. - :param params: (optional) Dictionary of GET Parameters to send with the :class:`Request`. - :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. - :param cookies: (optional) CookieJar object to send with the :class:`Request`. - :param auth: (optional) AuthObject to enable Basic HTTP Auth. - :param timeout: (optional) Float describing the timeout of the request. - """ - - return request('HEAD', url, params=params, headers=headers, cookies=cookies, auth=auth, **kwargs) - - -def post(url, data={}, headers={}, files=None, cookies=None, auth=None, **kwargs): - """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 files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. - :param cookies: (optional) CookieJar object to send with the :class:`Request`. - :param auth: (optional) AuthObject to enable Basic HTTP Auth. - :param timeout: (optional) Float describing the timeout of the request. - """ - - return request('POST', url, data=data, headers=headers, files=files, cookies=cookies, auth=auth, **kwargs) - - -def put(url, data='', headers={}, files={}, cookies=None, auth=None, **kwargs): - """Sends a PUT request. Returns :class:`Response` object. - - :param url: URL for the new :class:`Request` object. - :param params: (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 files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. - :param cookies: (optional) CookieJar object to send with the :class:`Request`. - :param auth: (optional) AuthObject to enable Basic HTTP Auth. - :param timeout: (optional) Float describing the timeout of the request. - """ - - return request('PUT', url, data=data, headers=headers, files=files, cookies=cookies, auth=auth, **kwargs) - - -def delete(url, params={}, headers={}, cookies=None, auth=None, **kwargs): - """Sends a DELETE request. Returns :class:`Response` object. - - :param url: URL for the new :class:`Request` object. - :param params: (optional) Dictionary of DELETE Parameters to send with the :class:`Request`. - :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`. - :param cookies: (optional) CookieJar object to send with the :class:`Request`. - :param auth: (optional) AuthObject to enable Basic HTTP Auth. - :param timeout: (optional) Float describing the timeout of the request. - """ - - return request('DELETE', url, params=params, headers=headers, cookies=cookies, auth=auth, **kwargs) - - - -class RequestException(Exception): - """There was an ambiguous exception that occured while handling your - request.""" - -class AuthenticationError(RequestException): - """The authentication credentials provided were invalid.""" - -class URLRequired(RequestException): - """A valid URL is required to make a request.""" - -class InvalidMethod(RequestException): - """An inappropriate method was attempted.""" +from .models import HTTPError, auth_manager +from .api import * diff --git a/requests/models.py b/requests/models.py new file mode 100644 index 00000000..5ea19698 --- /dev/null +++ b/requests/models.py @@ -0,0 +1,435 @@ +# -*- coding: utf-8 -*- + +""" +requests.system +~~~~~~~~~~~~~~~ + +""" + +import requests +import urllib +import urllib2 +import socket +import zlib + +from urllib2 import HTTPError +from urlparse import urlparse + +from .monkeys import _Request, _HTTPBasicAuthHandler, _HTTPDigestAuthHandler + +from .packages.poster.encode import multipart_encode +from .packages.poster.streaminghttp import register_openers, get_handlers + + + +class Request(object): + """The :class:`Request` object. It carries out all functionality of + Requests. Recommended interface is with the Requests functions. + """ + + _METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE') + + def __init__(self, url=None, headers=dict(), files=None, method=None, + data=dict(), auth=None, cookiejar=None, timeout=None): + + socket.setdefaulttimeout(timeout) + + self.url = url + self.headers = headers + self.files = files + self.method = method + self.data = {} + + # self.data = {} + if hasattr(data, 'items'): + for (k, v) in data.items(): + self.data.update({ + k.encode('utf-8') if isinstance(k, unicode) else k: + v.encode('utf-8') if isinstance(v, unicode) else v + }) + + # url encode data if it's a dict + if hasattr(data, 'items'): + self._enc_data = urllib.urlencode(self.data) + else: + self._enc_data = data + + + self.response = Response() + + if isinstance(auth, (list, tuple)): + auth = AuthObject(*auth) + if not auth: + auth = auth_manager.get_auth(self.url) + self.auth = auth + self.cookiejar = cookiejar + self.sent = False + + + def __repr__(self): + return '' % (self.method) + + + def __setattr__(self, name, value): + if (name == 'method') and (value): + if not value in self._METHODS: + raise InvalidMethod() + + object.__setattr__(self, name, value) + + + def _checks(self): + """Deterministic checks for consistency.""" + + if not self.url: + raise URLRequired + + + def _get_opener(self): + """Creates appropriate opener object for urllib2.""" + + _handlers = [] + + if self.cookiejar is not None: + _handlers.append(urllib2.HTTPCookieProcessor(self.cookiejar)) + + if self.auth: + if not isinstance(self.auth.handler, (urllib2.AbstractBasicAuthHandler, urllib2.AbstractDigestAuthHandler)): + auth_manager.add_password(self.auth.realm, self.url, self.auth.username, self.auth.password) + self.auth.handler = self.auth.handler(auth_manager) + auth_manager.add_auth(self.url, self.auth) + + _handlers.append(self.auth.handler) + + if not _handlers: + return urllib2.urlopen + + _handlers.extend(get_handlers()) + opener = urllib2.build_opener(*_handlers) + + if self.headers: + # Allow default headers in the opener to be overloaded + normal_keys = [k.capitalize() for k in self.headers] + for key, val in opener.addheaders[:]: + if key not in normal_keys: + continue + # Remove it, we have a value to take its place + opener.addheaders.remove((key, val)) + + return opener.open + + def _build_response(self, resp): + """Build internal Response object from given response.""" + if isinstance(resp, HTTPError): + # print resp.__dict__ + pass + + self.response.status_code = getattr(resp, 'code', None) + + try: + self.response.headers = getattr(resp.info(), 'dict', None) + self.response.content = resp.read() + except AttributeError, why: + pass + + if self.response.headers.get('content-encoding', None) == 'gzip': + try: + self.response.content = zlib.decompress(self.response.content, 16+zlib.MAX_WBITS) + except zlib.error: + pass + + self.response.url = getattr(resp, 'url', None) + + + @staticmethod + def _build_url(url, data=None): + """Build URLs.""" + + if urlparse(url).query: + return '%s&%s' % (url, data) + else: + if data: + return '%s?%s' % (url, data) + else: + return url + + + def send(self, anyway=False): + """Sends the request. Returns True of successful, false if not. + If there was an HTTPError during transmission, + self.response.status_code will contain the HTTPError code. + + Once a request is successfully sent, `sent` will equal True. + + :param anyway: If True, request will be sent, even if it has + already been sent. + """ + self._checks() + success = False + + if self.method in ('GET', 'HEAD', 'DELETE'): + req = _Request(self._build_url(self.url, self._enc_data), method=self.method) + else: + + if self.files: + register_openers() + + if self.data: + self.files.update(self.data) + + datagen, headers = multipart_encode(self.files) + req = _Request(self.url, data=datagen, headers=headers, method=self.method) + + else: + req = _Request(self.url, data=self._enc_data, method=self.method) + + if self.headers: + req.headers.update(self.headers) + + if not self.sent or anyway: + + try: + opener = self._get_opener() + resp = opener(req) + + if self.cookiejar is not None: + self.cookiejar.extract_cookies(resp, req) + + except urllib2.HTTPError, why: + self._build_response(why) + self.response.error = why + else: + self._build_response(resp) + self.response.ok = True + + self.response.cached = False + else: + self.response.cached = True + + self.sent = self.response.ok + + return self.sent + + + def read(self, *args): + return self.response.read() + + + +class Response(object): + """The :class:`Request` object. All :class:`Request` objects contain a + :class:`Request.response ` attribute, which is an instance of + this class. + """ + + def __init__(self): + self.content = None + self.status_code = None + self.headers = dict() + self.url = None + self.ok = False + self.error = None + self.cached = False + + + def __repr__(self): + return '' % (self.status_code) + + + def __nonzero__(self): + """Returns true if status_code is 'OK'.""" + return not self.error + + + def raise_for_status(self): + """Raises stored HTTPError if one exists.""" + if self.error: + raise self.error + + def read(self, *args): + return self.content + + + +class AuthManager(object): + """Authentication Manager.""" + + def __new__(cls): + singleton = cls.__dict__.get('__singleton__') + if singleton is not None: + return singleton + + cls.__singleton__ = singleton = object.__new__(cls) + + return singleton + + + def __init__(self): + self.passwd = {} + self._auth = {} + + + def __repr__(self): + return '' % (self.method) + + + def add_auth(self, uri, auth): + """Registers AuthObject to AuthManager.""" + + uri = self.reduce_uri(uri, False) + + # try to make it an AuthObject + if not isinstance(auth, AuthObject): + try: + auth = AuthObject(*auth) + except TypeError: + pass + + self._auth[uri] = auth + + + def add_password(self, realm, uri, user, passwd): + """Adds password to AuthManager.""" + # uri could be a single URI or a sequence + if isinstance(uri, basestring): + uri = [uri] + + reduced_uri = tuple([self.reduce_uri(u, False) for u in uri]) + + if reduced_uri not in self.passwd: + self.passwd[reduced_uri] = {} + self.passwd[reduced_uri] = (user, passwd) + + + def find_user_password(self, realm, authuri): + for uris, authinfo in self.passwd.iteritems(): + reduced_authuri = self.reduce_uri(authuri, False) + for uri in uris: + if self.is_suburi(uri, reduced_authuri): + return authinfo + + return (None, None) + + + def get_auth(self, uri): + (in_domain, in_path) = self.reduce_uri(uri, False) + + for domain, path, authority in ( + (i[0][0], i[0][1], i[1]) for i in self._auth.iteritems() + ): + if in_domain == domain: + if path in in_path: + return authority + + + def reduce_uri(self, uri, default_port=True): + """Accept authority or URI and extract only the authority and path.""" + # note HTTP URLs do not have a userinfo component + parts = urllib2.urlparse.urlsplit(uri) + if parts[1]: + # URI + scheme = parts[0] + authority = parts[1] + path = parts[2] or '/' + else: + # host or host:port + scheme = None + authority = uri + path = '/' + host, port = urllib2.splitport(authority) + if default_port and port is None and scheme is not None: + dport = {"http": 80, + "https": 443, + }.get(scheme) + if dport is not None: + authority = "%s:%d" % (host, dport) + + return authority, path + + + def is_suburi(self, base, test): + """Check if test is below base in a URI tree + + Both args must be URIs in reduced form. + """ + if base == test: + return True + if base[0] != test[0]: + return False + common = urllib2.posixpath.commonprefix((base[1], test[1])) + if len(common) == len(base[1]): + return True + return False + + + def empty(self): + self.passwd = {} + + + def remove(self, uri, realm=None): + # uri could be a single URI or a sequence + if isinstance(uri, basestring): + uri = [uri] + + for default_port in True, False: + reduced_uri = tuple([self.reduce_uri(u, default_port) for u in uri]) + del self.passwd[reduced_uri][realm] + + + def __contains__(self, uri): + # uri could be a single URI or a sequence + if isinstance(uri, basestring): + uri = [uri] + + uri = tuple([self.reduce_uri(u, False) for u in uri]) + + if uri in self.passwd: + return True + + return False + +auth_manager = AuthManager() + + + +class AuthObject(object): + """The :class:`AuthObject` is a simple HTTP Authentication token. When + given to a Requests function, it enables Basic HTTP Authentication for that + Request. You can also enable Authorization for domain realms with AutoAuth. + See AutoAuth for more details. + + :param username: Username to authenticate with. + :param password: Password for given username. + :param realm: (optional) the realm this auth applies to + :param handler: (optional) basic || digest || proxy_basic || proxy_digest + """ + + _handlers = { + 'basic': _HTTPBasicAuthHandler, + 'digest': _HTTPDigestAuthHandler, + 'proxy_basic': urllib2.ProxyBasicAuthHandler, + 'proxy_digest': urllib2.ProxyDigestAuthHandler + } + + def __init__(self, username, password, handler='basic', realm=None): + self.username = username + self.password = password + self.realm = realm + + if isinstance(handler, basestring): + self.handler = self._handlers.get(handler.lower(), urllib2.HTTPBasicAuthHandler) + else: + self.handler = handler + +class RequestException(Exception): + """There was an ambiguous exception that occured while handling your + request.""" + +class AuthenticationError(RequestException): + """The authentication credentials provided were invalid.""" + +class URLRequired(RequestException): + """A valid URL is required to make a request.""" + +class InvalidMethod(RequestException): + """An inappropriate method was attempted.""" diff --git a/requests/monkeys.py b/requests/monkeys.py new file mode 100644 index 00000000..0b746356 --- /dev/null +++ b/requests/monkeys.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +""" +requests.monkeys +~~~~~~~~~~~~~~~~ + +Monkey patches to urllib2 and the like. + +""" + +# import requests +# import urllib +import urllib2 +# import socket +# import zlib + + +class _Request(urllib2.Request): + """Hidden wrapper around the urllib2.Request object. Allows for manual + setting of HTTP methods. + """ + + def __init__(self, url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None): + urllib2.Request.__init__(self, url, data, headers, origin_req_host, unverifiable) + self.method = method + + def get_method(self): + if self.method: + return self.method + + return urllib2.Request.get_method(self) + + +class _HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler): + # from mercurial + + def __init__(self, *args, **kwargs): + urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs) + self.retried_req = None + + def reset_retry_count(self): + # Python 2.6.5 will call this on 401 or 407 errors and thus loop + # forever. We disable reset_retry_count completely and reset in + # http_error_auth_reqed instead. + pass + + def http_error_auth_reqed(self, auth_header, host, req, headers): + # Reset the retry counter once for each request. + if req is not self.retried_req: + self.retried_req = req + self.retried = 0 + return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed( + self, auth_header, host, req, headers) + + + +class _HTTPDigestAuthHandler(urllib2.HTTPDigestAuthHandler): + + def __init__(self, *args, **kwargs): + urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs) + self.retried_req = None + + def reset_retry_count(self): + # Python 2.6.5 will call this on 401 or 407 errors and thus loop + # forever. We disable reset_retry_count completely and reset in + # http_error_auth_reqed instead. + pass + + def http_error_auth_reqed(self, auth_header, host, req, headers): + # Reset the retry counter once for each request. + if req is not self.retried_req: + self.retried_req = req + self.retried = 0 + # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if + # it doesn't know about the auth type requested. This can happen if + # somebody is using BasicAuth and types a bad password. + + try: + return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed( + self, auth_header, host, req, headers) + except ValueError, inst: + arg = inst.args[0] + if arg.startswith("AbstractDigestAuthHandler doesn't know "): + return + raise \ No newline at end of file diff --git a/requests/patches.py b/requests/patches.py new file mode 100644 index 00000000..43a3b4c4 --- /dev/null +++ b/requests/patches.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +""" +requests.monkeys +""" diff --git a/test_requests.py b/test_requests.py index 3bc2e092..41686722 100755 --- a/test_requests.py +++ b/test_requests.py @@ -123,6 +123,7 @@ class RequestsTestSuite(unittest.TestCase): """ .. todo:: This really doesn't test to make sure the cookie is working """ + jar = cookielib.CookieJar() self.assertFalse(jar) @@ -154,7 +155,6 @@ class RequestsTestSuite(unittest.TestCase): r = requests.get('https://convore.com/api/account/verify.json', auth=conv_auth) self.assertEquals(r.status_code, 401) - print r.__dict__ if __name__ == '__main__': From 8ba12ff4f3fb17ad5b9f21076474cbd7558ca881 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 14:24:06 -0400 Subject: [PATCH 08/10] cleaner patch names --- requests/models.py | 6 +++--- requests/monkeys.py | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/requests/models.py b/requests/models.py index 5ea19698..65696bcc 100644 --- a/requests/models.py +++ b/requests/models.py @@ -15,7 +15,7 @@ import zlib from urllib2 import HTTPError from urlparse import urlparse -from .monkeys import _Request, _HTTPBasicAuthHandler, _HTTPDigestAuthHandler +from .monkeys import Request as _Request, HTTPBasicAuthHandler, HTTPDigestAuthHandler from .packages.poster.encode import multipart_encode from .packages.poster.streaminghttp import register_openers, get_handlers @@ -405,8 +405,8 @@ class AuthObject(object): """ _handlers = { - 'basic': _HTTPBasicAuthHandler, - 'digest': _HTTPDigestAuthHandler, + 'basic': HTTPBasicAuthHandler, + 'digest': HTTPDigestAuthHandler, 'proxy_basic': urllib2.ProxyBasicAuthHandler, 'proxy_digest': urllib2.ProxyDigestAuthHandler } diff --git a/requests/monkeys.py b/requests/monkeys.py index 0b746356..145838f2 100644 --- a/requests/monkeys.py +++ b/requests/monkeys.py @@ -4,18 +4,14 @@ requests.monkeys ~~~~~~~~~~~~~~~~ -Monkey patches to urllib2 and the like. +Urllib2 Monkey patches. """ -# import requests -# import urllib import urllib2 -# import socket -# import zlib -class _Request(urllib2.Request): +class Request(urllib2.Request): """Hidden wrapper around the urllib2.Request object. Allows for manual setting of HTTP methods. """ @@ -31,7 +27,7 @@ class _Request(urllib2.Request): return urllib2.Request.get_method(self) -class _HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler): +class HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler): # from mercurial def __init__(self, *args, **kwargs): @@ -54,7 +50,7 @@ class _HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler): -class _HTTPDigestAuthHandler(urllib2.HTTPDigestAuthHandler): +class HTTPDigestAuthHandler(urllib2.HTTPDigestAuthHandler): def __init__(self, *args, **kwargs): urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs) From d77c0ec138c47bc554d627761042c995fa6803b1 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 14:29:41 -0400 Subject: [PATCH 09/10] history update --- HISTORY.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index c579f8a1..b3a26d04 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,9 +4,12 @@ History 0.3.4 +++++ +* Urllib2 HTTPAuthentication Recursion fix (Basic/Digest) +* Internal Refactor * Bytes data upload Bugfix + 0.3.3 (2011-05-12) ++++++++++++++++++ From fa1db9581477b8748b76344623e9b3eb8f77f4f9 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 14:30:36 -0400 Subject: [PATCH 10/10] Version bump: v0.3.4 --- requests/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requests/core.py b/requests/core.py index a07fca2c..e5231c41 100644 --- a/requests/core.py +++ b/requests/core.py @@ -12,8 +12,8 @@ This module implements the main Requests system. """ __title__ = 'requests' -__version__ = '0.3.3' -__build__ = 0x000303 +__version__ = '0.3.4' +__build__ = 0x000304 __author__ = 'Kenneth Reitz' __license__ = 'ISC' __copyright__ = 'Copyright 2011 Kenneth Reitz'