From 291d4742d63e523eb4353758039d145f2c1e2dfc Mon Sep 17 00:00:00 2001 From: Jeremy Selier Date: Tue, 2 Aug 2011 18:05:03 +0300 Subject: [PATCH 01/83] Fixes correct "relative" redirections. Example: http://digg.com/d1qIwX, which redirects to "/news/story/A_Manifesto_How_to_Save_Media", which requests transforms to: http://digg.com//news/story/A_Manifesto_How_to_Save_Media" which results in an endless redirect loop. --- requests/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/requests/models.py b/requests/models.py index 7fb07fb3..55a6a48a 100644 --- a/requests/models.py +++ b/requests/models.py @@ -12,7 +12,7 @@ import socket import zlib from urllib2 import HTTPError -from urlparse import urlparse, urlunparse +from urlparse import urlparse, urlunparse, urljoin from datetime import datetime from .config import settings @@ -197,9 +197,7 @@ class Request(object): # Facilitate non-RFC2616-compliant 'location' headers # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') - if not urlparse(url).netloc: - parent_url_components = urlparse(self.url) - url = '%s://%s/%s' % (parent_url_components.scheme, parent_url_components.netloc, url) + url = urljoin(self.url, url) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 if r.status_code is 303: From 5a7ec361f4bf093e251d4953bcf6a432717cf690 Mon Sep 17 00:00:00 2001 From: Jeremy Selier Date: Tue, 2 Aug 2011 18:51:21 +0300 Subject: [PATCH 02/83] Join with the latest URL not the first one. --- requests/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests/models.py b/requests/models.py index 55a6a48a..51954310 100644 --- a/requests/models.py +++ b/requests/models.py @@ -197,7 +197,7 @@ class Request(object): # Facilitate non-RFC2616-compliant 'location' headers # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') - url = urljoin(self.url, url) + url = urljoin(r.url, url) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 if r.status_code is 303: From a987729712c3fa150fc86550495f6ed2aff4bfc0 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 09:14:49 -0400 Subject: [PATCH 03/83] whitespace! --- test_requests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test_requests.py b/test_requests.py index 8c137fad..bb68a124 100755 --- a/test_requests.py +++ b/test_requests.py @@ -324,15 +324,18 @@ class RequestsTestSuite(unittest.TestCase): self.assertEquals(rbody.get('form'), None) self.assertEquals(rbody.get('data'), 'foobar') + def test_idna(self): r = requests.get(u'http://➡.ws/httpbin') assert 'httpbin' in r.url + def test_urlencoded_get_query_multivalued_param(self): r = requests.get(httpbin('get'), params=dict(test=['foo','baz'])) self.assertEquals(r.status_code, 200) self.assertEquals(r.url, httpbin('get?test=foo&test=baz')) + def test_urlencoded_post_querystring_multivalued(self): r = requests.post(httpbin('post'), params=dict(test=['foo','baz'])) self.assertEquals(r.status_code, 200) @@ -342,6 +345,7 @@ class RequestsTestSuite(unittest.TestCase): self.assertEquals(rbody.get('form'), {}) # No form supplied self.assertEquals(rbody.get('data'), '') + def test_urlencoded_post_query_multivalued_and_data(self): r = requests.post(httpbin('post'), params=dict(test=['foo','baz']), data=dict(test2="foobar",test3=['foo','baz'])) From bb16fe8115420f9ff40faed039f79ec57e4f4323 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 09:37:07 -0400 Subject: [PATCH 04/83] http/https tests --- test_requests.py | 295 ++++++++++++++++++++++++++++++----------------- 1 file changed, 188 insertions(+), 107 deletions(-) diff --git a/test_requests.py b/test_requests.py index bb68a124..dabf03c8 100755 --- a/test_requests.py +++ b/test_requests.py @@ -34,6 +34,9 @@ def httpsbin(*suffix): return HTTPSBIN_URL + '/'.join(suffix) +SERVICES = (httpbin, httpsbin) + + class RequestsTestSuite(unittest.TestCase): """Requests test cases.""" @@ -129,61 +132,81 @@ class RequestsTestSuite(unittest.TestCase): def test_AUTH_HTTPS_200_OK_GET(self): - auth = ('user', 'pass') - url = httpsbin('basic-auth', 'user', 'pass') - r = requests.get(url, auth=auth) - self.assertEqual(r.status_code, 200) + for service in SERVICES: - r = requests.get(url) - self.assertEqual(r.status_code, 200) + auth = ('user', 'pass') + url = service('basic-auth', 'user', 'pass') + r = requests.get(url, auth=auth) - # reset auto authentication - requests.auth_manager.empty() + self.assertEqual(r.status_code, 200) + + r = requests.get(url) + self.assertEqual(r.status_code, 200) + + # reset auto authentication + requests.auth_manager.empty() def test_POSTBIN_GET_POST_FILES(self): - url = httpbin('post') - post = requests.post(url).raise_for_status() - post = requests.post(url, data={'some': 'data'}) - self.assertEqual(post.status_code, 200) + for service in SERVICES: - post2 = requests.post(url, files={'some': open('test_requests.py')}) - self.assertEqual(post2.status_code, 200) + url = service('post') + post = requests.post(url).raise_for_status() - post3 = requests.post(url, data='[{"some": "json"}]') - self.assertEqual(post3.status_code, 200) + post = requests.post(url, data={'some': 'data'}) + self.assertEqual(post.status_code, 200) + + post2 = requests.post(url, files={'some': open('test_requests.py')}) + self.assertEqual(post2.status_code, 200) + + post3 = requests.post(url, data='[{"some": "json"}]') + self.assertEqual(post3.status_code, 200) def test_POSTBIN_GET_POST_FILES_WITH_PARAMS(self): - url = httpbin('post') - post = requests.post(url, files={'some': open('test_requests.py')}, data={'some': 'data'}) - self.assertEqual(post.status_code, 200) + for service in SERVICES: + + url = service('post') + post = requests.post(url, + files={'some': open('test_requests.py')}, + data={'some': 'data'}) + + self.assertEqual(post.status_code, 200) def test_POSTBIN_GET_POST_FILES_WITH_HEADERS(self): - url = httpbin('post') + for service in SERVICES: - post2 = requests.post(url, files={'some': open('test_requests.py')}, - headers = {'User-Agent': 'requests-tests'}) + url = service('post') - self.assertEqual(post2.status_code, 200) + post2 = requests.post(url, + files={'some': open('test_requests.py')}, + headers = {'User-Agent': 'requests-tests'}) + + self.assertEqual(post2.status_code, 200) def test_nonzero_evaluation(self): - r = requests.get(httpbin('status', '500')) - self.assertEqual(bool(r), False) - r = requests.get(httpbin('/')) - self.assertEqual(bool(r), True) + for service in SERVICES: + + r = requests.get(service('status', '500')) + self.assertEqual(bool(r), False) + + r = requests.get(service('/')) + self.assertEqual(bool(r), True) def test_request_ok_set(self): - r = requests.get(httpbin('status', '404')) - self.assertEqual(r.ok, False) + + for service in SERVICES: + + r = requests.get(service('status', '404')) + self.assertEqual(r.ok, False) def test_status_raising(self): @@ -230,22 +253,32 @@ class RequestsTestSuite(unittest.TestCase): self.assertEquals(r.status_code, 200) + requests.auth_manager.add_auth('httpbin.ep.io', http_auth) + + r = requests.get(httpsbin('basic-auth', 'user', 'pass')) + self.assertEquals(r.status_code, 200) + + def test_unicode_get(self): - url = httpbin('/') + for service in SERVICES: - requests.get(url, params={'foo': u'føø'}) - requests.get(url, params={u'føø': u'føø'}) - requests.get(url, params={'føø': 'føø'}) - requests.get(url, params={'foo': u'foo'}) - requests.get(httpbin('ø'), params={'foo': u'foo'}) + url = service('/') + + requests.get(url, params={'foo': u'føø'}) + requests.get(url, params={u'føø': u'føø'}) + requests.get(url, params={'føø': 'føø'}) + requests.get(url, params={'foo': u'foo'}) + requests.get(service('ø'), params={'foo': u'foo'}) def test_httpauth_recursion(self): + http_auth = ('user', 'BADpass') - r = requests.get(httpbin('basic-auth', 'user', 'pass'), auth=http_auth) - self.assertEquals(r.status_code, 401) + for service in SERVICES: + r = requests.get(service('basic-auth', 'user', 'pass'), auth=http_auth) + self.assertEquals(r.status_code, 401) def test_settings(self): @@ -262,67 +295,101 @@ class RequestsTestSuite(unittest.TestCase): def test_urlencoded_post_data(self): - r = requests.post(httpbin('post'), data=dict(test='fooaowpeuf')) - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post')) - rbody = json.loads(r.content) - self.assertEquals(rbody.get('form'), dict(test='fooaowpeuf')) - self.assertEquals(rbody.get('data'), '') + + for service in SERVICES: + + r = requests.post(service('post'), data=dict(test='fooaowpeuf')) + + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post')) + + rbody = json.loads(r.content) + + self.assertEquals(rbody.get('form'), dict(test='fooaowpeuf')) + self.assertEquals(rbody.get('data'), '') def test_nonurlencoded_post_data(self): - r = requests.post(httpbin('post'), data='fooaowpeuf') - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post')) - rbody = json.loads(r.content) - # Body wasn't valid url encoded data, so the server returns None as - # "form" and the raw body as "data". - self.assertEquals(rbody.get('form'), None) - self.assertEquals(rbody.get('data'), 'fooaowpeuf') + + for service in SERVICES: + + r = requests.post(service('post'), data='fooaowpeuf') + + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post')) + + rbody = json.loads(r.content) + # Body wasn't valid url encoded data, so the server returns None as + # "form" and the raw body as "data". + self.assertEquals(rbody.get('form'), None) + self.assertEquals(rbody.get('data'), 'fooaowpeuf') def test_urlencoded_post_querystring(self): - r = requests.post(httpbin('post'), params=dict(test='fooaowpeuf')) - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post?test=fooaowpeuf')) - rbody = json.loads(r.content) - self.assertEquals(rbody.get('form'), {}) # No form supplied - self.assertEquals(rbody.get('data'), '') + + for service in SERVICES: + + r = requests.post(service('post'), params=dict(test='fooaowpeuf')) + + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post?test=fooaowpeuf')) + + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), {}) # No form supplied + self.assertEquals(rbody.get('data'), '') def test_nonurlencoded_post_querystring(self): - r = requests.post(httpbin('post'), params='fooaowpeuf') - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post?fooaowpeuf')) - rbody = json.loads(r.content) - self.assertEquals(rbody.get('form'), {}) # No form supplied - self.assertEquals(rbody.get('data'), '') + + for service in SERVICES: + + r = requests.post(service('post'), params='fooaowpeuf') + + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post?fooaowpeuf')) + + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), {}) # No form supplied + self.assertEquals(rbody.get('data'), '') def test_urlencoded_post_query_and_data(self): - r = requests.post(httpbin('post'), params=dict(test='fooaowpeuf'), - data=dict(test2="foobar")) - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post?test=fooaowpeuf')) - rbody = json.loads(r.content) - self.assertEquals(rbody.get('form'), dict(test2='foobar')) - self.assertEquals(rbody.get('data'), '') + + for service in SERVICES: + + r = requests.post( + service('post'), + params=dict(test='fooaowpeuf'), + data=dict(test2="foobar")) + + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post?test=fooaowpeuf')) + + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), dict(test2='foobar')) + self.assertEquals(rbody.get('data'), '') def test_nonurlencoded_post_query_and_data(self): - r = requests.post(httpbin('post'), params='fooaowpeuf', - data="foobar") - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post?fooaowpeuf')) - rbody = json.loads(r.content) - self.assertEquals(rbody.get('form'), None) - self.assertEquals(rbody.get('data'), 'foobar') + + for service in SERVICES: + + r = requests.post(service('post'), + params='fooaowpeuf', data="foobar") + + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post?fooaowpeuf')) + + rbody = json.loads(r.content) + + self.assertEquals(rbody.get('form'), None) + self.assertEquals(rbody.get('data'), 'foobar') def test_idna(self): @@ -331,40 +398,54 @@ class RequestsTestSuite(unittest.TestCase): def test_urlencoded_get_query_multivalued_param(self): - r = requests.get(httpbin('get'), params=dict(test=['foo','baz'])) - self.assertEquals(r.status_code, 200) - self.assertEquals(r.url, httpbin('get?test=foo&test=baz')) + + for service in SERVICES: + + r = requests.get(service('get'), params=dict(test=['foo','baz'])) + self.assertEquals(r.status_code, 200) + self.assertEquals(r.url, service('get?test=foo&test=baz')) def test_urlencoded_post_querystring_multivalued(self): - r = requests.post(httpbin('post'), params=dict(test=['foo','baz'])) - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post?test=foo&test=baz')) - rbody = json.loads(r.content) - self.assertEquals(rbody.get('form'), {}) # No form supplied - self.assertEquals(rbody.get('data'), '') + + for service in SERVICES: + + r = requests.post(service('post'), params=dict(test=['foo','baz'])) + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post?test=foo&test=baz')) + + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), {}) # No form supplied + self.assertEquals(rbody.get('data'), '') def test_urlencoded_post_query_multivalued_and_data(self): - r = requests.post(httpbin('post'), params=dict(test=['foo','baz']), - data=dict(test2="foobar",test3=['foo','baz'])) - self.assertEquals(r.status_code, 200) - self.assertEquals(r.headers['content-type'], 'application/json') - self.assertEquals(r.url, httpbin('post?test=foo&test=baz')) - rbody = json.loads(r.content) - self.assertEquals(rbody.get('form'), dict(test2='foobar',test3='foo')) - self.assertEquals(rbody.get('data'), '') + + for service in SERVICES: + + r = requests.post( + service('post'), + params=dict(test=['foo','baz']), + data=dict(test2="foobar",test3=['foo','baz'])) + + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, service('post?test=foo&test=baz')) + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), dict(test2='foobar',test3='foo')) + self.assertEquals(rbody.get('data'), '') def test_redirect_history(self): - r = requests.get(httpbin('redirect', '3')) - self.assertEquals(r.status_code, 200) - self.assertEquals(len(r.history), 3) - r = requests.get(httpsbin('redirect', '3')) - self.assertEquals(r.status_code, 200) - self.assertEquals(len(r.history), 3) + for service in SERVICES: + + r = requests.get(service('redirect', '3')) + self.assertEquals(r.status_code, 200) + self.assertEquals(len(r.history), 3) + + if __name__ == '__main__': unittest.main() From b141da36a45f64a717e78e7a2091b6680f015104 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 09:37:14 -0400 Subject: [PATCH 05/83] test relative redirects --- test_requests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test_requests.py b/test_requests.py index dabf03c8..ff98c412 100755 --- a/test_requests.py +++ b/test_requests.py @@ -446,6 +446,14 @@ class RequestsTestSuite(unittest.TestCase): self.assertEquals(len(r.history), 3) + def test_relative_redirect_history(self): + + for service in SERVICES: + + r = requests.get(service('relative-redirect', '3')) + self.assertEquals(r.status_code, 200) + self.assertEquals(len(r.history), 3) + if __name__ == '__main__': unittest.main() From e7084940e1461833f2b6da840e36b87ba03a6c26 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 09:40:24 -0400 Subject: [PATCH 06/83] added Jeremy Selie to AUTHORS --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index b6e45cf4..38528a61 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,4 +30,5 @@ Patches and Suggestions - 潘旭 (Xu Pan) - Tamás Gulácsi - Rubén Abad -- Peter Manser \ No newline at end of file +- Peter Manser +- Jeremy Selie \ No newline at end of file From 2e4724cceaed86cfd6a73ee495db668ceea80be7 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 12:34:13 -0400 Subject: [PATCH 07/83] history --- HISTORY.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 171dae44..e641f0fd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,14 @@ History ------- +0.5.2 (2011-09-??) +++++++++++++++++++ + +* status code reference dict? +* Relative redirect support +* Improved https testing +* Bugfixes + 0.5.1 (2011-07-23) ++++++++++++++++++ From 5693314a37ff8815c3c6a4bb7b9ebd5ef1abb5b9 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 13:01:44 -0400 Subject: [PATCH 08/83] response error handling rework --- requests/models.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/requests/models.py b/requests/models.py index 51954310..b90a2bda 100644 --- a/requests/models.py +++ b/requests/models.py @@ -158,7 +158,7 @@ class Request(object): return opener.open - def _build_response(self, resp): + def _build_response(self, resp, is_error=False): """Build internal :class:`Response ` object from given response.""" def build(resp): @@ -173,6 +173,9 @@ class Request(object): except AttributeError: pass + if is_error: + response.error = resp + response.url = getattr(resp, 'url', None) return response @@ -312,9 +315,8 @@ class Request(object): if isinstance(why.reason, socket.timeout): why = Timeout(why) - self._build_response(why) - if not self.redirect: - self.response.error = why + self._build_response(why, is_error=True) + else: self._build_response(resp) From d4eabaaacc5ebbdaafbb619c7d809409e80c7c1b Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 13:04:14 -0400 Subject: [PATCH 09/83] better --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index e641f0fd..5ef43142 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,7 @@ History * status code reference dict? * Relative redirect support +* HTTPError handling improvements * Improved https testing * Bugfixes From 629d730da606110f2adcf46a16b7a73b829fa159 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 13:11:16 -0400 Subject: [PATCH 10/83] all main functions are **kwargs now. People shouldn't be relying on the order. --- requests/api.py | 49 ++++++++++++------------------------------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/requests/api.py b/requests/api.py index c4690f2b..872208ce 100644 --- a/requests/api.py +++ b/requests/api.py @@ -55,9 +55,7 @@ def request(method, url, return r.response -def get(url, - params=None, headers=None, cookies=None, auth=None, timeout=None, - proxies=None): +def get(url, **kwargs): """Sends a GET request. Returns :class:`Response` object. @@ -70,14 +68,10 @@ def get(url, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ - return request('GET', url, - params=params, headers=headers, cookies=cookies, auth=auth, - timeout=timeout, proxies=proxies) + return request('GET', url, **kwargs) -def head(url, - params=None, headers=None, cookies=None, auth=None, timeout=None, - proxies=None): +def head(url, **kwargs): """Sends a HEAD request. Returns :class:`Response` object. @@ -90,14 +84,10 @@ def head(url, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ - return request('HEAD', url, - params=params, headers=headers, cookies=cookies, auth=auth, - timeout=timeout, proxies=proxies) + return request('HEAD', url, **kwargs) -def post(url, - data='', headers=None, files=None, cookies=None, auth=None, timeout=None, - allow_redirects=False, params=None, proxies=None): +def post(url, data='', **kwargs): """Sends a POST request. Returns :class:`Response` object. @@ -113,14 +103,10 @@ def post(url, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ - return request('POST', url, - params=params, data=data, headers=headers, files=files, - cookies=cookies, auth=auth, timeout=timeout, - allow_redirects=allow_redirects, proxies=proxies) + return request('POST', url, data=data, **kwargs) -def put(url, data='', headers=None, files=None, cookies=None, auth=None, - timeout=None, allow_redirects=False, params=None, proxies=None): +def put(url, data='', **kwargs): """Sends a PUT request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -135,14 +121,10 @@ def put(url, data='', headers=None, files=None, cookies=None, auth=None, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ - return request('PUT', url, - params=params, data=data, headers=headers, files=files, - cookies=cookies, auth=auth, timeout=timeout, - allow_redirects=allow_redirects, proxies=proxies) + return request('PUT', url, data=data, **kwargs) -def patch(url, data='', headers=None, files=None, cookies=None, auth=None, - timeout=None, allow_redirects=False, params=None, proxies=None): +def patch(url, data='', **kwargs): """Sends a PATCH request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -157,15 +139,10 @@ def patch(url, data='', headers=None, files=None, cookies=None, auth=None, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ - return request('PATCH', url, - params=params, data=data, headers=headers, files=files, - cookies=cookies, auth=auth, timeout=timeout, - allow_redirects=allow_redirects, proxies=proxies) + return request('PATCH', url, **kwargs) -def delete(url, - params=None, headers=None, cookies=None, auth=None, timeout=None, - allow_redirects=False, proxies=None): +def delete(url, **kwargs): """Sends a DELETE request. Returns :class:`Response` object. @@ -179,6 +156,4 @@ def delete(url, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ - return request('DELETE', url, - params=params, headers=headers, cookies=cookies, auth=auth, - timeout=timeout, allow_redirects=allow_redirects, proxies=proxies) + return request('DELETE', url, **kwargs) From 10cc4673f9a6570443394c6421042f2e822d2c86 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 13:14:10 -0400 Subject: [PATCH 11/83] remove Response.cached --- requests/models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/requests/models.py b/requests/models.py index b90a2bda..14802b9e 100644 --- a/requests/models.py +++ b/requests/models.py @@ -322,13 +322,10 @@ class Request(object): 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 @@ -357,8 +354,6 @@ class Response(object): self.ok = False #: Resulting :class:`HTTPError` of request, if one occured. self.error = None - #: True, if the response :attr:`content` is cached locally. - self.cached = False #: A list of :class:`Response ` objects from #: the history of the Request. Any redirect responses will end #: up here. From 2d70647f72fc230aec3453349fc4475cd3341de7 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 13:17:17 -0400 Subject: [PATCH 12/83] attach request object to responses --- requests/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requests/models.py b/requests/models.py index 14802b9e..942bd9b2 100644 --- a/requests/models.py +++ b/requests/models.py @@ -219,6 +219,7 @@ class Request(object): r.history = history self.response = r + self.response.request = self @staticmethod @@ -322,7 +323,6 @@ class Request(object): self._build_response(resp) self.response.ok = True - else: self.sent = self.response.ok @@ -359,6 +359,8 @@ class Response(object): #: up here. self.history = [] + self.request = None + def __repr__(self): return '' % (self.status_code) From 907e5294719c7d5a230eaa3c1a03f6230b90d875 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 13:17:41 -0400 Subject: [PATCH 13/83] changing history muahaha --- HISTORY.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 5ef43142..93cb1428 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,9 @@ History ++++++++++++++++++ * status code reference dict? +* Removed Response.cached +* Added Response.request +* all args are kwargs * Relative redirect support * HTTPError handling improvements * Improved https testing From 773630b0109c7671930b7ab38bb71c22ac8a94c5 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 14:50:38 -0400 Subject: [PATCH 14/83] requests.codes --- requests/api.py | 1 + requests/core.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requests/api.py b/requests/api.py index 872208ce..1a2ce300 100644 --- a/requests/api.py +++ b/requests/api.py @@ -13,6 +13,7 @@ This module impliments the Requests API. import config from .models import Request, Response, AuthManager, AuthObject, auth_manager +from .status_codes import codes __all__ = ('request', 'get', 'head', 'post', 'patch', 'put', 'delete') diff --git a/requests/core.py b/requests/core.py index 5215fca2..7b5f4b20 100644 --- a/requests/core.py +++ b/requests/core.py @@ -22,4 +22,5 @@ __copyright__ = 'Copyright 2011 Kenneth Reitz' from models import HTTPError, auth_manager from api import * from exceptions import * +from status_codes import codes from config import settings \ No newline at end of file From 6cead64345d875d0049ef27655c9d5f650033dc5 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 14:50:48 -0400 Subject: [PATCH 15/83] supporting codes class --- requests/structures.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/requests/structures.py b/requests/structures.py index dd5168cf..d068bf9c 100644 --- a/requests/structures.py +++ b/requests/structures.py @@ -45,3 +45,21 @@ class CaseInsensitiveDict(dict): return self[key] else: return default + +class LookupDict(dict): + """Dictionary lookup object.""" + + def __init__(self, name=None): + self.name = name + super(LookupDict, self).__init__() + + def __repr__(self): + return '' % (self.name) + + def __getitem__(self, key): + # We allow fall-through here, so values default to None + + return self.__dict__.get(key, None) + + def get(self, key, default=None): + return self.__dict__.get(key, default) \ No newline at end of file From 9469b4bb6939e84b4539e6cd854e12c3d6b5d1fb Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 14:50:59 -0400 Subject: [PATCH 16/83] status_codes lookup dict --- requests/status_codes.py | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 requests/status_codes.py diff --git a/requests/status_codes.py b/requests/status_codes.py new file mode 100644 index 00000000..e9a72fcf --- /dev/null +++ b/requests/status_codes.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +from .structures import LookupDict + +_codes = { + 100: ('continue',), + 101: ('switching_protocols',), + 102: ('processing',), + 103: ('checkpoint',), + 122: ('uri_too_long', 'request_uri_too_long'), + 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\o/'), + 201: ('created',), + 202: ('accepted',), + 203: ('non_authoritative_info', 'non_authoritative_information'), + 204: ('no_content',), + 205: ('reset_content', 'reset'), + 206: ('partial_content', 'partial'), + 207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'), + 208: ('im_used',), + 300: ('multiple_choices',), + 301: ('moved_pemanently', 'moved'), + 302: ('found',), + 302: ('see_other', 'other'), + 304: ('not_modified',), + 305: ('use_proxy',), + 306: ('switch_proxy',), + 307: ('temporary_redirect', 'temporary_moved', 'temporary'), + 308: ('resume_incomplete', 'resume'), + 400: ('bad_request', 'bad'), + 401: ('unauthorized',), + 402: ('payment_required', 'payment'), + 403: ('forbidden',), + 404: ('not_found',), + 405: ('method_not_allowed', 'not_allowed'), + 406: ('not_acceptable',), + 407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'), + 408: ('request_timeout', 'timeout'), + 409: ('conflict',), + 410: ('gone',), + 411: ('length_required',), + 412: ('precondition_failed', 'precondition'), + 413: ('request_entity_too_large',), + 414: ('request_uri_too_large',), + 415: ('unspported_media_type', 'unspported_media', 'media_type'), + 416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'), + 417: ('expectation_failed',), + 418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'), + 422: ('unprocessable_entity', 'unprocessable'), + 423: ('locked',), + 424: ('failed_depdendency', 'depdendency'), + 425: ('unordered_collection', 'unordered'), + 426: ('upgrade_required', 'upgrade'), + 444: ('no_response', 'none'), + 449: ('retry_with', 'retry'), + 450: ('blocked_by_windows_parental_controls', 'parental_controls'), + 499: ('client_closed_request',), + 500: ('iternal_server_error', 'server_error'), + 501: ('not_implemented',), + 502: ('bad_gateway',), + 503: ('service_unavailable', 'unavailable'), + 504: ('gateway_timeout',), + 505: ('http_version_not_supported', 'http_version'), + 506: ('variant_also_negotiates',), + 507: ('insufficient_storage',), + 509: ('bandwidth_limit_exceeded', 'bandwidth'), + 510: ('not_extended',), +} + +codes = LookupDict(name='status_codes') + +for (code, titles) in _codes.items(): + for title in titles: + setattr(codes, title, code) + setattr(codes, title.upper(), code) \ No newline at end of file From 47378164ef18323a96b443db4a415057bc3d92ce Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 13 Aug 2011 14:55:41 -0400 Subject: [PATCH 17/83] \o/ --- requests/status_codes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requests/status_codes.py b/requests/status_codes.py index e9a72fcf..299b9037 100644 --- a/requests/status_codes.py +++ b/requests/status_codes.py @@ -8,7 +8,7 @@ _codes = { 102: ('processing',), 103: ('checkpoint',), 122: ('uri_too_long', 'request_uri_too_long'), - 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\o/'), + 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', r'\o/'), 201: ('created',), 202: ('accepted',), 203: ('non_authoritative_info', 'non_authoritative_information'), @@ -71,4 +71,5 @@ codes = LookupDict(name='status_codes') for (code, titles) in _codes.items(): for title in titles: setattr(codes, title, code) - setattr(codes, title.upper(), code) \ No newline at end of file + if not title.startswith(r'\'): + setattr(codes, title.upper(), code) \ No newline at end of file From 56e58fdcafa3ec173d43b41e26b8f8c8b07edc34 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Sun, 14 Aug 2011 09:50:25 +0300 Subject: [PATCH 18/83] Fix a link in the documentation. --- docs/user/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/install.rst b/docs/user/install.rst index cd811ab7..d658db14 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -24,7 +24,7 @@ But, you really `shouldn't do that `_:: +If the Cheeseshop is down, you can also install Requests from Kenneth Reitz's personal `Cheeseshop mirror `_:: $ pip install -i http://pip.kreitz.co/simple requests From 43669183ce3199533680f0552c96cb1aa33dbfea Mon Sep 17 00:00:00 2001 From: Juarez Bochi Date: Sun, 14 Aug 2011 07:30:43 -0300 Subject: [PATCH 19/83] Edited PATCH Requests in README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 96a1f537..312dfc3c 100644 --- a/README.rst +++ b/README.rst @@ -83,7 +83,7 @@ If CookieJar object is is passed in (cookies=...), the cookies will be sent with PATCH Requests - >>> requests.post(url, data={}, headers={}, files={}, cookies=None, auth=None, timeout=None, allow_redirects=False, params{}, proxies={}) + >>> requests.patch(url, data={}, headers={}, files={}, cookies=None, auth=None, timeout=None, allow_redirects=False, params{}, proxies={}) DELETE Requests From 95dcdd8a05ef5dade3dd2234e445ba78d42a1f4e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 14 Aug 2011 09:13:50 -0400 Subject: [PATCH 20/83] Fixing a small typo. --- docs/user/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/install.rst b/docs/user/install.rst index cd811ab7..5012119a 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -32,7 +32,7 @@ If the Cheeseshop is down, you can also install Requests from Kenneth Reitz's pe Get the Code ------------ -Requsts is actively developed on GitHub, where the code is +Requests is actively developed on GitHub, where the code is `always available `_. You can either clone the public repository:: From c4c86fd6e8f7400d0a8d3bf656db201ed7bec767 Mon Sep 17 00:00:00 2001 From: zed Date: Sun, 14 Aug 2011 17:46:43 +0400 Subject: [PATCH 21/83] fix typo --- docs/user/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/install.rst b/docs/user/install.rst index cd811ab7..28f3c2bb 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -15,7 +15,7 @@ Installing requests is simple with `pip `_:: or, with `easy_install `_:: - $ easy_install install requests + $ easy_install requests But, you really `shouldn't do that `_. From ce1945c78025af0ec235d70d8cb501bfe7623145 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 10:43:02 -0400 Subject: [PATCH 22/83] status codes fix --- requests/status_codes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requests/status_codes.py b/requests/status_codes.py index 299b9037..ae7241c9 100644 --- a/requests/status_codes.py +++ b/requests/status_codes.py @@ -8,7 +8,7 @@ _codes = { 102: ('processing',), 103: ('checkpoint',), 122: ('uri_too_long', 'request_uri_too_long'), - 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', r'\o/'), + 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\o/'), 201: ('created',), 202: ('accepted',), 203: ('non_authoritative_info', 'non_authoritative_information'), @@ -71,5 +71,5 @@ codes = LookupDict(name='status_codes') for (code, titles) in _codes.items(): for title in titles: setattr(codes, title, code) - if not title.startswith(r'\'): + if not title.startswith('\\'): setattr(codes, title.upper(), code) \ No newline at end of file From 933cbd735bc57383b812a35aaf1e51c51de59d95 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 11:06:41 -0400 Subject: [PATCH 23/83] emphasize r.content --- docs/index.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 0f6d9ca9..e1628d42 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,9 +21,11 @@ Things shouldn’t be this way. Not in Python. >>> r = requests.get('https://api.github.com', auth=('user', 'pass')) >>> r.status_code - 200 + 204 >>> r.headers['content-type'] 'application/json' + >>> r.content + ... See `the same code, without Requests `_. From 23ff27ea4b285bb55f7e87a62719e52a491141cf Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 11:43:25 -0400 Subject: [PATCH 24/83] Documentation fixes --- requests/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requests/models.py b/requests/models.py index 942bd9b2..ea4d685d 100644 --- a/requests/models.py +++ b/requests/models.py @@ -341,6 +341,7 @@ class Response(object): #: Raw content of the response, in bytes. #: If ``content-encoding`` of response was set to ``gzip``, the #: response data will be automatically deflated. + self.content = None self._content = None #: Integer Code of responded HTTP Status. self.status_code = None @@ -358,7 +359,7 @@ class Response(object): #: the history of the Request. Any redirect responses will end #: up here. self.history = [] - + #: The Request that created the Response. self.request = None From 1361506ae6ab1f25fd0d25147335a4576469d8a8 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 15:44:03 -0400 Subject: [PATCH 25/83] status blocks --- requests/status_codes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/requests/status_codes.py b/requests/status_codes.py index ae7241c9..28ce00b8 100644 --- a/requests/status_codes.py +++ b/requests/status_codes.py @@ -3,6 +3,8 @@ from .structures import LookupDict _codes = { + + # Informational. 100: ('continue',), 101: ('switching_protocols',), 102: ('processing',), @@ -17,6 +19,8 @@ _codes = { 206: ('partial_content', 'partial'), 207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'), 208: ('im_used',), + + # Redirection. 300: ('multiple_choices',), 301: ('moved_pemanently', 'moved'), 302: ('found',), @@ -26,6 +30,8 @@ _codes = { 306: ('switch_proxy',), 307: ('temporary_redirect', 'temporary_moved', 'temporary'), 308: ('resume_incomplete', 'resume'), + + # Client Error. 400: ('bad_request', 'bad'), 401: ('unauthorized',), 402: ('payment_required', 'payment'), @@ -54,6 +60,8 @@ _codes = { 449: ('retry_with', 'retry'), 450: ('blocked_by_windows_parental_controls', 'parental_controls'), 499: ('client_closed_request',), + + # Server Error. 500: ('iternal_server_error', 'server_error'), 501: ('not_implemented',), 502: ('bad_gateway',), From 975720a30c0437a2b9076ca699408cf5b0348dfa Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 15:50:18 -0400 Subject: [PATCH 26/83] I'll have to manually add that to sphinx --- requests/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/requests/models.py b/requests/models.py index ea4d685d..08f3e321 100644 --- a/requests/models.py +++ b/requests/models.py @@ -341,7 +341,6 @@ class Response(object): #: Raw content of the response, in bytes. #: If ``content-encoding`` of response was set to ``gzip``, the #: response data will be automatically deflated. - self.content = None self._content = None #: Integer Code of responded HTTP Status. self.status_code = None From 3bc9a181a16418eb9486a158ec7d07256186ec40 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 21:16:04 -0400 Subject: [PATCH 27/83] basic FAQ --- docs/index.rst | 17 ++++++++++++----- docs/user/faq.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 docs/user/faq.rst diff --git a/docs/index.rst b/docs/index.rst index e1628d42..e2bc2b92 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,8 @@ Requests is an :ref:`ISC Licensed ` HTTP library, written in Python, for hu Most existing Python modules for sending HTTP requests are extremely verbose and cumbersome. Python's builtin **urllib2** module provides most of the HTTP capabilities you should need, but the api is thoroughly **broken**. -It requires an *enormous* amount of work (even method overrides) to perform the simplest of tasks. +It requires an *enormous* amount of work (even method overrides) to perform +the simplest of tasks. Things shouldn’t be this way. Not in Python. @@ -41,28 +42,34 @@ Testimonals `Twitter, Inc `_ uses Requests internally. **Daniel Greenfeld** - Nuked a 1200 LOC spaghetti code library with 10 lines of code thanks to @kennethreitz's request library. Today has been AWESOME. + Nuked a 1200 LOC spaghetti code library with 10 lines of code thanks to + @kennethreitz's request library. Today has been AWESOME. **Kenny Meyers** - Python HTTP: When in doubt, or when not in doubt, use Requests. Beautiful, simple, Pythonic. + Python HTTP: When in doubt, or when not in doubt, use Requests. Beautiful, + simple, Pythonic. **Rich Leland** Requests is awesome. That is all. **Steve Pike** - I can never remember how to do it the regular way. ``import requests; requests.get()`` is just so easy! + I can never remember how to do it the regular way. + ``import requests; requests.get()`` is just so easy! User Guide ---------- -This part of the documentation, which is mostly prose, begins with some background information about Requests, then focuses on step-by-step instructions for getting the most out of Requests. +This part of the documentation, which is mostly prose, begins with some +background information about Requests, then focuses on step-by-step +instructions for getting the most out of Requests. .. toctree:: :maxdepth: 2 user/intro user/install + user/faq .. user/quickstart user/advanced diff --git a/docs/user/faq.rst b/docs/user/faq.rst new file mode 100644 index 00000000..2ccb6a7b --- /dev/null +++ b/docs/user/faq.rst @@ -0,0 +1,43 @@ +.. _faq: + +Frequently Asked Questions +========================== + +This part of the documentation covers common questions about Requests. + +Why not Httplib2? +----------------- + +Chris Adams gave an excellent summary on +`Hacker News `_: + + httplib2 is part of why you should use requests: it's far more respectable + as a client but not as well documented and it still takes way too much code + for basic operations. I appreciate what httplib2 is trying to do, that + there's a ton of hard low-level annoyances in building a modern HTTP + client, but really, just use requests instead. Kenneth Reitz is very + motivated and he gets the degree to which simple things should be simple + whereas httplib2 feels more like an academic exercise than something + people should use to build production systems[1]. + + Disclosure: I'm listed in the requests AUTHORS file but can claim credit + for, oh, about 0.0001% of the awesomeness. + + 1. http://code.google.com/p/httplib2/issues/detail?id=96 is a good example: + an annoying bug which affect many people, there was a fix available for + months, which worked great when I applied it in a fork and pounded a couple + TB of data through it, but it took over a year to make it into trunk and + even longer to make it onto PyPI where any other project which required " + httplib2" would get the working version. + + +Python 3 Support? +----------------- + +It's on the way. + + +Keep-alive Support? +------------------- + +It's on the way. \ No newline at end of file From d50742f059ee40abb1d410c3b0d1115a8ea8d8fb Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 22:07:15 -0400 Subject: [PATCH 28/83] massive documentation update --- docs/{user => community}/faq.rst | 0 docs/community/support.rst | 37 ++++++++++++ docs/community/updates.rst | 31 ++++++++++ docs/dev/authors.rst | 5 ++ docs/index.rst | 17 +++++- docs/user/quickstart.rst | 100 +++++++++++-------------------- 6 files changed, 123 insertions(+), 67 deletions(-) rename docs/{user => community}/faq.rst (100%) create mode 100644 docs/community/support.rst create mode 100644 docs/community/updates.rst create mode 100644 docs/dev/authors.rst diff --git a/docs/user/faq.rst b/docs/community/faq.rst similarity index 100% rename from docs/user/faq.rst rename to docs/community/faq.rst diff --git a/docs/community/support.rst b/docs/community/support.rst new file mode 100644 index 00000000..53f3c819 --- /dev/null +++ b/docs/community/support.rst @@ -0,0 +1,37 @@ +.. _support: + +Support +======= + +If you have a questions or issues about Requests, there are serveral options: + +Send a Tweet +------------ + +If your question is less than 140 characters, feel free to send a tweet to +`@kennethreitz `_. + + +File an Issue +------------- + +If you notice some unexpected behavior in Requests, or want to see support +for a new feature, +`file an issue on GitHub `_. + + +E-mail +------ + +I'm more than happy to answer any personal or in-depth questions about +Requests. Feel free to email +`requests@kennethreitz.com `_. + + +IRC +--- + +The official Freenode channel for Requests is +`#python-requests `_ + +I'm also available as **kennethreitz** on Freenode. \ No newline at end of file diff --git a/docs/community/updates.rst b/docs/community/updates.rst new file mode 100644 index 00000000..942ccac1 --- /dev/null +++ b/docs/community/updates.rst @@ -0,0 +1,31 @@ +.. _updates: + +Updates +======= + +If you'd like to stay up to date on the community and development of Requests, +there are serveral options: + +GitHub +------ + +The best way to track the development of Requests is through +`the GitHub repo `_. + +Twitter +------- + +I often tweet about new features and releases of Requests. + +Follow `@kennethreitz `_ for updates. + + + +Mailing List +------------ + +There's a low-volume mailing list for Requests. To subscribe to the +mailing list, send an email to +`requests@librelist.org `_. + + diff --git a/docs/dev/authors.rst b/docs/dev/authors.rst new file mode 100644 index 00000000..5b3cc85b --- /dev/null +++ b/docs/dev/authors.rst @@ -0,0 +1,5 @@ +Authors +======= + + +.. include:: ../../AUTHORS \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index e2bc2b92..8bef62bb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,11 +69,23 @@ instructions for getting the most out of Requests. user/intro user/install - user/faq -.. user/quickstart + user/quickstart user/advanced +Community Guide +----------------- + +This part of the documentation, which is mostly prose, details the +Requests ecosystem and community. + +.. toctree:: + :maxdepth: 2 + + community/faq + community/support + community/updates + API Documentation ----------------- @@ -97,3 +109,4 @@ you. dev/internals dev/todo + dev/authors diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index fea07662..75ea128a 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -1,84 +1,54 @@ -Feature Overview -================ - -Requests is designed to solve a 90% use case — making simple requests. While most -HTTP libraries are extremely extensible, they often attempt to support the entire HTTP Spec. -This often leads to extremely messy and cumbersome APIs, as is the case with urllib2. Requests abandons support for edge-cases, and focuses on the essentials. - - -.. _features: - -Requests Can: -------------- - -- Make **GET**, **POST**, **PUT**, **DELETE**, and **HEAD** requests. -- Handle HTTP and HTTPS Requests -- Add Request headers (with a simple dictionary) -- URLEncode your Form Data (with a simple dictionary) -- Add Multi-part File Uploads (with a simple dictionary) -- Handle CookieJars (with a single parameter) -- Add HTTP Authentication (with a single parameter) -- Handle redirects (with history) -- Automatically decompress GZip'd responses -- Support Unicode URLs -- Gracefully timeout -- Interface with Eventlet & Gevent - - -Requests Can't: ---------------- - -- Handle Caching -- Handle Keep-Alives - +.. _quickstart: Quickstart ========== +.. module:: requests -GET Request ------------ +Eager to get started? This page gives a good introduction in how to get started with Requests. This assumes you already have Tablib installed. If you do not, head over to the :ref:`Installation ` section. + +First, make sure that: + +* Tablib is :ref:`installed ` +* Tablib is :ref:`up-to-date ` -Adding Parameters ------------------ +Lets gets started with some simple use cases and examples. - -Adding Headers --------------- - - - -HTTP Basic Auth ---------------- - - -Tracking Redirects +Make a GET Request ------------------ +Making a standard request with Requests is very simple. + +Let's get GitHub's public timeline :: + + r = requests.get('https://github.com/timeline.json') + +Now, we have a :class:`Response` object. We can get all the information +we need from this. + + +Response Content +---------------- + +We can read the content of the server's response:: + + >>> r.content + '[{"repository":{"open_issues":0,"url":"https://github.com/... -HTTP POST (Form Data) +Response Status Codes --------------------- +We can check the response status code:: -HTTP POST (Binary Data) ------------------------ + >>> r.status_code + 200 +Requests also comes with a built-in status code lookup object for easy +reference:: -HTTP POST (Multipart Files) ---------------------------- - - -HTTP PUT --------- - - -HTTP DELETE ------------ - - -HTTP HEAD ---------- + >>> r.status_code == requests.codes.ok + True \ No newline at end of file From 414b9dde840f2624b247f42eec2c11e5ee5c6717 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 22:08:01 -0400 Subject: [PATCH 29/83] not tablib.. --- docs/user/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 75ea128a..768468db 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -5,7 +5,7 @@ Quickstart .. module:: requests -Eager to get started? This page gives a good introduction in how to get started with Requests. This assumes you already have Tablib installed. If you do not, head over to the :ref:`Installation ` section. +Eager to get started? This page gives a good introduction in how to get started with Requests. This assumes you already have Requests installed. If you do not, head over to the :ref:`Installation ` section. First, make sure that: From 2cf0dad5738b877fffc2cc3c6013d4152dfc1860 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 22:12:50 -0400 Subject: [PATCH 30/83] models.. --- docs/user/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 768468db..02d66f68 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -3,7 +3,7 @@ Quickstart ========== -.. module:: requests +.. module:: requests.models Eager to get started? This page gives a good introduction in how to get started with Requests. This assumes you already have Requests installed. If you do not, head over to the :ref:`Installation ` section. From f137db6803d4feadf0616dfb74059df2d6586dc1 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 22:20:03 -0400 Subject: [PATCH 31/83] raise_for_status quick start --- docs/user/quickstart.rst | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 02d66f68..06d5f286 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -25,8 +25,7 @@ Let's get GitHub's public timeline :: r = requests.get('https://github.com/timeline.json') -Now, we have a :class:`Response` object. We can get all the information -we need from this. +Now, we have a :class:`Response` object called ``r``. We can get all the information we need from this. Response Content @@ -38,7 +37,6 @@ We can read the content of the server's response:: '[{"repository":{"open_issues":0,"url":"https://github.com/... - Response Status Codes --------------------- @@ -51,4 +49,24 @@ Requests also comes with a built-in status code lookup object for easy reference:: >>> r.status_code == requests.codes.ok - True \ No newline at end of file + True + +If we made a bad request, we can raise it with +:class:`Response.raise_for_status()`:: + + >>> _r = requests.get('http://httpbin.org/status/404') + >>> _r.status_code + 404 + + >>> _r.raise_for_status() + Traceback (most recent call last): + File "requests/models.py", line 394, in raise_for_status + raise self.error + urllib2.HTTPError: HTTP Error 404: NOT FOUND + +But, sice our ``status_code`` was ``200``, when we call it:: + + >>> r.raise_for_status() + None + +All is well. \ No newline at end of file From 2357b13824952b4380c0fd2939eae827595f6ad3 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 22:30:30 -0400 Subject: [PATCH 32/83] Response Headers quickstart --- docs/user/quickstart.rst | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 06d5f286..c28ad1c6 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -69,4 +69,39 @@ But, sice our ``status_code`` was ``200``, when we call it:: >>> r.raise_for_status() None -All is well. \ No newline at end of file +All is well. + + +Response Headers +---------------- + +We can view the server's response headers with a simple Python dictionary +interface:: + + >>> r.headers + { + 'status': '200 OK', + 'content-encoding': 'gzip', + 'transfer-encoding': 'chunked', + 'connection': 'close', + 'server': 'nginx/1.0.4', + 'x-runtime': '148ms', + 'etag': '"e1ca502697e5c9317743dc078f67693f"', + 'content-type': 'application/json; charset=utf-8' + } + +The dictionary is special, though: it's made just for HTTP headers. According to `RFC 2616 `_, HTTP +Headers are case-insensitive. + +So, we can access the headers using any capitalization we want:: + + >>> r.headers['Content-Type'] + 'application/json; charset=utf-8' + + >>> r.headers.get('content-type') + 'application/json; charset=utf-8' + +If a header doesn't exist in the Response, its value defaults to ``None``:: + + >>> r.headers['X-Random'] + None \ No newline at end of file From 49e5df1ff9771810108a346cf8329c196cb099b2 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 14 Aug 2011 22:32:35 -0400 Subject: [PATCH 33/83] whoops --- docs/user/quickstart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index c28ad1c6..4669e810 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -9,8 +9,8 @@ Eager to get started? This page gives a good introduction in how to get started First, make sure that: -* Tablib is :ref:`installed ` -* Tablib is :ref:`up-to-date ` +* Requests is :ref:`installed ` +* Requests is :ref:`up-to-date ` Lets gets started with some simple use cases and examples. From c510d21e4ee8affe66ad0f5c1de32c659bf04fc3 Mon Sep 17 00:00:00 2001 From: moliware Date: Mon, 15 Aug 2011 11:58:57 +0200 Subject: [PATCH 34/83] headers.update is not the same that add headers one by one with add_header --- requests/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requests/models.py b/requests/models.py index 08f3e321..3c0d0517 100644 --- a/requests/models.py +++ b/requests/models.py @@ -300,7 +300,8 @@ class Request(object): req = _Request(url, data=self._enc_data, method=self.method) if self.headers: - req.headers.update(self.headers) + for k,v in self.headers.iteritems(): + req.add_header(k, v) if not self.sent or anyway: From 647cdd38476f6b21887d3ebcd6e0c79a6c97baa1 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Mon, 15 Aug 2011 16:51:33 +0200 Subject: [PATCH 35/83] Use socket.setdefaulttimeout(timeout) only if timeout argument doesn't exist (Python <2.6) - https://github.com/kennethreitz/requests/issues/112 --- AUTHORS | 3 ++- requests/models.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 38528a61..196f0cd2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,4 +31,5 @@ Patches and Suggestions - Tamás Gulácsi - Rubén Abad - Peter Manser -- Jeremy Selie \ No newline at end of file +- Jeremy Selie +- Jens Diemer \ No newline at end of file diff --git a/requests/models.py b/requests/models.py index 08f3e321..5f778691 100644 --- a/requests/models.py +++ b/requests/models.py @@ -38,7 +38,8 @@ class Request(object): params=dict(), auth=None, cookiejar=None, timeout=None, redirect=False, allow_redirects=False, proxies=None): - socket.setdefaulttimeout(timeout) + #socket.setdefaulttimeout(timeout) + self.timeout = timeout #: Request URL. self.url = url @@ -306,7 +307,17 @@ class Request(object): try: opener = self._get_opener() - resp = opener(req) + try: + resp = opener(req, timeout=self.timeout) + except TypeError, err: + # timeout argument is new since Python v2.6 + if not "timeout" in str(err): + raise + # set global socket timeout + old_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(self.timeout) + resp = opener(req) + socket.setdefaulttimeout(old_timeout) if self.cookiejar is not None: self.cookiejar.extract_cookies(resp, req) From 88f79b74ce70815b343f5eeff871f0ee9e8cec67 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Mon, 15 Aug 2011 17:03:37 +0200 Subject: [PATCH 36/83] use settings.timeout_fallback bool for socket.setdefaulttimeout() fallback --- requests/config.py | 1 + requests/models.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/requests/config.py b/requests/config.py index 1ba438fd..89e40c98 100644 --- a/requests/config.py +++ b/requests/config.py @@ -57,3 +57,4 @@ settings = Settings() settings.base_headers = {'User-Agent': 'python-requests.org'} settings.accept_gzip = True settings.proxies = None +settings.timeout_fallback = True # Use socket.setdefaulttimeout() as fallback? diff --git a/requests/models.py b/requests/models.py index 5f778691..845a778e 100644 --- a/requests/models.py +++ b/requests/models.py @@ -38,7 +38,7 @@ class Request(object): params=dict(), auth=None, cookiejar=None, timeout=None, redirect=False, allow_redirects=False, proxies=None): - #socket.setdefaulttimeout(timeout) + #: Float describ the timeout of the request. (Use socket.setdefaulttimeout() as fallback) self.timeout = timeout #: Request URL. @@ -313,11 +313,17 @@ class Request(object): # timeout argument is new since Python v2.6 if not "timeout" in str(err): raise - # set global socket timeout - old_timeout = socket.getdefaulttimeout() - socket.setdefaulttimeout(self.timeout) + + if settings.timeout_fallback: + # fall-back and use global socket timeout (This is not thread-safe!) + old_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(self.timeout) + resp = opener(req) - socket.setdefaulttimeout(old_timeout) + + if settings.timeout_fallback: + # restore gobal timeout + socket.setdefaulttimeout(old_timeout) if self.cookiejar is not None: self.cookiejar.extract_cookies(resp, req) From ca428504d485b0495b5e794cdd1b47bbb0bd890f Mon Sep 17 00:00:00 2001 From: Tom Hogans Date: Mon, 15 Aug 2011 16:01:26 -0400 Subject: [PATCH 37/83] Added request.session functionality --- AUTHORS | 3 ++- requests/session.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 requests/session.py diff --git a/AUTHORS b/AUTHORS index 38528a61..acaa7583 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,4 +31,5 @@ Patches and Suggestions - Tamás Gulácsi - Rubén Abad - Peter Manser -- Jeremy Selie \ No newline at end of file +- Jeremy Selie +- Tom Hogans diff --git a/requests/session.py b/requests/session.py new file mode 100644 index 00000000..72505424 --- /dev/null +++ b/requests/session.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +""" +requests.session +~~~~~~~~~~~~~~~ + +This module provides a Session object to manage and persist settings across +requests (cookies, auth, proxies). + +""" + +import requests.api +import cookielib + +class Session(object): + __attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies'] + + def __init__(self, **kwargs): + # Set up a CookieJar to be used by default + self.cookies = cookielib.FileCookieJar() + # Map args from kwargs to instance-local variables + map(lambda k, v: (k in self.__attrs__) and setattr(self, k, v), + kwargs.iterkeys(), kwargs.itervalues()) + # Map and wrap requests.api methods + self._map_api_methods() + + def _map_api_methods(self): + """ Reads each available method from requests.api and decorates + them with a wrapper that inserts any instance-local attributes + (from __attrs__) that have been set, combining them with **kwargs """ + def pass_args(func): + def wrapper_func(*args, **kwargs): + inst_attrs = dict((k, v) for k, v in self.__dict__.iteritems() + if k in self.__attrs__) + # Combine instance-local values with kwargs values, with + # priority to values in kwargs + kwargs = dict(inst_attrs.items() + kwargs.items()) + return func(*args, **kwargs) + return wrapper_func + # Map and decorate each function available in requests.api + map(lambda fn: setattr(self, fn, pass_args(getattr(requests.api, fn))), + requests.api.__all__) + + From 08a821381aa87803e925eb1e9c61269e4186fa80 Mon Sep 17 00:00:00 2001 From: alopatin Date: Mon, 15 Aug 2011 22:53:22 -0300 Subject: [PATCH 38/83] Fixed misc spelling mistakes. --- requests/status_codes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requests/status_codes.py b/requests/status_codes.py index 28ce00b8..e8023142 100644 --- a/requests/status_codes.py +++ b/requests/status_codes.py @@ -22,7 +22,7 @@ _codes = { # Redirection. 300: ('multiple_choices',), - 301: ('moved_pemanently', 'moved'), + 301: ('moved_permanently', 'moved'), 302: ('found',), 302: ('see_other', 'other'), 304: ('not_modified',), @@ -62,7 +62,7 @@ _codes = { 499: ('client_closed_request',), # Server Error. - 500: ('iternal_server_error', 'server_error'), + 500: ('internal_server_error', 'server_error'), 501: ('not_implemented',), 502: ('bad_gateway',), 503: ('service_unavailable', 'unavailable'), From 8f72c48548ef033b31f944e4ed4ada1e753d21d2 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 15 Aug 2011 22:17:55 -0400 Subject: [PATCH 39/83] added alex to authors --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 196f0cd2..ccb95e24 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,4 +32,5 @@ Patches and Suggestions - Rubén Abad - Peter Manser - Jeremy Selie -- Jens Diemer \ No newline at end of file +- Jens Diemer +- Alex <@alopatin> \ No newline at end of file From c2fd5686950f49128c9d282bf28d440a71225c6b Mon Sep 17 00:00:00 2001 From: Tom Hogans Date: Tue, 16 Aug 2011 01:38:39 -0400 Subject: [PATCH 40/83] Added tests for requests.session --- test_requests.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test_requests.py b/test_requests.py index ff98c412..d6f948ee 100755 --- a/test_requests.py +++ b/test_requests.py @@ -13,6 +13,7 @@ except ImportError: import requests +from requests.session import Session HTTPBIN_URL = 'http://httpbin.org/' @@ -455,5 +456,28 @@ class RequestsTestSuite(unittest.TestCase): self.assertEquals(len(r.history), 3) + def test_session_HTTP_200_OK_GET(self): + s = Session() + r = s.get(httpbin('/')) + self.assertEqual(r.status_code, 200) + + def test_session_HTTPS_200_OK_GET(self): + s = Session() + r = s.get(httpsbin('/')) + self.assertEqual(r.status_code, 200) + + def test_session_persistent_headers(self): + heads = {'User-agent': 'Mozilla/5.0'} + s = Session() + s.headers = heads + # Make 2 requests from Session object, should send header both times + r1 = s.get(httpbin('user-agent') + assert heads['User-agent'] in r1.content + r2 = s.get(httpbin('user-agent') + assert heads['User-agent'] in r2.content + self.assertEqual(r.status_code, 200) + + + if __name__ == '__main__': unittest.main() From 0ed641a26ec2200de00e4bbf3d170c767375351e Mon Sep 17 00:00:00 2001 From: Tom Hogans Date: Tue, 16 Aug 2011 02:00:11 -0400 Subject: [PATCH 41/83] Added docs regarding requests.session to docs/user/advanced.rst --- docs/user/advanced.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index e69de29b..066ebdef 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -0,0 +1,22 @@ +.. _advanced: + +Advanced Usage +============== + +This document covers more advanced features. + +Session Objects +=============== + +.. module:: requests.session + +The Session object allows you to persist certain parameters across requests. It also establishes a CookieJar by default and passes it along in any requests made from the Session instance. For a complete list of allowed parameters, please see the *__attrs__* field in *requests/session.py*. :: + + from requests.session import Session + + s = Session() + s.get("http://httpbin.org/cookies/set/sessioncookie/123456789") + r = s.get("http://httpbin.org/cookies") + print r.content + +Note: Certain parameters are best set at the request.config level (i.e. a global proxy, user agent header). From f319006813c76454d5a0dfccebc6d73f02de88f1 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 22:15:03 -0400 Subject: [PATCH 42/83] session => sessions --- requests/{session.py => sessions.py} | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) rename requests/{session.py => sessions.py} (91%) diff --git a/requests/session.py b/requests/sessions.py similarity index 91% rename from requests/session.py rename to requests/sessions.py index 72505424..9303a825 100644 --- a/requests/session.py +++ b/requests/sessions.py @@ -23,22 +23,25 @@ class Session(object): kwargs.iterkeys(), kwargs.itervalues()) # Map and wrap requests.api methods self._map_api_methods() - + + def __repr__(self): + return '' % (id(self)) + def _map_api_methods(self): """ Reads each available method from requests.api and decorates them with a wrapper that inserts any instance-local attributes (from __attrs__) that have been set, combining them with **kwargs """ def pass_args(func): def wrapper_func(*args, **kwargs): - inst_attrs = dict((k, v) for k, v in self.__dict__.iteritems() + inst_attrs = dict((k, v) for k, v in self.__dict__.iteritems() if k in self.__attrs__) - # Combine instance-local values with kwargs values, with + # Combine instance-local values with kwargs values, with # priority to values in kwargs kwargs = dict(inst_attrs.items() + kwargs.items()) return func(*args, **kwargs) return wrapper_func # Map and decorate each function available in requests.api map(lambda fn: setattr(self, fn, pass_args(getattr(requests.api, fn))), - requests.api.__all__) + requests.api.__all__) From 853383a8d3e58ba30c6682cd7ddc0f2d82f42b18 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 22:16:22 -0400 Subject: [PATCH 43/83] docstring fix --- requests/sessions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requests/sessions.py b/requests/sessions.py index 9303a825..5f62a78f 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -28,9 +28,10 @@ class Session(object): return '' % (id(self)) def _map_api_methods(self): - """ Reads each available method from requests.api and decorates - them with a wrapper that inserts any instance-local attributes - (from __attrs__) that have been set, combining them with **kwargs """ + """Reads each available method from requests.api and decorates + them with a wrapper, which inserts any instance-local attributes + (from __attrs__) that have been set, combining them with **kwargs. + """ def pass_args(func): def wrapper_func(*args, **kwargs): inst_attrs = dict((k, v) for k, v in self.__dict__.iteritems() From 5bb18810d4a9bad0255628dde7d3d2825ca5113a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 22:19:49 -0400 Subject: [PATCH 44/83] memory location repr for requests-client --- requests/sessions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/requests/sessions.py b/requests/sessions.py index 5f62a78f..f8a6883f 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -16,22 +16,26 @@ class Session(object): __attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies'] def __init__(self, **kwargs): + # Set up a CookieJar to be used by default self.cookies = cookielib.FileCookieJar() + # Map args from kwargs to instance-local variables map(lambda k, v: (k in self.__attrs__) and setattr(self, k, v), kwargs.iterkeys(), kwargs.itervalues()) + # Map and wrap requests.api methods self._map_api_methods() def __repr__(self): - return '' % (id(self)) + return '' % (id(self)) def _map_api_methods(self): """Reads each available method from requests.api and decorates them with a wrapper, which inserts any instance-local attributes (from __attrs__) that have been set, combining them with **kwargs. """ + def pass_args(func): def wrapper_func(*args, **kwargs): inst_attrs = dict((k, v) for k, v in self.__dict__.iteritems() @@ -41,6 +45,7 @@ class Session(object): kwargs = dict(inst_attrs.items() + kwargs.items()) return func(*args, **kwargs) return wrapper_func + # Map and decorate each function available in requests.api map(lambda fn: setattr(self, fn, pass_args(getattr(requests.api, fn))), requests.api.__all__) From efba606e574707968ec2b35985b8cbb78213cb71 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 22:20:26 -0400 Subject: [PATCH 45/83] doctoring for Session --- requests/sessions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/requests/sessions.py b/requests/sessions.py index f8a6883f..6681e3cc 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -13,8 +13,11 @@ import requests.api import cookielib class Session(object): + """A Requests session.""" + __attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies'] + def __init__(self, **kwargs): # Set up a CookieJar to be used by default @@ -27,9 +30,11 @@ class Session(object): # Map and wrap requests.api methods self._map_api_methods() + def __repr__(self): return '' % (id(self)) + def _map_api_methods(self): """Reads each available method from requests.api and decorates them with a wrapper, which inserts any instance-local attributes From 519b9cee969e7364654a05ed466db253d35370d3 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 22:55:10 -0400 Subject: [PATCH 46/83] relative imports --- requests/sessions.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/requests/sessions.py b/requests/sessions.py index 6681e3cc..1ce94251 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -9,13 +9,16 @@ requests (cookies, auth, proxies). """ -import requests.api import cookielib +from . import api + + + class Session(object): """A Requests session.""" - __attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies'] + __attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks'] def __init__(self, **kwargs): @@ -34,6 +37,13 @@ class Session(object): def __repr__(self): return '' % (id(self)) + def __enter__(self): + return self + + def __exit__(self, *args): + # print args + pass + def _map_api_methods(self): """Reads each available method from requests.api and decorates @@ -52,7 +62,11 @@ class Session(object): return wrapper_func # Map and decorate each function available in requests.api - map(lambda fn: setattr(self, fn, pass_args(getattr(requests.api, fn))), - requests.api.__all__) + map(lambda fn: setattr(self, fn, pass_args(getattr(api, fn))), + api.__all__) +def session(**kwargs): + """Returns a :class:`Session` for context-managment.""" + + return Session(**kwargs) \ No newline at end of file From fc04d8a368b373945f2b7d701a683d1167ff4101 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 22:55:18 -0400 Subject: [PATCH 47/83] cleanups --- requests/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/requests/config.py b/requests/config.py index 89e40c98..ce464484 100644 --- a/requests/config.py +++ b/requests/config.py @@ -53,8 +53,12 @@ class Settings(object): return None return object.__getattribute__(self, key) + settings = Settings() + settings.base_headers = {'User-Agent': 'python-requests.org'} settings.accept_gzip = True settings.proxies = None -settings.timeout_fallback = True # Use socket.setdefaulttimeout() as fallback? + +#: Use socket.setdefaulttimeout() as fallback? +settings.timeout_fallback = True From d0cb56932c251667420e4876f99f5fa6b5c8a9a6 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 23:57:44 -0400 Subject: [PATCH 48/83] new hooks system #118 --- requests/api.py | 13 +++++++++---- requests/hooks.py | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 requests/hooks.py diff --git a/requests/api.py b/requests/api.py index 1a2ce300..a1a39387 100644 --- a/requests/api.py +++ b/requests/api.py @@ -12,15 +12,16 @@ This module impliments the Requests API. """ import config -from .models import Request, Response, AuthManager, AuthObject, auth_manager +from .models import Request, Response, AuthObject from .status_codes import codes +from .hooks import dispatch_hook __all__ = ('request', 'get', 'head', 'post', 'patch', 'put', 'delete') def request(method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, - timeout=None, allow_redirects=False, proxies=None): + timeout=None, allow_redirects=False, proxies=None, hooks=None): """Constructs and sends a :class:`Request `. Returns :class:`Response ` object. @@ -37,7 +38,7 @@ def request(method, url, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ - r = Request( + args = dict( method = method, url = url, data = data, @@ -45,12 +46,16 @@ def request(method, url, headers = headers, cookiejar = cookies, files = files, - auth = auth or auth_manager.get_auth(url), + auth = auth, timeout = timeout or config.settings.timeout, allow_redirects = allow_redirects, proxies = proxies or config.settings.proxies ) + args = dispatch_hook('args', hooks, args) + + r = Request(**args) + r.send() return r.response diff --git a/requests/hooks.py b/requests/hooks.py new file mode 100644 index 00000000..7e9f23a4 --- /dev/null +++ b/requests/hooks.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +requests.hooks +~~~~~~~~~~~~~~ + +This module provides the capabilities for the Requests hooks system. +""" + +import warnings + +def dispatch_hook(key, hooks, hook_data): + """""" + + hooks = hooks or dict() + + if key in hooks: + try: + return hooks.get(key).__call__(hook_data) or hook_data + + except Exception, why: + warnings.warn(str(why)) + + + return hook_data From 86a06c5c184272ffd8e6fd4a33b427400713f739 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 23:58:07 -0400 Subject: [PATCH 49/83] auth_manager internal --- requests/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests/core.py b/requests/core.py index 7b5f4b20..e02a75ed 100644 --- a/requests/core.py +++ b/requests/core.py @@ -19,7 +19,7 @@ __license__ = 'ISC' __copyright__ = 'Copyright 2011 Kenneth Reitz' -from models import HTTPError, auth_manager +from models import HTTPError from api import * from exceptions import * from status_codes import codes From 9322d13f610fc26f63cc51c8c4b0a907d7392153 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Aug 2011 23:58:55 -0400 Subject: [PATCH 50/83] cleanups --- test_requests.py | 44 ++++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/test_requests.py b/test_requests.py index d6f948ee..dd923471 100755 --- a/test_requests.py +++ b/test_requests.py @@ -13,7 +13,7 @@ except ImportError: import requests -from requests.session import Session +from requests.sessions import Session HTTPBIN_URL = 'http://httpbin.org/' @@ -132,22 +132,21 @@ class RequestsTestSuite(unittest.TestCase): self.assertEqual(r.status_code, 200) - def test_AUTH_HTTPS_200_OK_GET(self): + def test_AUTH_HTTP_200_OK_GET(self): for service in SERVICES: auth = ('user', 'pass') url = service('basic-auth', 'user', 'pass') - r = requests.get(url, auth=auth) + r = requests.get(url, auth=auth) + # print r.__dict__ self.assertEqual(r.status_code, 200) + r = requests.get(url) self.assertEqual(r.status_code, 200) - # reset auto authentication - requests.auth_manager.empty() - def test_POSTBIN_GET_POST_FILES(self): @@ -245,21 +244,6 @@ class RequestsTestSuite(unittest.TestCase): r.content.decode('ascii') - def test_autoauth(self): - - http_auth = ('user', 'pass') - requests.auth_manager.add_auth('httpbin.org', http_auth) - - r = requests.get(httpbin('basic-auth', 'user', 'pass')) - self.assertEquals(r.status_code, 200) - - - requests.auth_manager.add_auth('httpbin.ep.io', http_auth) - - r = requests.get(httpsbin('basic-auth', 'user', 'pass')) - self.assertEquals(r.status_code, 200) - - def test_unicode_get(self): for service in SERVICES: @@ -457,27 +441,35 @@ class RequestsTestSuite(unittest.TestCase): def test_session_HTTP_200_OK_GET(self): + s = Session() r = s.get(httpbin('/')) self.assertEqual(r.status_code, 200) + def test_session_HTTPS_200_OK_GET(self): + s = Session() r = s.get(httpsbin('/')) self.assertEqual(r.status_code, 200) + def test_session_persistent_headers(self): + heads = {'User-agent': 'Mozilla/5.0'} + s = Session() s.headers = heads # Make 2 requests from Session object, should send header both times - r1 = s.get(httpbin('user-agent') + r1 = s.get(httpbin('user-agent')) + assert heads['User-agent'] in r1.content - r2 = s.get(httpbin('user-agent') + r2 = s.get(httpbin('user-agent')) + assert heads['User-agent'] in r2.content - self.assertEqual(r.status_code, 200) - - + self.assertEqual(r2.status_code, 200) + + if __name__ == '__main__': unittest.main() From 6b11cf21f3d703315f4ae82ee493ee52a57d0b30 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:00:47 -0400 Subject: [PATCH 51/83] todo --- requests/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requests/models.py b/requests/models.py index a2775550..693aaa44 100644 --- a/requests/models.py +++ b/requests/models.py @@ -128,6 +128,7 @@ class Request(object): if self.auth: if not isinstance(self.auth.handler, (urllib2.AbstractBasicAuthHandler, urllib2.AbstractDigestAuthHandler)): + # TODO: REMOVE THIS COMPLETELY 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) @@ -314,14 +315,14 @@ class Request(object): # timeout argument is new since Python v2.6 if not "timeout" in str(err): raise - + if settings.timeout_fallback: # fall-back and use global socket timeout (This is not thread-safe!) old_timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(self.timeout) - + resp = opener(req) - + if settings.timeout_fallback: # restore gobal timeout socket.setdefaulttimeout(old_timeout) From 7322aa5b6442af61a7c0d96c33c7e253215b6f95 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:02:18 -0400 Subject: [PATCH 52/83] newlines --- requests/api.py | 3 ++- requests/models.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requests/api.py b/requests/api.py index a1a39387..d81a56d7 100644 --- a/requests/api.py +++ b/requests/api.py @@ -23,7 +23,8 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=False, proxies=None, hooks=None): - """Constructs and sends a :class:`Request `. Returns :class:`Response ` object. + """Constructs and sends a :class:`Request `. + Returns :class:`Response ` object. :param method: method for the new :class:`Request` object. :param url: URL for the new :class:`Request` object. diff --git a/requests/models.py b/requests/models.py index 693aaa44..3c757ae2 100644 --- a/requests/models.py +++ b/requests/models.py @@ -38,7 +38,8 @@ class Request(object): params=dict(), auth=None, cookiejar=None, timeout=None, redirect=False, allow_redirects=False, proxies=None): - #: Float describ the timeout of the request. (Use socket.setdefaulttimeout() as fallback) + #: Float describ the timeout of the request. + # (Use socket.setdefaulttimeout() as fallback) self.timeout = timeout #: Request URL. From 8d2ab68a9716670fb44e6be729aa9d72fa433876 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:08:06 -0400 Subject: [PATCH 53/83] space --- requests/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests/models.py b/requests/models.py index 3c757ae2..2a699259 100644 --- a/requests/models.py +++ b/requests/models.py @@ -39,7 +39,7 @@ class Request(object): allow_redirects=False, proxies=None): #: Float describ the timeout of the request. - # (Use socket.setdefaulttimeout() as fallback) + # (Use socket.setdefaulttimeout() as fallback) self.timeout = timeout #: Request URL. From 17d3e1d498cd8afade26a31ec313739a71550eed Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:08:39 -0400 Subject: [PATCH 54/83] whitespace --- requests/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/requests/models.py b/requests/models.py index 2a699259..90b356f9 100644 --- a/requests/models.py +++ b/requests/models.py @@ -44,23 +44,31 @@ class Request(object): #: Request URL. self.url = url + #: Dictonary of HTTP Headers to attach to the :class:`Request `. self.headers = headers + #: Dictionary of files to multipart upload (``{filename: content}``). self.files = files + #: HTTP Method to use. Available: GET, HEAD, PUT, POST, DELETE. self.method = method + #: Dictionary or byte of request body data to attach to the #: :class:`Request `. self.data = None + #: Dictionary or byte of querystring data to attach to the #: :class:`Request `. self.params = None + #: True if :class:`Request ` is part of a redirect chain (disables history #: and HTTPError storage). self.redirect = redirect + #: Set to True if full redirects are allowed (e.g. re-POST-ing of data at new ``Location``) self.allow_redirects = allow_redirects + # Dictionary mapping protocol to the URL of the proxy (e.g. {'http': 'foo.bar:3128'}) self.proxies = proxies @@ -75,10 +83,13 @@ class Request(object): auth = AuthObject(*auth) if not auth: auth = auth_manager.get_auth(self.url) + #: :class:`AuthObject` to attach to :class:`Request `. self.auth = auth + #: CookieJar to attach to :class:`Request `. self.cookiejar = cookiejar + #: True if Request has been sent. self.sent = False From 24da41005c5d36922516d22aa9d596ec88a1a8d8 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:09:38 -0400 Subject: [PATCH 55/83] hmmm --- requests/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requests/models.py b/requests/models.py index 90b356f9..a48e67ea 100644 --- a/requests/models.py +++ b/requests/models.py @@ -260,7 +260,7 @@ class Request(object): def _build_url(self): - """Build the actual URL to use""" + """Build the actual URL to use.""" # Support for unicode domain names. parsed_url = list(urlparse(self.url)) @@ -286,6 +286,7 @@ class Request(object): :param anyway: If True, request will be sent, even if it has already been sent. """ + self._checks() success = False @@ -495,8 +496,10 @@ class AuthManager(object): 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] From 783e540d6dc63c3fdebdcc94d163a05a54127dd6 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:09:47 -0400 Subject: [PATCH 56/83] sigh --- requests/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requests/models.py b/requests/models.py index a48e67ea..d32193fd 100644 --- a/requests/models.py +++ b/requests/models.py @@ -510,7 +510,9 @@ class AuthManager(object): 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, From 5ba43d0fe5ff5b1705e94ae1b7f2abfe744c56d4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:16:57 -0400 Subject: [PATCH 57/83] hook plans --- requests/hooks.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/requests/hooks.py b/requests/hooks.py index 7e9f23a4..272dc07d 100644 --- a/requests/hooks.py +++ b/requests/hooks.py @@ -5,6 +5,21 @@ requests.hooks ~~~~~~~~~~~~~~ This module provides the capabilities for the Requests hooks system. + +Available hooks: + +``args``: + A dictionary of the arguments being sent to Request(). + +``pre-request``: + The Request object, directly before being sent. + +``post-request``: + The Request object, directly after being sent. + +``response``: + The response generated from a Request. + """ import warnings From e102c1b4ca68c19920dfb835f89719985f532b2c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:17:58 -0400 Subject: [PATCH 58/83] RTD doesn't work w/ this --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 899bc588..8987bb8c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -155,7 +155,7 @@ html_sidebars = { #html_split_index = False # If true, links to the reST sources are added to the pages. -html_show_sourcelink = True +html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False From 8ef0e88220dd5c87e6489a174ce4d87654a6b2dc Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:27:41 -0400 Subject: [PATCH 59/83] attach hooks to Request --- requests/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requests/api.py b/requests/api.py index d81a56d7..e6ff715c 100644 --- a/requests/api.py +++ b/requests/api.py @@ -50,12 +50,12 @@ def request(method, url, auth = auth, timeout = timeout or config.settings.timeout, allow_redirects = allow_redirects, - proxies = proxies or config.settings.proxies + proxies = proxies or config.settings.proxies, ) args = dispatch_hook('args', hooks, args) - r = Request(**args) + r = Request(hooks=hooks, **args) r.send() From 3e8a14e133d8c2dc35a76e0a2a2ae3151c171c26 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:27:53 -0400 Subject: [PATCH 60/83] pre_request hook --- requests/hooks.py | 4 ++-- requests/models.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/requests/hooks.py b/requests/hooks.py index 272dc07d..c5a24c24 100644 --- a/requests/hooks.py +++ b/requests/hooks.py @@ -11,10 +11,10 @@ Available hooks: ``args``: A dictionary of the arguments being sent to Request(). -``pre-request``: +``pre_request``: The Request object, directly before being sent. -``post-request``: +``post_request``: The Request object, directly after being sent. ``response``: diff --git a/requests/models.py b/requests/models.py index d32193fd..daf48ea8 100644 --- a/requests/models.py +++ b/requests/models.py @@ -16,6 +16,7 @@ from urlparse import urlparse, urlunparse, urljoin from datetime import datetime from .config import settings +from .hooks import dispatch_hook from .monkeys import Request as _Request, HTTPBasicAuthHandler, HTTPForcedBasicAuthHandler, HTTPDigestAuthHandler, HTTPRedirectHandler from .structures import CaseInsensitiveDict from .packages.poster.encode import multipart_encode @@ -36,7 +37,7 @@ class Request(object): def __init__(self, url=None, headers=dict(), files=None, method=None, data=dict(), params=dict(), auth=None, cookiejar=None, timeout=None, redirect=False, - allow_redirects=False, proxies=None): + allow_redirects=False, proxies=None, hooks=None): #: Float describ the timeout of the request. # (Use socket.setdefaulttimeout() as fallback) @@ -93,6 +94,9 @@ class Request(object): #: True if Request has been sent. self.sent = False + #: Dictionary of event hook callbacks. + self.hooks = hooks + # Header manipulation and defaults. @@ -323,10 +327,18 @@ class Request(object): try: opener = self._get_opener() try: + + # pre-request hook. + self.__dict__.update( + dispatch_hook('pre_request', + self.hooks, self.__dict__) + ) + resp = opener(req, timeout=self.timeout) + except TypeError, err: # timeout argument is new since Python v2.6 - if not "timeout" in str(err): + if not 'timeout' in str(err): raise if settings.timeout_fallback: From 2a0eff9c7130371874a07ec74806cc703ebad2cc Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:33:21 -0400 Subject: [PATCH 61/83] docstrings --- requests/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests/hooks.py b/requests/hooks.py index c5a24c24..59c82696 100644 --- a/requests/hooks.py +++ b/requests/hooks.py @@ -25,7 +25,7 @@ Available hooks: import warnings def dispatch_hook(key, hooks, hook_data): - """""" + """Dipatches a hook dictionary on a given peice of data.""" hooks = hooks or dict() From 7836536459b6a15796c004440d3f927281808455 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:33:28 -0400 Subject: [PATCH 62/83] post_request hook --- requests/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requests/models.py b/requests/models.py index daf48ea8..f9670a4c 100644 --- a/requests/models.py +++ b/requests/models.py @@ -370,6 +370,9 @@ class Request(object): self.sent = self.response.ok + self.__dict__.update( + dispatch_hook('post_request', self.hooks, self.__dict__)) + return self.sent From 281ab5a8f9403135ffd05ff40f177529decd749e Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:40:28 -0400 Subject: [PATCH 63/83] move hooks into api layer only --- requests/api.py | 12 ++++++++++++ requests/models.py | 10 ---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/requests/api.py b/requests/api.py index e6ff715c..c5a81c37 100644 --- a/requests/api.py +++ b/requests/api.py @@ -53,12 +53,24 @@ def request(method, url, proxies = proxies or config.settings.proxies, ) + # Arguments manipulation hook. args = dispatch_hook('args', hooks, args) + r = Request(hooks=hooks, **args) + # Pre-request hook. + r = dispatch_hook('pre_request', hooks, r) + + # Send the HTTP Request. r.send() + # Post-request hook. + r = dispatch_hook('post_request', hooks, r) + + # Response manipulation hook. + r.response = dispatch_hook('response', hooks, r.response) + return r.response diff --git a/requests/models.py b/requests/models.py index f9670a4c..c802f030 100644 --- a/requests/models.py +++ b/requests/models.py @@ -16,7 +16,6 @@ from urlparse import urlparse, urlunparse, urljoin from datetime import datetime from .config import settings -from .hooks import dispatch_hook from .monkeys import Request as _Request, HTTPBasicAuthHandler, HTTPForcedBasicAuthHandler, HTTPDigestAuthHandler, HTTPRedirectHandler from .structures import CaseInsensitiveDict from .packages.poster.encode import multipart_encode @@ -328,12 +327,6 @@ class Request(object): opener = self._get_opener() try: - # pre-request hook. - self.__dict__.update( - dispatch_hook('pre_request', - self.hooks, self.__dict__) - ) - resp = opener(req, timeout=self.timeout) except TypeError, err: @@ -370,9 +363,6 @@ class Request(object): self.sent = self.response.ok - self.__dict__.update( - dispatch_hook('post_request', self.hooks, self.__dict__)) - return self.sent From 855c27370790c0ba781d20f776a907fa5e2995ae Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:40:37 -0400 Subject: [PATCH 64/83] whitespace --- requests/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests/hooks.py b/requests/hooks.py index 59c82696..2938029b 100644 --- a/requests/hooks.py +++ b/requests/hooks.py @@ -24,6 +24,7 @@ Available hooks: import warnings + def dispatch_hook(key, hooks, hook_data): """Dipatches a hook dictionary on a given peice of data.""" @@ -36,5 +37,4 @@ def dispatch_hook(key, hooks, hook_data): except Exception, why: warnings.warn(str(why)) - return hook_data From 51038497f6551e132bee06c1220daa69a7b7a71d Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 00:40:49 -0400 Subject: [PATCH 65/83] better --- requests/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/requests/api.py b/requests/api.py index c5a81c37..a433b0ed 100644 --- a/requests/api.py +++ b/requests/api.py @@ -56,7 +56,6 @@ def request(method, url, # Arguments manipulation hook. args = dispatch_hook('args', hooks, args) - r = Request(hooks=hooks, **args) # Pre-request hook. From b0a0b509da5df358b1ab2f66f0aa6aebad27bd16 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 01:00:00 -0400 Subject: [PATCH 66/83] session at root package level --- requests/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/requests/core.py b/requests/core.py index e02a75ed..27384397 100644 --- a/requests/core.py +++ b/requests/core.py @@ -22,5 +22,6 @@ __copyright__ = 'Copyright 2011 Kenneth Reitz' from models import HTTPError from api import * from exceptions import * +from sessions import session from status_codes import codes from config import settings \ No newline at end of file From 7e255177dc206ec22118ef98b3b857c598073bd9 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 01:23:49 -0400 Subject: [PATCH 67/83] get cookies from response #116 --- requests/models.py | 8 ++++++++ requests/utils.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 requests/utils.py diff --git a/requests/models.py b/requests/models.py index c802f030..5e54f377 100644 --- a/requests/models.py +++ b/requests/models.py @@ -20,6 +20,7 @@ from .monkeys import Request as _Request, HTTPBasicAuthHandler, HTTPForcedBasicA from .structures import CaseInsensitiveDict from .packages.poster.encode import multipart_encode from .packages.poster.streaminghttp import register_openers, get_handlers +from .utils import dict_from_cookiejar from .exceptions import RequestException, AuthenticationError, Timeout, URLRequired, InvalidMethod @@ -187,6 +188,12 @@ class Request(object): response.headers = CaseInsensitiveDict(getattr(resp.info(), 'dict', None)) response.read = resp.read response.close = resp.close + + if self.cookiejar: + + response.cookies = dict_from_cookiejar(self.cookiejar) + + except AttributeError: pass @@ -397,6 +404,7 @@ class Response(object): self.history = [] #: The Request that created the Response. self.request = None + self.cookies = None def __repr__(self): diff --git a/requests/utils.py b/requests/utils.py new file mode 100644 index 00000000..f308f668 --- /dev/null +++ b/requests/utils.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +""" +requests.utils +~~~~~~~~~~~~~~ + +This module provides utlity functions that are used within Requests +that are also useful for external consumption. + +""" + + +def dict_from_cookiejar(cookiejar): + """Returns a key/value dictoinary from a CookieJar.""" + + cookie_dict = {} + + for _, cookies in cookiejar._cookies.items(): + for _, cookies in cookies.items(): + for cookie in cookies.values(): + cookie_dict[cookie.name] = cookie.value + + return cookie_dict + From d9e571737716e2929386f5279ce80c130d772a5b Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 01:29:58 -0400 Subject: [PATCH 68/83] cookiejar_from_dict #12 --- requests/utils.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/requests/utils.py b/requests/utils.py index f308f668..0719f810 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -9,9 +9,12 @@ that are also useful for external consumption. """ +import Cookie +import cookielib + def dict_from_cookiejar(cookiejar): - """Returns a key/value dictoinary from a CookieJar.""" + """Returns a key/value dictionary from a CookieJar.""" cookie_dict = {} @@ -22,3 +25,24 @@ def dict_from_cookiejar(cookiejar): return cookie_dict + +def cookiejar_from_dict(cookie_dict, domain=None): + """Returns a CookieJar from a key/value dictoinary.""" + + # create cookiejar + cj = cookielib.CookieJar() + + for k, v in cookie_dict.items(): + + # create cookie + ck = Cookie.SimpleCookie() + ck.name = v + ck.expires = 0 + ck.path = '/' + ck.domain = domain + + # add cookie to cookiejar + cj.set_cookie(ck) + + return cj + From d0aa0175e6b32bdb5b4f6b87c07d02c0088fcdea Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 01:32:04 -0400 Subject: [PATCH 69/83] v0.6.0 --- requests/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requests/core.py b/requests/core.py index 27384397..68e53923 100644 --- a/requests/core.py +++ b/requests/core.py @@ -12,8 +12,8 @@ This module implements the main Requests system. """ __title__ = 'requests' -__version__ = '0.5.1' -__build__ = 0x000501 +__version__ = '0.6.0' +__build__ = 0x000600 __author__ = 'Kenneth Reitz' __license__ = 'ISC' __copyright__ = 'Copyright 2011 Kenneth Reitz' From ec33ce9d29d591e18ded52018501942d3eb02776 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 01:32:12 -0400 Subject: [PATCH 70/83] sp. --- requests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests/utils.py b/requests/utils.py index 0719f810..716986b8 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -27,7 +27,7 @@ def dict_from_cookiejar(cookiejar): def cookiejar_from_dict(cookie_dict, domain=None): - """Returns a CookieJar from a key/value dictoinary.""" + """Returns a CookieJar from a key/value dictionary.""" # create cookiejar cj = cookielib.CookieJar() From f7024a60004db98e467260402e6c2f5338e9d619 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 02:01:13 -0400 Subject: [PATCH 71/83] super cookiejar_from_dict powwerrrs --- requests/utils.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/requests/utils.py b/requests/utils.py index 716986b8..e4236b0f 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -9,7 +9,6 @@ that are also useful for external consumption. """ -import Cookie import cookielib @@ -21,28 +20,45 @@ def dict_from_cookiejar(cookiejar): for _, cookies in cookiejar._cookies.items(): for _, cookies in cookies.items(): for cookie in cookies.values(): + # print cookie cookie_dict[cookie.name] = cookie.value return cookie_dict -def cookiejar_from_dict(cookie_dict, domain=None): +def cookiejar_from_dict(cookie_dict): """Returns a CookieJar from a key/value dictionary.""" + # return cookiejar if one was passed in + if isinstance(cookie_dict, cookielib.CookieJar): + return cookie_dict + # create cookiejar cj = cookielib.CookieJar() for k, v in cookie_dict.items(): - # create cookie - ck = Cookie.SimpleCookie() - ck.name = v - ck.expires = 0 - ck.path = '/' - ck.domain = domain + cookie = cookielib.Cookie( + version=0, + name=k, + value=v, + port=None, + port_specified=False, + domain='', + domain_specified=False, + domain_initial_dot=False, + path='/', + path_specified=True, + secure=False, + expires=None, + discard=True, + comment=None, + comment_url=None, + rest={'HttpOnly': None}, + rfc2109=False + ) # add cookie to cookiejar - cj.set_cookie(ck) + cj.set_cookie(cookie) return cj - From 66391e5c9da77889ebaf1c8a1cd3f84543d6943b Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 02:01:19 -0400 Subject: [PATCH 72/83] ACTIVVVAATTTEEEEEEE --- requests/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/requests/api.py b/requests/api.py index a433b0ed..9c923e50 100644 --- a/requests/api.py +++ b/requests/api.py @@ -15,7 +15,9 @@ import config from .models import Request, Response, AuthObject from .status_codes import codes from .hooks import dispatch_hook +from .utils import cookiejar_from_dict +from urlparse import urlparse __all__ = ('request', 'get', 'head', 'post', 'patch', 'put', 'delete') @@ -39,6 +41,11 @@ def request(method, url, :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. """ + if cookies is None: + cookies = {} + + cookies = cookiejar_from_dict(cookies) + args = dict( method = method, url = url, From 467766cfe8b2f4f13f5bee1346b9ab7a8cbfcb62 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 02:25:41 -0400 Subject: [PATCH 73/83] cookie session persistence --- requests/sessions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/requests/sessions.py b/requests/sessions.py index 1ce94251..540089cc 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -12,6 +12,7 @@ requests (cookies, auth, proxies). import cookielib from . import api +from .utils import add_dict_to_cookiejar @@ -58,6 +59,14 @@ class Session(object): # Combine instance-local values with kwargs values, with # priority to values in kwargs kwargs = dict(inst_attrs.items() + kwargs.items()) + + # If a session request has a cookie_dict, inject the + # values into the existing CookieJar instead. + if isinstance(kwargs.get('cookies', None), dict): + kwargs['cookies'] = add_dict_to_cookiejar( + inst_attrs['cookies'], kwargs['cookies'] + ) + return func(*args, **kwargs) return wrapper_func From e477fce2112dc7078363c34798facbde7be4f7c4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 02:25:56 -0400 Subject: [PATCH 74/83] add_dict_to_cookiejar Signed-off-by: Kenneth Reitz --- requests/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/requests/utils.py b/requests/utils.py index e4236b0f..8ac78b4e 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -36,6 +36,14 @@ def cookiejar_from_dict(cookie_dict): # create cookiejar cj = cookielib.CookieJar() + cj = add_dict_to_cookiejar(cj, cookie_dict) + + return cj + + +def add_dict_to_cookiejar(cj, cookie_dict): + """Returns a CookieJar from a key/value dictionary.""" + for k, v in cookie_dict.items(): cookie = cookielib.Cookie( From abcf81e2a8cd5497080fad7fb4ac6b6a87eacd6c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 02:49:13 -0400 Subject: [PATCH 75/83] docs update --- docs/user/advanced.rst | 46 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 066ebdef..97687b2e 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -3,20 +3,46 @@ Advanced Usage ============== -This document covers more advanced features. +This document covers some of Requests more advanced features. + Session Objects -=============== +--------------- -.. module:: requests.session +The Session object allows you to persist certain parameters across +requests. It also establishes a CookieJar and passes it along +to any requests made from the Session instance. -The Session object allows you to persist certain parameters across requests. It also establishes a CookieJar by default and passes it along in any requests made from the Session instance. For a complete list of allowed parameters, please see the *__attrs__* field in *requests/session.py*. :: +A session object has all the methods of the main Requests API. - from requests.session import Session +Let's persist some cookies across requests:: - s = Session() - s.get("http://httpbin.org/cookies/set/sessioncookie/123456789") - r = s.get("http://httpbin.org/cookies") - print r.content + with requests.session() as s: -Note: Certain parameters are best set at the request.config level (i.e. a global proxy, user agent header). + s.get('http://httpbin.org/cookies/set/sessioncookie/123456789') + r = s.get("http://httpbin.org/cookies") + + print r.content + + +Sessions can also be used to provide default data to the request methods:: + + headers = {'x-test': 'true'} + auth = ('user', 'pass') + + with requests.session(auth=auth, headers=headers) as c: + + # both 'x-test' and 'x-test2' are sent + c.get('http://httpbin.org/headers', header={'x-test2', 'true'}) + + +.. admonition:: Global Settings + + Certain parameters are best set at the ``request.config`` level + (e.g.. a global proxy, user agent header). + + +Event Hooks +----------- + +Requests has a hook system that allows you . This is useful for \ No newline at end of file From b7307298d28a30ed893d095338a36f7f62a7f1b9 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 02:49:29 -0400 Subject: [PATCH 76/83] merge headers, don't overwrite --- requests/sessions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requests/sessions.py b/requests/sessions.py index 540089cc..50b09f61 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -67,6 +67,9 @@ class Session(object): inst_attrs['cookies'], kwargs['cookies'] ) + if kwargs.get('headers', None) and inst_attrs.get('headers', None): + kwargs['headers'].update(inst_attrs['headers']) + return func(*args, **kwargs) return wrapper_func From 77b37687b69ef21262e04c60401e0bcf712f0357 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 03:07:24 -0400 Subject: [PATCH 77/83] advanced docs --- docs/user/advanced.rst | 58 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 97687b2e..430a4720 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -45,4 +45,60 @@ Sessions can also be used to provide default data to the request methods:: Event Hooks ----------- -Requests has a hook system that allows you . This is useful for \ No newline at end of file +Requests has a hook system that you can use to manipulate portions of +the request process, or signal event handling. + +Available hooks: + +``args``: + A dictionary of the arguments being sent to Request(). + +``pre_request``: + The Request object, directly before being sent. + +``post_request``: + The Request object, directly after being sent. + +``response``: + The response generated from a Request. + + +You can assign a hook function on a per-request basis by passing a +``{hook_name: callback_function}`` dictionary to the ``hooks`` request +paramaeter:: + + hooks=dict(args=print_url) + +That ``callback_function`` will receive a chunk of data as its first +argument. + +:: + + def print_url(args): + print args['url'] + +If an error occurs while executing your callback, a warning is given. + +If the callback function returns a value, it is assumed that it is to +replace the data that was passed in. If the function doesn't return +anything, nothing else is effected. + +Let's print some request method arguments at runtime:: + + >>> requests.get('http://httpbin', hooks=dict(args=print_url)) + http://httpbin + + + +Verbose Logging +--------------- + +If you want to get a good look at what HTTP requests are being sent +by your application, you can turn on verbose logging. + +To do so, just configure Requests with a stream to write to:: + + >>> requests.settings.verbose = sys.stderr + >>> requests.get('http://httpbin.org/headers') + 2011-08-17T03:04:23.380175 GET http://httpbin.org/headers + \ No newline at end of file From 008c77c911f504cd72f7e097f1744daaadab2162 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 03:19:46 -0400 Subject: [PATCH 78/83] hijack arguments --- docs/user/advanced.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 430a4720..499c354d 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -89,6 +89,37 @@ Let's print some request method arguments at runtime:: http://httpbin +Let's hijack some arguments this time:: + + def hack_headers(args): + if not args[headers]: + args['headers'] = dict() + + args['headers'].update({'X-Testing': 'True'}) + + + return args + + hooks = dict(args=hack_headers) + headers = dict(yo=dawg) + + >>> requests.get('http://httpbin/headers', hooks=hooks, headers=headers) + { + "headers": { + "Content-Length": "", + "Accept-Encoding": "gzip", + "Yo": "dawg", + "X-Forwarded-For": "::ffff:24.127.96.129", + "Connection": "close", + "User-Agent": "python-requests.org", + "Host": "httpbin.org", + "X-Testing": "True", + "X-Forwarded-Protocol": "", + "Content-Type": "" + } + } + + Verbose Logging --------------- From 3f8f1dc00c11c501c9596a8d8008e40d4d99179f Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 03:19:51 -0400 Subject: [PATCH 79/83] cleaner defaults --- requests/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requests/config.py b/requests/config.py index ce464484..a42f15d2 100644 --- a/requests/config.py +++ b/requests/config.py @@ -12,7 +12,7 @@ class Settings(object): _singleton = {} # attributes with defaults - __attrs__ = ('timeout', 'verbose') + __attrs__ = [] def __init__(self, **kwargs): super(Settings, self).__init__() @@ -59,6 +59,8 @@ settings = Settings() settings.base_headers = {'User-Agent': 'python-requests.org'} settings.accept_gzip = True settings.proxies = None +settings.verbose = None +settings.timeout = None #: Use socket.setdefaulttimeout() as fallback? settings.timeout_fallback = True From 4d0c6cefd033c39b80bd7efb74c53f91c5ec9903 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 03:21:31 -0400 Subject: [PATCH 80/83] yawns --- docs/user/advanced.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 499c354d..83792b58 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -89,7 +89,7 @@ Let's print some request method arguments at runtime:: http://httpbin -Let's hijack some arguments this time:: +Let's hijack some arguments this time with a new callback:: def hack_headers(args): if not args[headers]: @@ -97,12 +97,13 @@ Let's hijack some arguments this time:: args['headers'].update({'X-Testing': 'True'}) - return args hooks = dict(args=hack_headers) headers = dict(yo=dawg) +And give it a try:: + >>> requests.get('http://httpbin/headers', hooks=hooks, headers=headers) { "headers": { From b72eb53a7303c290b1c18c40f1f918a7ec17c58d Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 03:25:31 -0400 Subject: [PATCH 81/83] changing history --- HISTORY.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 93cb1428..7d98f8f8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,10 +1,14 @@ History ------- -0.5.2 (2011-09-??) + +0.6.0 (2011-09-??) ++++++++++++++++++ -* status code reference dict? +* New callback hook system +* New persistient sessions object and context manager +* Transparent Dict-cookie handling +* status code reference object * Removed Response.cached * Added Response.request * all args are kwargs From 3992ed6ad17347c128dac1784020c5e627b3e742 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 03:58:55 -0400 Subject: [PATCH 82/83] cookies docs --- docs/user/quickstart.rst | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 4669e810..61d5c7ba 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -104,4 +104,33 @@ So, we can access the headers using any capitalization we want:: If a header doesn't exist in the Response, its value defaults to ``None``:: >>> r.headers['X-Random'] - None \ No newline at end of file + None + + +Cookies +------- + +If a response contains some Cookies, you can get quick access to them:: + + # cookies test url + >>> url = 'http://httpbin.org/cookies/set/requests-is/awesome' + + >>> r = requests.get(url) + + >>> print r.cookies + {'requests-is': 'awesome'} + +The underlying CookieJar is also available for more advanced handing:: + + >>> r.request.cookiejar + + +To send your own cookies to the server, you can use the ``cookies`` +parameter:: + + >>> url = 'http://httpbin.org/cookies' + >>> cookies = dict(cookies_are='working') + + >>> r = requests.get(url, cookies=cookies) + >>> r.content + '{"cookies": {"cookies_are": "working"}}' From d4502aff8751bbb82e74974c8f6423865d1d210a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Aug 2011 04:00:12 -0400 Subject: [PATCH 83/83] consiserererr --- docs/user/quickstart.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 61d5c7ba..60402f91 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -112,9 +112,7 @@ Cookies If a response contains some Cookies, you can get quick access to them:: - # cookies test url >>> url = 'http://httpbin.org/cookies/set/requests-is/awesome' - >>> r = requests.get(url) >>> print r.cookies