Compare commits

...

90 Commits

Author SHA1 Message Date
Kenneth Reitz
e15834da8e v0.7.2 2011-10-23 15:40:26 -04:00
Kenneth Reitz
f1bb25b1ec bugfix 2011-10-23 15:39:33 -04:00
Kenneth Reitz
02cf3dce37 v0.7.1 2011-10-23 15:18:27 -04:00
Kenneth Reitz
4fb72a2e68 Merge branch 'feature/detach' into develop 2011-10-23 15:18:09 -04:00
Kenneth Reitz
58b1c69b6a Explain custom auth handlers 2011-10-23 15:13:34 -04:00
Kenneth Reitz
8f8dbec0d5 docs for basic/digest auth 2011-10-23 14:59:46 -04:00
Kenneth Reitz
e46251b727 ch ch changes 2011-10-23 14:52:20 -04:00
Kenneth Reitz
82cbae479c history for this 2011-10-23 14:51:22 -04:00
Kenneth Reitz
b88fbb5e0c KILL THE AUTH HANDLERS 2011-10-23 14:49:59 -04:00
Kenneth Reitz
68b4830948 Add argument injection hook back 2011-10-23 14:47:15 -04:00
Kenneth Reitz
9966017a49 Add new utilities from werkzeug 2011-10-23 14:46:58 -04:00
Kenneth Reitz
53c7b77735 Digest Authentication test. 2011-10-23 14:46:44 -04:00
Kenneth Reitz
491a3c075d Move hooks into internal event loop 2011-10-23 14:46:35 -04:00
Kenneth Reitz
4916b1c2ce pep8 2011-10-23 14:44:26 -04:00
Kenneth Reitz
60b37e54b5 Digest authentication support!
Ripped from urllib2
2011-10-23 14:44:20 -04:00
Kenneth Reitz
b7c788cc56 prettier async patching 2011-10-23 14:44:00 -04:00
Kenneth Reitz
12db1576e9 auth tuple fix 2011-10-23 12:26:46 -04:00
Kenneth Reitz
af6aa3e572 Move hooks out of top-level loop 2011-10-23 12:26:41 -04:00
Kenneth Reitz
20e0ee4968 Remove AuthObject entirely. 2011-10-23 11:35:01 -04:00
Kenneth Reitz
4892a305d9 more advanced authentication mechanism 2011-10-23 11:33:03 -04:00
Kenneth Reitz
495c3d5006 Use new authentication style in models
🤘
2011-10-23 11:15:42 -04:00
Kenneth Reitz
06a5aed8e2 assert sessions are using new auth 2011-10-23 11:15:11 -04:00
Kenneth Reitz
0dae52220b fix base64 encoding for httpbasic 2011-10-23 11:14:59 -04:00
Kenneth Reitz
d23c4b94c1 httpbasic authentication cleanup 2011-10-23 11:00:36 -04:00
Kenneth Reitz
94c587d642 Comment out authentication handlers 2011-10-23 11:00:21 -04:00
Kenneth Reitz
340f7a0733 initial auth module w/ httpbasic auth 2011-10-23 10:56:04 -04:00
Kenneth Reitz
b8d4a5ea61 v0.7.1 plans 2011-10-23 10:04:46 -04:00
Kenneth Reitz
1a4dfad52d mod string formatting for 2.5 2011-10-22 21:33:00 -04:00
Kenneth Reitz
b66871e871 v0.7.0 2011-10-22 21:30:47 -04:00
Kenneth Reitz
a74568d8d5 simplify request dispatch flow 2011-10-22 21:10:24 -04:00
Kenneth Reitz
302c4d9c41 Don't alter behavior for safe methods anymore. 2011-10-22 21:01:45 -04:00
Kenneth Reitz
97e3c84084 simplify the URL check 2011-10-22 20:57:39 -04:00
Kenneth Reitz
0ee6c1fa68 kill accept_gzip config 2011-10-22 20:55:46 -04:00
Kenneth Reitz
f0fe8b971b Merge branch 'feature/config' into develop 2011-10-22 20:32:11 -04:00
Kenneth Reitz
8c34fa8477 settings => config 2011-10-22 20:31:51 -04:00
Kenneth Reitz
6be69e303b history for config 2011-10-22 20:29:00 -04:00
Kenneth Reitz
2f6cf50a6c deprecate settings 2011-10-22 20:28:25 -04:00
Kenneth Reitz
f9dfbb73dd requests.request uses config 2011-10-22 20:28:19 -04:00
Kenneth Reitz
903f96cc6a use config throughout models 2011-10-22 20:28:06 -04:00
Kenneth Reitz
5c81fba1eb use config object in sessions 2011-10-22 20:26:12 -04:00
Kenneth Reitz
4dc48b6521 _config => defaults 2011-10-22 20:25:27 -04:00
Kenneth Reitz
ab3200b4cc config.py => _config.py 2011-10-22 19:41:39 -04:00
Kenneth Reitz
185450c5e1 test entry points 2011-10-22 19:40:45 -04:00
Kenneth Reitz
958c424c89 I don't even want to know. 2011-10-22 19:39:23 -04:00
Kenneth Reitz
61be59d996 show both session styles 2011-10-22 19:34:03 -04:00
Kenneth Reitz
e007a5a13f docstring 2011-10-22 19:27:44 -04:00
Kenneth Reitz
3d29f6535f kill wrapping of api methods 2011-10-22 19:27:08 -04:00
Kenneth Reitz
98dc08236c loose yourself 2011-10-22 18:18:39 -04:00
Kenneth Reitz
4ab0bfefd3 Merge branch 'master' into develop 2011-10-22 18:17:04 -04:00
Kenneth Reitz
e418631ce8 Merge branch 'feature/primary-sessions' into develop 2011-10-22 18:16:43 -04:00
Kenneth Reitz
4d97cc7506 disable rudolf until it obeys 2011-10-22 18:16:10 -04:00
Kenneth Reitz
2b3bd78685 Add all methods to session 2011-10-22 18:15:28 -04:00
Kenneth Reitz
597ace5718 import cleanups 2011-10-22 18:13:22 -04:00
Kenneth Reitz
f269c6c89a cookiejar => cookies in models 2011-10-22 18:10:47 -04:00
Kenneth Reitz
d72d116214 Move request and get to session 2011-10-22 18:10:32 -04:00
Kenneth Reitz
d552e6c884 pep8 all the things 2011-10-22 18:07:14 -04:00
Kenneth Reitz
77909f1d49 v0.6.7 2011-10-22 18:07:00 -04:00
Kenneth Reitz
dbf70fc40f All methods are valid now 2011-10-22 18:02:34 -04:00
Kenneth Reitz
3a6d9b09db test improvements 2011-10-22 12:27:10 -04:00
Kenneth Reitz
b19d8785a4 Update AUTHORS 2011-10-21 11:25:02 -03:00
Kenneth Reitz
41e60b8a00 Merge pull request #217 from thedaniel/async-doc
Clarify documentation around async.map and Request.send
2011-10-21 07:24:11 -07:00
Daniel Hengeveld
eddf5254fb Clarify documentation around async.map and Request.send 2011-10-20 23:55:03 -07:00
Kenneth Reitz
ba5eddcf5e WhitespaceBot 2011-10-20 23:53:25 -04:00
Kenneth Reitz
d63cd5f547 periods. 2011-10-20 23:53:20 -04:00
Kenneth Reitz
348d2a2d51 v0.6.7 initial history 2011-10-20 22:43:52 -04:00
Kenneth Reitz
194ff1ad89 #160 2011-10-20 22:09:08 -04:00
Kenneth Reitz
3b6904c5ef Merge branch 'master' into develop 2011-10-20 11:42:09 -04:00
Kenneth Reitz
8d0e03ce41 Merge remote-tracking branch 'origin/develop' into develop 2011-10-20 11:42:00 -04:00
Kenneth Reitz
baa3f78430 Merge pull request #214 from zsiciarz/new_http_codes
New http codes
2011-10-20 08:41:43 -07:00
Zbigniew Siciarz
4f984b0fe6 New HTTP status codes (draft only).
See IETF document at
https://tools.ietf.org/html/draft-nottingham-http-new-status-02.
2011-10-20 17:37:16 +02:00
Kenneth Reitz
c5ed4c9953 Update AUTHORS 2011-10-20 09:46:32 -03:00
Kenneth Reitz
681956e0c4 Merge pull request #213 from fredrikmollerstrand/develop
Fix pip-compared-to-easy-install url
2011-10-20 05:46:16 -07:00
Fredrik Möllerstrand
ad6a8d53d6 docs: fix pip-compared-to-easy-install url. 2011-10-20 11:50:27 +01:00
Kenneth Reitz
0e9d7aad27 add back the ascii art 2011-10-19 16:50:20 -04:00
Kenneth Reitz
6dc664caaf move core imports into __init__.py
because you can't splat import relatively. dumb.
2011-10-19 16:47:46 -04:00
Kenneth Reitz
98c5ded4f3 Merge branch 'master' into develop 2011-10-19 16:36:22 -04:00
Kenneth Reitz
5568f77ad5 Merge branch 'master' into develop 2011-10-19 16:36:12 -04:00
Kenneth Reitz
cfd38263d1 imports cleanup for @alex 🍰 2011-10-19 15:54:22 -04:00
Kenneth Reitz
c052029399 Update AUTHORS 2011-10-19 12:04:34 -03:00
Kenneth Reitz
b7243684de Merge pull request #211 from tvaught/master
Minor doc spelling tweaks
2011-10-19 08:04:04 -07:00
travis
055f7c1eb6 Some Doc and doctoring fixes by the Speling Poleece. 2011-10-19 10:00:13 -05:00
Kenneth Reitz
e27241df73 Update docs/index.rst 2011-10-19 11:41:33 -03:00
Kenneth Reitz
23efd56b12 Merge branch 'develop' 2011-10-19 03:40:39 -04:00
Kenneth Reitz
d938f32089 v0.6.6 2011-10-19 03:39:36 -04:00
Kenneth Reitz
7dc75ce3ce test session persistent params 2011-10-19 03:37:52 -04:00
Kenneth Reitz
8c74860fd1 add params to session merge attributes 2011-10-19 03:32:53 -04:00
Kenneth Reitz
492133ba64 white in spaaaaaccceeee 2011-10-19 01:36:38 -04:00
Kenneth Reitz
82e8fdbb29 Merge branch 'develop' 2011-10-19 01:31:32 -04:00
Kenneth Reitz
4f6fc98509 Merge branch 'release/0.6.4' 2011-10-13 22:23:04 -04:00
Kenneth Reitz
15511584d1 Merge branch 'release/0.6.3' 2011-10-13 21:33:55 -04:00
27 changed files with 825 additions and 653 deletions

View File

@@ -44,3 +44,6 @@ Patches and Suggestions
- Rick Mak
- Johan Bergström
- Josselin Jacquard
- Travis N. Vaught
- Fredrik Möllerstrand
- Daniel Hengeveld

View File

@@ -1,7 +1,36 @@
History
-------
0.6.5 (2011-10-19)
0.7.2 (2011-10-23)
++++++++++++++++++
* PATCH Fix.
0.7.1 (2011-10-23)
++++++++++++++++++
* Move away from urllib2 authentication handling.
* Fully Remove AuthManager, AuthObject, &c.
* New tuple-based auth system with handler callbacks.
0.7.0 (2011-10-22)
++++++++++++++++++
* Sessions are now the primary interface.
* Deprecated InvalidMethodException.
* PATCH fix.
* New config system (no more global settings).
0.6.6 (2011-10-19)
++++++++++++++++++
* Session parameter bugfix (params merging).
0.6.5 (2011-10-18)
++++++++++++++++++
* Offline (fast) test suite.
@@ -12,20 +41,22 @@ History
++++++++++++++++++
* Automatic decoding of unicode, based on HTTP Headers.
* New ``decode_unicode`` setting
* Removal of ``r.read/close`` methods
* New ``decode_unicode`` setting.
* Removal of ``r.read/close`` methods.
* New ``r.faw`` interface for advanced response usage.*
* Automatic expansion of parameterized headers
* Automatic expansion of parameterized headers.
0.6.3 (2011-10-13)
++++++++++++++++++
* Beautiful ``requests.async`` module, for making async requests w/ gevent.
0.6.2 (2011-10-09)
++++++++++++++++++
* GET/HEAD obeys allow_redirects=False
* GET/HEAD obeys allow_redirects=False.
0.6.1 (2011-08-20)

View File

@@ -2,7 +2,7 @@ init:
pip install -r reqs.txt
test:
nosetests test_requests.py --with-color
nosetests test_requests.py
ci: init
nosetests test_requests.py --with-xunit --xunit-file=junit-report.xml

View File

@@ -9,8 +9,7 @@ Encoded Data?
-------------
Requests automatically decompresses gzip-encoded responses, and does
its best to decodes response content to unicode when possible.
it's best to decodes response content to unicode when possible.
its best to decode response content to unicode when possible.
You can get direct access to the raw response (and even the socket),
if needed as well.

View File

@@ -6,7 +6,7 @@ Modules
- `robotframework-requests <https://github.com/bulkan/robotframework-requests>`_, a Robot Framework API wrapper.
- `fullerene <https://github.com/bitprophet/fullerene>`_, a Graphite Dashboard.
- `urbanairship-python <https://github.com/benjaminws/urbanairship-python>`_, a fork of the Urban Airship API wrapper.
- `WhitespaceBot <https://github.com/Gunio/WhitespaceBot/>`_, a project that automatically forks repos, strips trailing whitespace, and sends a pull request.
Articles & Talks
================

View File

@@ -3,7 +3,7 @@
Support
=======
If you have a questions or issues about Requests, there are serveral options:
If you have a questions or issues about Requests, there are several options:
Send a Tweet
------------

View File

@@ -4,7 +4,7 @@ Updates
=======
If you'd like to stay up to date on the community and development of Requests,
there are serveral options:
there are several options:
GitHub
------

View File

@@ -41,6 +41,7 @@ Testimonials
`Twitter, Inc <http://twitter.com>`_,
a U.S. Federal Institution,
NIH,
`Readability <http://readability.com>`_, and
`Work for Pie <http://workforpie.com>`_
use Requests internally.

View File

@@ -17,13 +17,13 @@ A session object has all the methods of the main Requests API.
Let's persist some cookies across requests::
with requests.session() as s:
s = requests.session()
s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get("http://httpbin.org/cookies")
s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get("http://httpbin.org/cookies")
print r.content
# '{"cookies": {"sessioncookie": "123456789"}}'
print r.content
# '{"cookies": {"sessioncookie": "123456789"}}'
Sessions can also be used to provide default data to the request methods::
@@ -39,8 +39,8 @@ Sessions can also be used to provide default data to the request methods::
.. admonition:: Global Settings
Certain parameters are best set at the ``request.config`` level
(e.g.. a global proxy, user agent header).
Certain parameters are best set in the ``config`` dictionary
(e.g. user agent header).
Asynchronous Requests
@@ -69,9 +69,10 @@ We can build a list of ``Request`` objects easily::
rs = [async.get(u) for u in urls]
Now we have a list of ``Request`` objects, ready to be sent. We could
send them one at a time with ``Request.send()``, but that would take a while.
Instead, we'll send them all at the same time with ``async.map()``::
Now we have a list of ``Request`` objects, ready to be sent. We could send them
one at a time with ``Request.send()``, but that would take a while. Instead,
we'll send them all at the same time with ``async.map()``. Using ``async.map()``
will also guarantee execution of the ``response`` hook, described below. ::
>>> async.map(rs)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
@@ -156,6 +157,39 @@ And give it a try::
}
Custom Authentication
---------------------
Requests allows you to use specify your own authentication mechanism.
When you pass our authentication tuple to a request method, the first
string is the type of authentication. 'basic' is inferred if none is
provided.
You can pass in a callable object instead of a string for the first item
in the tuple, and it will be used in place of the built in authentication
callbacks.
Let's pretend that we have a web service that will only respond if the
``X-Pizza`` header is set to a password value. Unlikely, but just go with it.
We simply need to define a callback function that will be used to update the
Request object, right before it is dispatched.
::
def pizza_auth(r, username):
"""Attaches HTTP Pizza Authentication to the given Request object.
"""
r.headers['X-Pizza'] = username
return r
Then, we can make a request using our Pizza Auth::
>>> requests.get('http://pizzabin.org/admin', auth=(pizza_auth, 'kenneth'))
<Response [200]>
Verbose Logging
---------------

View File

@@ -18,7 +18,7 @@ or, with `easy_install <http://pypi.python.org/pypi/setuptools>`_::
$ easy_install requests
But, you really `shouldn't do that <http://www.pip-installer.org/en/latest/index.html#pip-compared-to-easy-install>`_.
But, you really `shouldn't do that <http://www.pip-installer.org/en/latest/other-tools.html#pip-compared-to-easy-install>`_.

View File

@@ -136,3 +136,32 @@ parameter::
>>> r = requests.get(url, cookies=cookies)
>>> r.content
'{"cookies": {"cookies_are": "working"}}'
Basic Authentication
--------------------
Most web services require authentication. There many different types of
authentication, but the most common is called HTTP Basic Auth.
Making requests with Basic Auth is easy, with Requests::
>>> requests.get('https://api.github.com/user', auth=('user', 'pass'))
<Response [200]>
Digest Authentication
---------------------
Another popular form of protecting web service is Digest Authentication.
Requests supports it!::
>>> url = 'http://httpbin.org/digest-auth/auth/user/pass'
>>> requests.get(url, auth=('digest', 'user', 'pass'))
<Response [200]>
-----------------------
Ready for more? Check out the advanced_ section.

View File

@@ -1,4 +1,33 @@
# -*- coding: utf-8 -*-
from core import *
from core import __version__
# __
# /__) _ _ _ _ _/ _
# / ( (- (/ (/ (- _) / _)
# /
"""
requests
~~~~~~~~
:copyright: (c) 2011 by Kenneth Reitz.
:license: ISC, see LICENSE for more details.
"""
__title__ = 'requests'
__version__ = '0.7.2'
__build__ = 0x000702
__author__ = 'Kenneth Reitz'
__license__ = 'ISC'
__copyright__ = 'Copyright 2011 Kenneth Reitz'
from . import utils
from .models import HTTPError, Request, Response
from .api import request, get, head, post, patch, put, delete
from .sessions import session
from .status_codes import codes
from .exceptions import (
RequestException, AuthenticationError, Timeout, URLRequired,
TooManyRedirects
)

View File

@@ -4,103 +4,48 @@
requests.api
~~~~~~~~~~~~
This module impliments the Requests API.
This module implements the Requests API.
:copyright: (c) 2011 by Kenneth Reitz.
:license: ISC, see LICENSE for more details.
"""
import config
from .models import Request, Response, AuthObject
from .status_codes import codes
from .hooks import dispatch_hook
from .utils import cookiejar_from_dict, header_expand
from .sessions import session
__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, hooks=None, return_response=True):
params=None,
data=None,
headers=None,
cookies=None,
files=None,
auth=None,
timeout=None,
allow_redirects=False,
proxies=None,
hooks=None,
return_response=True,
config=None):
"""Constructs and sends a :class:`Request <Request>`.
Returns :class:`Response <Response>` object.
:param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload.
:param auth: (optional) AuthObject to enable Basic HTTP Auth.
:param timeout: (optional) Float describing the timeout of the request.
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param return_response: (optional) If False, an un-sent Request object will returned.
"""
method = str(method).upper()
if cookies is None:
cookies = {}
cookies = cookiejar_from_dict(cookies)
# Expand header values
if headers:
for k, v in headers.items() or {}:
headers[k] = header_expand(v)
args = dict(
method = method,
url = url,
data = data,
params = params,
headers = headers,
cookiejar = cookies,
files = files,
auth = auth,
hooks = hooks,
timeout = timeout or config.settings.timeout,
allow_redirects = allow_redirects,
proxies = proxies or config.settings.proxies,
s = session()
return s.request(
method, url, params, data, headers, cookies, files, auth,
timeout, allow_redirects, proxies, hooks, return_response,
config
)
# Arguments manipulation hook.
args = dispatch_hook('args', hooks, args)
r = Request(**args)
# Pre-request hook.
r = dispatch_hook('pre_request', hooks, r)
# Don't send if asked nicely.
if not return_response:
return 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
def get(url, **kwargs):
"""Sends a GET request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
kwargs.setdefault('allow_redirects', True)
return request('GET', url, **kwargs)
@@ -146,7 +91,7 @@ def patch(url, data='', **kwargs):
:param **kwargs: Optional arguments that ``request`` takes.
"""
return request('patch', url, **kwargs)
return request('patch', url, data=data, **kwargs)
def delete(url, **kwargs):

View File

@@ -28,7 +28,7 @@ __all__ = (
)
def _patched(f):
def patched(f):
"""Patches a given API function to not send."""
def wrapped(*args, **kwargs):
@@ -37,7 +37,7 @@ def _patched(f):
return wrapped
def _send(r, pools=None):
def send(r, pools=None):
"""Sends a given Request object."""
if pools:
@@ -45,23 +45,17 @@ def _send(r, pools=None):
r.send()
# Post-request hook.
r = dispatch_hook('post_request', r.hooks, r)
# Response manipulation hook.
r.response = dispatch_hook('response', r.hooks, r.response)
return r.response
# Patched requests.api functions.
get = _patched(api.get)
head = _patched(api.head)
post = _patched(api.post)
put = _patched(api.put)
patch = _patched(api.patch)
delete = _patched(api.delete)
request = _patched(api.request)
get = patched(api.get)
head = patched(api.head)
post = patched(api.post)
put = patched(api.put)
patch = patched(api.patch)
delete = patched(api.delete)
request = patched(api.request)
def map(requests, prefetch=True):
@@ -71,7 +65,7 @@ def map(requests, prefetch=True):
:param prefetch: If False, the content will not be downloaded immediately.
"""
jobs = [gevent.spawn(_send, r) for r in requests]
jobs = [gevent.spawn(send, r) for r in requests]
gevent.joinall(jobs)
if prefetch:

151
requests/auth.py Normal file
View File

@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
"""
requests.auth
~~~~~~~~~~~~~
This module contains the authentication handlers for Requests.
"""
import time
import hashlib
from base64 import b64encode
from urlparse import urlparse
from .utils import randombytes, parse_dict_header
def http_basic(r, username, password):
"""Attaches HTTP Basic Authentication to the given Request object.
Arguments should be considered non-positional.
"""
username = str(username)
password = str(password)
auth_s = b64encode('%s:%s' % (username, password))
r.headers['Authorization'] = ('Basic %s' % auth_s)
return r
def http_digest(r, username, password):
"""Attaches HTTP Digest Authentication to the given Request object.
Arguments should be considered non-positional.
"""
def handle_401(r):
"""Takes the given response and tries digest-auth, if needed."""
s_auth = r.headers.get('www-authenticate', '')
if 'digest' in s_auth.lower():
last_nonce = ''
nonce_count = 0
chal = parse_dict_header(s_auth.replace('Digest ', ''))
realm = chal['realm']
nonce = chal['nonce']
qop = chal.get('qop')
algorithm = chal.get('algorithm', 'MD5')
opaque = chal.get('opaque', None)
algorithm = algorithm.upper()
# lambdas assume digest modules are imported at the top level
if algorithm == 'MD5':
H = lambda x: hashlib.md5(x).hexdigest()
elif algorithm == 'SHA':
H = lambda x: hashlib.sha1(x).hexdigest()
# XXX MD5-sess
KD = lambda s, d: H("%s:%s" % (s, d))
if H is None:
return None
# XXX not implemented yet
entdig = None
path = urlparse(r.request.url).path
A1 = "%s:%s:%s" % (username, realm, password)
A2 = "%s:%s" % (r.request.method, path)
if qop == 'auth':
if nonce == last_nonce:
nonce_count += 1
else:
nonce_count = 1
last_nonce = nonce
ncvalue = '%08x' % nonce_count
cnonce = (hashlib.sha1("%s:%s:%s:%s" % (
nonce_count, nonce, time.ctime(), randombytes(8)))
.hexdigest()[:16]
)
noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, H(A2))
respdig = KD(H(A1), noncebit)
elif qop is None:
respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
else:
# XXX handle auth-int.
return None
# XXX should the partial digests be encoded too?
base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
'response="%s"' % (username, realm, nonce, path, respdig)
if opaque:
base += ', opaque="%s"' % opaque
if entdig:
base += ', digest="%s"' % entdig
base += ', algorithm="%s"' % algorithm
if qop:
base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
r.request.headers['Authorization'] = 'Digest %s' % (base)
r.request.send(anyway=True)
_r = r.request.response
_r.history.append(r)
print _r.status_code
# r.request.response
print locals()
print _r.headers
return _r
return r
r.hooks['response'] = handle_401
return r
def dispatch(t):
"""Given an auth tuple, return an expanded version."""
if not t:
return t
else:
t = list(t)
# Make sure they're passing in something.
assert len(t) >= 2
# If only two items are passed in, assume HTTPBasic.
if (len(t) == 2):
t.insert(0, 'basic')
# Allow built-in string referenced auths.
if isinstance(t[0], basestring):
if t[0] in ('basic', 'forced_basic'):
t[0] = http_basic
elif t[0] in ('digest',):
t[0] = http_digest
# Return a custom callable.
return (t[0], tuple(t[1:]))

View File

@@ -1,68 +0,0 @@
# -*- coding: utf-8 -*-
"""
requests.config
~~~~~~~~~~~~~~~
This module provides the Requests settings feature set.
"""
class Settings(object):
_singleton = {}
# attributes with defaults
__attrs__ = []
def __init__(self, **kwargs):
super(Settings, self).__init__()
self.__dict__ = self._singleton
def __call__(self, *args, **kwargs):
# new instance of class to call
r = self.__class__()
# cache previous settings for __exit__
r.__cache = self.__dict__.copy()
map(self.__cache.setdefault, self.__attrs__)
# set new settings
self.__dict__.update(*args, **kwargs)
return r
def __enter__(self):
pass
def __exit__(self, *args):
# restore cached copy
self.__dict__.update(self.__cache.copy())
del self.__cache
def __getattribute__(self, key):
if key in object.__getattribute__(self, '__attrs__'):
try:
return object.__getattribute__(self, key)
except AttributeError:
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.verbose = None
settings.timeout = None
settings.max_redirects = 30
settings.decode_unicode = True
#: Use socket.setdefaulttimeout() as fallback?
settings.timeout_fallback = True

View File

@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
"""
requests.core
~~~~~~~~~~~~~
This module implements the main Requests system.
:copyright: (c) 2011 by Kenneth Reitz.
:license: ISC, see LICENSE for more details.
"""
__title__ = 'requests'
__version__ = '0.6.5'
__build__ = 0x000605
__author__ = 'Kenneth Reitz'
__license__ = 'ISC'
__copyright__ = 'Copyright 2011 Kenneth Reitz'
from models import HTTPError, Request, Response
from api import *
from exceptions import *
from sessions import session
from status_codes import codes
from config import settings
import utils

38
requests/defaults.py Normal file
View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
requests.defaults
~~~~~~~~~~~~~~~~~
This module provides the Requests configuration defaults.
settings parameters:
- :base_headers: - Sets default User-Agent to `python-requests.org`
- :accept_gzip: - Whether or not to accept gzip-compressed data
- :proxies: - http proxies?
- :verbose: - display verbose information?
- :timeout: - timeout time until request terminates
- :max_redirects: - maximum number of allowed redirects?
- :decode_unicode: - whether or not to accept unicode?
"""
from . import __version__
defaults = dict()
defaults['base_headers'] = {
'User-Agent': 'python-requests/%s' % __version__,
'Accept-Encoding': ', '.join(('identity', 'deflate', 'compress', 'gzip')),
}
defaults['proxies'] = {}
defaults['verbose'] = None
defaults['timeout'] = None
defaults['max_redirects'] = 30
defaults['decode_unicode'] = True
defaults['timeout_fallback'] = True
# defaults['keep_alive'] = True
# defaults['max_connections'] = 10

View File

@@ -2,25 +2,24 @@
"""
requests.exceptions
~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~
This module contains the set of Requests' exceptions.
"""
class RequestException(Exception):
"""There was an ambiguous exception that occured while handling your
"""There was an ambiguous exception that occurred while handling your
request."""
class AuthenticationError(RequestException):
"""The authentication credentials provided were invalid."""
class Timeout(RequestException):
"""The request timed out."""
class URLRequired(RequestException):
"""A valid URL is required to make a request."""
class InvalidMethod(RequestException):
"""An inappropriate method was attempted."""
class TooManyRedirects(RequestException):
"""Too many redirects."""

View File

@@ -26,7 +26,7 @@ import warnings
def dispatch_hook(key, hooks, hook_data):
"""Dipatches a hook dictionary on a given peice of data."""
"""Dispatches a hook dictionary on a given piece of data."""
hooks = hooks or dict()

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""
@@ -9,23 +10,23 @@ requests.models
import urllib
import urllib2
import socket
import codecs
import zlib
from urllib2 import HTTPError
from urlparse import urlparse, urlunparse, urljoin
from datetime import datetime
from .config import settings
from .monkeys import Request as _Request, HTTPBasicAuthHandler, HTTPForcedBasicAuthHandler, HTTPDigestAuthHandler, HTTPRedirectHandler
from .hooks import dispatch_hook
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, get_unicode_from_response, stream_decode_response_unicode, decode_gzip, stream_decode_gzip
from .utils import (dict_from_cookiejar, get_unicode_from_response, stream_decode_response_unicode, decode_gzip, stream_decode_gzip)
from .status_codes import codes
from .exceptions import RequestException, AuthenticationError, Timeout, URLRequired, InvalidMethod, TooManyRedirects
from .exceptions import Timeout, URLRequired, TooManyRedirects
from .monkeys import Request as _Request
from .monkeys import HTTPRedirectHandler
from .auth import dispatch as auth_dispatch
REDIRECT_STATI = (codes.moved, codes.found, codes.other, codes.temporary_moved)
@@ -37,18 +38,29 @@ 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, hooks=None):
url=None,
headers=dict(),
files=None,
method=None,
data=dict(),
params=dict(),
auth=None,
cookies=None,
timeout=None,
redirect=False,
allow_redirects=False,
proxies=None,
hooks=None,
config=None):
#: Float describ the timeout of the request.
#: Float describes the timeout of the request.
# (Use socket.setdefaulttimeout() as fallback)
self.timeout = timeout
#: Request URL.
self.url = url
#: Dictonary of HTTP Headers to attach to the :class:`Request <Request>`.
#: Dictionary of HTTP Headers to attach to the :class:`Request <Request>`.
self.headers = headers
#: Dictionary of files to multipart upload (``{filename: content}``).
@@ -82,16 +94,14 @@ class Request(object):
#: content and metadata of HTTP Response, once :attr:`sent <send>`.
self.response = Response()
if isinstance(auth, (list, tuple)):
auth = AuthObject(*auth)
if not auth:
auth = auth_manager.get_auth(self.url)
#: :class:`AuthObject` to attach to :class:`Request <Request>`.
self.auth = auth
#: Authentication tuple to attach to :class:`Request <Request>`.
self.auth = auth_dispatch(auth)
#: CookieJar to attach to :class:`Request <Request>`.
self.cookiejar = cookiejar
self.cookies = cookies
#: Dictionary of configurations for this request.
self.config = config
#: True if Request has been sent.
self.sent = False
@@ -99,57 +109,33 @@ class Request(object):
#: Event-handling hooks.
self.hooks = hooks
# Header manipulation and defaults.
if settings.accept_gzip:
settings.base_headers.update({'Accept-Encoding': 'gzip'})
if headers:
headers = CaseInsensitiveDict(self.headers)
else:
headers = CaseInsensitiveDict()
for (k, v) in settings.base_headers.items():
for (k, v) in self.config.get('base_headers', {}).items():
if k not in headers:
headers[k] = v
self.headers = headers
# Pre-request hook.
r = dispatch_hook('pre_request', hooks, self)
self.__dict__.update(r.__dict__)
def __repr__(self):
return '<Request [%s]>' % (self.method)
def _checks(self):
"""Deterministic checks for consistency."""
if not self.url:
raise URLRequired
def _get_opener(self):
"""Creates appropriate opener object for urllib2."""
_handlers = []
if self.cookiejar is not None:
_handlers.append(urllib2.HTTPCookieProcessor(self.cookiejar))
if self.auth:
if not isinstance(self.auth.handler,
(urllib2.AbstractBasicAuthHandler,
urllib2.AbstractDigestAuthHandler)):
# 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)
_handlers.append(self.auth.handler)
if self.cookies is not None:
_handlers.append(urllib2.HTTPCookieProcessor(self.cookies))
if self.proxies:
_handlers.append(urllib2.ProxyHandler(self.proxies))
@@ -185,14 +171,15 @@ class Request(object):
def build(resp):
response = Response()
response.config = self.config
response.status_code = getattr(resp, 'code', None)
try:
response.headers = CaseInsensitiveDict(getattr(resp.info(), 'dict', None))
response.raw = resp
if self.cookiejar:
response.cookies = dict_from_cookiejar(self.cookiejar)
if self.cookies:
response.cookies = dict_from_cookiejar(self.cookies)
except AttributeError:
@@ -219,7 +206,7 @@ class Request(object):
r.raw.close()
if not len(history) < settings.max_redirects:
if not len(history) < self.config.get('max_redirects'):
raise TooManyRedirects()
history.append(r)
@@ -243,9 +230,16 @@ class Request(object):
method = self.method
request = Request(
url, self.headers, self.files, method,
self.data, self.params, self.auth, self.cookiejar,
redirect=True
url=url,
headers=self.headers,
files=self.files,
method=method,
# data=self.data,
# params=self.params,
auth=self.auth,
cookies=self.cookies,
redirect=True,
config=self.config
)
request.send()
r = request.response
@@ -310,32 +304,44 @@ class Request(object):
already been sent.
"""
self._checks()
# Some people...
if not self.url:
raise URLRequired
# Logging
if settings.verbose:
settings.verbose.write('%s %s %s\n' % (
if self.config.get('verbose'):
self.config.get('verbose').write('%s %s %s\n' % (
datetime.now().isoformat(), self.method, self.url
))
# Build the URL
url = self._build_url()
if self.method in ('GET', 'HEAD', 'DELETE'):
req = _Request(url, method=self.method)
# Attach uploaded files.
if self.files:
register_openers()
# Add form-data to the multipart.
if self.data:
self.files.update(self.data)
data, headers = multipart_encode(self.files)
else:
data = self._enc_data
headers = {}
if self.files:
register_openers()
if self.auth:
auth_func, auth_args = self.auth
if self.data:
self.files.update(self.data)
r = auth_func(self, *auth_args)
datagen, headers = multipart_encode(self.files)
req = _Request(url, data=datagen, headers=headers, method=self.method)
self.__dict__.update(r.__dict__)
else:
req = _Request(url, data=self._enc_data, method=self.method)
# Build the Urllib2 Request.
req = _Request(url, data=data, headers=headers, method=self.method)
# Add the headers to the request.
if self.headers:
for k,v in self.headers.iteritems():
req.add_header(k, v)
@@ -353,19 +359,19 @@ class Request(object):
if not 'timeout' in str(err):
raise
if settings.timeout_fallback:
if self.config.get('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
if self.config.get('timeout_fallback'):
# restore global timeout
socket.setdefaulttimeout(old_timeout)
if self.cookiejar is not None:
self.cookiejar.extract_cookies(resp, req)
if self.cookies is not None:
self.cookies.extract_cookies(resp, req)
except (urllib2.HTTPError, urllib2.URLError), why:
if hasattr(why, 'reason'):
@@ -383,6 +389,13 @@ class Request(object):
self.sent = self.response.ok
# Response manipulation hook.
self.response = dispatch_hook('response', self.hooks, self.response)
# Post-request hook.
r = dispatch_hook('post_request', self.hooks, self)
self.__dict__.update(r.__dict__)
return self.sent
@@ -412,10 +425,10 @@ class Response(object):
#: Final URL location of Response.
self.url = None
#: True if no :attr:`error` occured.
#: True if no :attr:`error` occurred.
self.ok = False
#: Resulting :class:`HTTPError` of request, if one occured.
#: Resulting :class:`HTTPError` of request, if one occurred.
self.error = None
#: A list of :class:`Response <Response>` objects from
@@ -429,6 +442,9 @@ class Response(object):
#: A dictionary of Cookies the server sent back.
self.cookies = None
#: Dictionary of configurations for this request.
self.config = None
def __repr__(self):
return '<Response [%s]>' % (self.status_code)
@@ -460,7 +476,7 @@ class Response(object):
if 'gzip' in self.headers.get('content-encoding', ''):
gen = stream_decode_gzip(gen)
if decode_unicode is None:
decode_unicode = settings.decode_unicode
decode_unicode = self.config.get('decode_unicode')
if decode_unicode:
gen = stream_decode_response_unicode(gen, self)
return gen
@@ -489,7 +505,7 @@ class Response(object):
pass
# Decode unicode content.
if settings.decode_unicode:
if self.config.get('decode_unicode'):
self._content = get_unicode_from_response(self)
self._content_consumed = True
@@ -497,183 +513,7 @@ class Response(object):
def raise_for_status(self):
"""Raises stored :class:`HTTPError` or :class:`URLError`, if one occured."""
"""Raises stored :class:`HTTPError` or :class:`URLError`, if one occurred."""
if self.error:
raise self.error
class AuthManager(object):
"""Requests Authentication Manager."""
def __new__(cls):
singleton = cls.__dict__.get('__singleton__')
if singleton is not None:
return singleton
cls.__singleton__ = singleton = object.__new__(cls)
return singleton
def __init__(self):
self.passwd = {}
self._auth = {}
def __repr__(self):
return '<AuthManager [%s]>' % (self.method)
def add_auth(self, uri, auth):
"""Registers AuthObject to AuthManager."""
uri = self.reduce_uri(uri, False)
# try to make it an AuthObject
if not isinstance(auth, AuthObject):
try:
auth = AuthObject(*auth)
except TypeError:
pass
self._auth[uri] = auth
def add_password(self, realm, uri, user, passwd):
"""Adds password to AuthManager."""
# uri could be a single URI or a sequence
if isinstance(uri, basestring):
uri = [uri]
reduced_uri = tuple([self.reduce_uri(u, False) for u in uri])
if reduced_uri not in self.passwd:
self.passwd[reduced_uri] = {}
self.passwd[reduced_uri] = (user, passwd)
def find_user_password(self, realm, authuri):
for uris, authinfo in self.passwd.iteritems():
reduced_authuri = self.reduce_uri(authuri, False)
for uri in uris:
if self.is_suburi(uri, reduced_authuri):
return authinfo
return (None, None)
def get_auth(self, uri):
(in_domain, in_path) = self.reduce_uri(uri, False)
for domain, path, authority in (
(i[0][0], i[0][1], i[1]) for i in self._auth.iteritems()
):
if in_domain == domain:
if path in in_path:
return authority
def reduce_uri(self, uri, default_port=True):
"""Accept authority or URI and extract only the authority and path."""
# note HTTP URLs do not have a userinfo component
parts = urllib2.urlparse.urlsplit(uri)
if parts[1]:
# URI
scheme = parts[0]
authority = parts[1]
path = parts[2] or '/'
else:
# host or host:port
scheme = None
authority = uri
path = '/'
host, port = urllib2.splitport(authority)
if default_port and port is None and scheme is not None:
dport = {"http": 80,
"https": 443,
}.get(scheme)
if dport is not None:
authority = "%s:%d" % (host, dport)
return authority, path
def is_suburi(self, base, test):
"""Check if test is below base in a URI tree
Both args must be URIs in reduced form.
"""
if base == test:
return True
if base[0] != test[0]:
return False
common = urllib2.posixpath.commonprefix((base[1], test[1]))
if len(common) == len(base[1]):
return True
return False
def empty(self):
self.passwd = {}
def remove(self, uri, realm=None):
# uri could be a single URI or a sequence
if isinstance(uri, basestring):
uri = [uri]
for default_port in True, False:
reduced_uri = tuple([self.reduce_uri(u, default_port) for u in uri])
del self.passwd[reduced_uri][realm]
def __contains__(self, uri):
# uri could be a single URI or a sequence
if isinstance(uri, basestring):
uri = [uri]
uri = tuple([self.reduce_uri(u, False) for u in uri])
if uri in self.passwd:
return True
return False
auth_manager = AuthManager()
class AuthObject(object):
"""The :class:`AuthObject` is a simple HTTP Authentication token. When
given to a Requests function, it enables Basic HTTP Authentication for that
Request. You can also enable Authorization for domain realms with AutoAuth.
See AutoAuth for more details.
:param username: Username to authenticate with.
:param password: Password for given username.
:param realm: (optional) the realm this auth applies to
:param handler: (optional) basic || digest || proxy_basic || proxy_digest
"""
_handlers = {
'basic': HTTPBasicAuthHandler,
'forced_basic': HTTPForcedBasicAuthHandler,
'digest': HTTPDigestAuthHandler,
'proxy_basic': urllib2.ProxyBasicAuthHandler,
'proxy_digest': urllib2.ProxyDigestAuthHandler
}
def __init__(self, username, password, handler='forced_basic', realm=None):
self.username = username
self.password = password
self.realm = realm
if isinstance(handler, basestring):
self.handler = self._handlers.get(handler.lower(), HTTPForcedBasicAuthHandler)
else:
self.handler = handler

View File

@@ -29,120 +29,11 @@ class Request(urllib2.Request):
class HTTPRedirectHandler(urllib2.HTTPRedirectHandler):
"""HTTP Redirect handler."""
def http_error_301(self, req, fp, code, msg, headers):
def _pass(self, req, fp, code, msg, headers):
pass
http_error_302 = http_error_303 = http_error_307 = http_error_301
http_error_302 = _pass
http_error_303 = _pass
http_error_307 = _pass
http_error_301 = _pass
class HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
"""HTTP Basic Auth Handler with authentication loop fixes."""
def __init__(self, *args, **kwargs):
urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
self.retried_req = None
self.retried = 0
def reset_retry_count(self):
# Python 2.6.5 will call this on 401 or 407 errors and thus loop
# forever. We disable reset_retry_count completely and reset in
# http_error_auth_reqed instead.
pass
def http_error_auth_reqed(self, auth_header, host, req, headers):
# Reset the retry counter once for each request.
if req is not self.retried_req:
self.retried_req = req
self.retried = 0
return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
self, auth_header, host, req, headers
)
class HTTPForcedBasicAuthHandler(HTTPBasicAuthHandler):
"""HTTP Basic Auth Handler with forced Authentication."""
auth_header = 'Authorization'
rx = re.compile('(?:.*,)*[ \t]*([^ \t]+)[ \t]+'
'realm=(["\'])(.*?)\\2', re.I)
def __init__(self, *args, **kwargs):
HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
def http_error_401(self, req, fp, code, msg, headers):
url = req.get_full_url()
response = self._http_error_auth_reqed('www-authenticate', url, req, headers)
self.reset_retry_count()
return response
http_error_404 = http_error_401
def _http_error_auth_reqed(self, authreq, host, req, headers):
authreq = headers.get(authreq, None)
if self.retried > 5:
# retry sending the username:password 5 times before failing.
raise urllib2.HTTPError(req.get_full_url(), 401, "basic auth failed",
headers, None)
else:
self.retried += 1
if authreq:
mo = self.rx.search(authreq)
if mo:
scheme, quote, realm = mo.groups()
if scheme.lower() == 'basic':
response = self.retry_http_basic_auth(host, req, realm)
if response and response.code not in (401, 404):
self.retried = 0
return response
else:
response = self.retry_http_basic_auth(host, req, 'Realm')
if response and response.code not in (401, 404):
self.retried = 0
return response
class HTTPDigestAuthHandler(urllib2.HTTPDigestAuthHandler):
def __init__(self, *args, **kwargs):
urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
self.retried_req = None
def reset_retry_count(self):
# Python 2.6.5 will call this on 401 or 407 errors and thus loop
# forever. We disable reset_retry_count completely and reset in
# http_error_auth_reqed instead.
pass
def http_error_auth_reqed(self, auth_header, host, req, headers):
# Reset the retry counter once for each request.
if req is not self.retried_req:
self.retried_req = req
self.retried = 0
# In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
# it doesn't know about the auth type requested. This can happen if
# somebody is using BasicAuth and types a bad password.
try:
return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
self, auth_header, host, req, headers)
except ValueError, inst:
arg = inst.args[0]
if arg.startswith("AbstractDigestAuthHandler doesn't know "):
return
raise

View File

@@ -11,9 +11,10 @@ requests (cookies, auth, proxies).
import cookielib
from . import api
from .utils import add_dict_to_cookiejar
from .defaults import defaults
from .models import Request
from .hooks import dispatch_hook
from .utils import add_dict_to_cookiejar, cookiejar_from_dict, header_expand
def merge_kwargs(local_kwarg, default_kwarg):
@@ -25,6 +26,9 @@ def merge_kwargs(local_kwarg, default_kwarg):
if default_kwarg is None:
return local_kwarg
if isinstance(local_kwarg, basestring):
return local_kwarg
if local_kwarg is None:
return default_kwarg
@@ -32,8 +36,6 @@ def merge_kwargs(local_kwarg, default_kwarg):
if not hasattr(default_kwarg, 'items'):
return local_kwarg
# Update new values.
kwargs = default_kwarg.copy()
kwargs.update(local_kwarg)
@@ -49,7 +51,7 @@ def merge_kwargs(local_kwarg, default_kwarg):
class Session(object):
"""A Requests session."""
__attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks']
__attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks', 'params', 'config']
def __init__(self,
@@ -58,7 +60,9 @@ class Session(object):
auth=None,
timeout=None,
proxies=None,
hooks=None):
hooks=None,
params=None,
config=None):
self.headers = headers or {}
self.cookies = cookies or {}
@@ -66,14 +70,15 @@ class Session(object):
self.timeout = timeout
self.proxies = proxies or {}
self.hooks = hooks or {}
self.params = params or {}
self.config = config or {}
for (k, v) in defaults.items():
self.config.setdefault(k, v)
# Set up a CookieJar to be used by default
self.cookies = cookielib.FileCookieJar()
# Map and wrap requests.api methods
self._map_api_methods()
def __repr__(self):
return '<requests-client at 0x%x>' % (id(self))
@@ -83,51 +88,158 @@ class Session(object):
def __exit__(self, *args):
pass
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 request(self, method, url,
params=None,
data=None,
headers=None,
cookies=None,
files=None,
auth=None,
timeout=None,
allow_redirects=False,
proxies=None,
hooks=None,
return_response=True,
config=None):
"""Constructs and sends a :class:`Request <Request>`.
Returns :class:`Response <Response>` object.
:param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload.
:param auth: (optional) Auth typle to enable Basic/Digest/Custom HTTP Auth.
:param timeout: (optional) Float describing the timeout of the request.
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param return_response: (optional) If False, an un-sent Request object will returned.
:param config: (optional) A configuration dictionary.
"""
def pass_args(func):
def wrapper_func(*args, **kwargs):
method = str(method).upper()
# Argument collector.
_kwargs = {}
if cookies is None:
cookies = {}
# 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(
self.cookies, kwargs['cookies']
)
if isinstance(cookies, dict):
cookies = add_dict_to_cookiejar(self.cookies, cookies)
for attr in self.__attrs__:
# for attr in ['headers',]:
s_val = self.__dict__.get(attr)
r_val = kwargs.get(attr)
cookies = cookiejar_from_dict(cookies)
new_attr = merge_kwargs(r_val, s_val)
# Expand header values
if headers:
for k, v in headers.items() or {}:
headers[k] = header_expand(v)
# Skip attributes that were set to None.
if new_attr is not None:
_kwargs[attr] = new_attr
args = dict(
method=method,
url=url,
data=data,
params=params,
headers=headers,
cookies=cookies,
files=files,
auth=auth,
hooks=hooks,
timeout=timeout,
allow_redirects=allow_redirects,
proxies=proxies,
config=config
)
# Make sure we didn't miss anything.
for (k, v) in kwargs.items():
if k not in _kwargs:
_kwargs[k] = v
for attr in self.__attrs__:
session_val = getattr(self, attr, None)
local_val = args.get(attr)
return func(*args, **_kwargs)
args[attr] = merge_kwargs(local_val, session_val)
return wrapper_func
# Arguments manipulation hook.
args = dispatch_hook('args', hooks, args)
r = Request(**args)
# Don't send if asked nicely.
if not return_response:
return r
# Send the HTTP Request.
r.send()
return r.response
def get(self, url, **kwargs):
"""Sends a GET request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
kwargs.setdefault('allow_redirects', True)
return self.request('GET', url, **kwargs)
def head(self, url, **kwargs):
"""Sends a HEAD request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
kwargs.setdefault('allow_redirects', True)
return self.request('HEAD', url, **kwargs)
def post(self, url, data='', **kwargs):
"""Sends a POST request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return self.request('post', url, data=data, **kwargs)
def put(self, url, data='', **kwargs):
"""Sends a PUT request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return self.request('put', url, data=data, **kwargs)
def patch(self, url, data='', **kwargs):
"""Sends a PATCH request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return self.request('patch', url, data='', **kwargs)
def delete(self, url, **kwargs):
"""Sends a DELETE request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
"""
return self.request('delete', url, **kwargs)
# Map and decorate each function available in requests.api
map(lambda fn: setattr(self, fn, pass_args(getattr(api, fn))),
api.__all__)
def session(**kwargs):
"""Returns a :class:`Session` for context-managment."""
"""Returns a :class:`Session` for context-management."""
return Session(**kwargs)

View File

@@ -47,15 +47,18 @@ _codes = {
412: ('precondition_failed', 'precondition'),
413: ('request_entity_too_large',),
414: ('request_uri_too_large',),
415: ('unspported_media_type', 'unspported_media', 'media_type'),
415: ('unsupported_media_type', 'unsupported_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'),
424: ('failed_dependency', 'dependency'),
425: ('unordered_collection', 'unordered'),
426: ('upgrade_required', 'upgrade'),
428: ('precondition_required', 'precondition'),
429: ('too_many_requests', 'too_many'),
431: ('header_fields_too_large', 'fields_too_large'),
444: ('no_response', 'none'),
449: ('retry_with', 'retry'),
450: ('blocked_by_windows_parental_controls', 'parental_controls'),

View File

@@ -4,7 +4,7 @@
requests.structures
~~~~~~~~~~~~~~~~~~~
Datastructures that power Requests.
Data structures that power Requests.
"""

View File

@@ -12,9 +12,103 @@ that are also useful for external consumption.
import cgi
import codecs
import cookielib
import os
import random
import re
import zlib
from urllib2 import parse_http_list as _parse_list_header
# From mitsuhiko/werkzeug (used with permission).
def parse_list_header(value):
"""Parse lists as described by RFC 2068 Section 2.
In particular, parse comma-separated lists where the elements of
the list may include quoted-strings. A quoted-string could
contain a comma. A non-quoted string could have quotes in the
middle. Quotes are removed automatically after parsing.
It basically works like :func:`parse_set_header` just that items
may appear multiple times and case sensitivity is preserved.
The return value is a standard :class:`list`:
>>> parse_list_header('token, "quoted value"')
['token', 'quoted value']
To create a header from the :class:`list` again, use the
:func:`dump_header` function.
:param value: a string with a list header.
:return: :class:`list`
"""
result = []
for item in _parse_list_header(value):
if item[:1] == item[-1:] == '"':
item = unquote_header_value(item[1:-1])
result.append(item)
return result
# From mitsuhiko/werkzeug (used with permission).
def parse_dict_header(value):
"""Parse lists of key, value pairs as described by RFC 2068 Section 2 and
convert them into a python dict:
>>> d = parse_dict_header('foo="is a fish", bar="as well"')
>>> type(d) is dict
True
>>> sorted(d.items())
[('bar', 'as well'), ('foo', 'is a fish')]
If there is no value for a key it will be `None`:
>>> parse_dict_header('key_without_value')
{'key_without_value': None}
To create a header from the :class:`dict` again, use the
:func:`dump_header` function.
:param value: a string with a dict header.
:return: :class:`dict`
"""
result = {}
for item in _parse_list_header(value):
if '=' not in item:
result[item] = None
continue
name, value = item.split('=', 1)
if value[:1] == value[-1:] == '"':
value = unquote_header_value(value[1:-1])
result[name] = value
return result
# From mitsuhiko/werkzeug (used with permission).
def unquote_header_value(value, is_filename=False):
r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
This does not use the real unquoting but what browsers are actually
using for quoting.
:param value: the header value to unquote.
"""
if value and value[0] == value[-1] == '"':
# this is not the real unquoting, but fixing this so that the
# RFC is met will result in bugs with internet explorer and
# probably some other browsers as well. IE for example is
# uploading files with "C:\foo\bar.txt" as filename
value = value[1:-1]
# if this is a filename and the starting characters look like
# a UNC path, then just return the value without quotes. Using the
# replace sequence below on a UNC path has the effect of turning
# the leading double slash into a single slash and then
# _fix_ie_filename() doesn't work correctly. See #458.
if not is_filename or value[:2] != '\\\\':
return value.replace('\\\\', '\\').replace('\\"', '"')
return value
def header_expand(headers):
"""Returns an HTTP Header value string from a dictionary.
@@ -55,7 +149,7 @@ def header_expand(headers):
collector.append(', ')
# Remove trailing seperators.
# Remove trailing separators.
if collector[-1] in (', ', '; '):
del collector[-1]
@@ -63,6 +157,21 @@ def header_expand(headers):
def randombytes(n):
"""Return n random bytes."""
# Use /dev/urandom if it is available. Fall back to random module
# if not. It might be worthwhile to extend this function to use
# other platform-specific mechanisms for getting random bytes.
if os.path.exists("/dev/urandom"):
f = open("/dev/urandom")
s = f.read(n)
f.close()
return s
else:
L = [chr(random.randrange(0, 256)) for i in range(n)]
return "".join(L)
def dict_from_cookiejar(cj):
"""Returns a key/value dictionary from a CookieJar.
@@ -199,7 +308,7 @@ def stream_decode_response_unicode(iterator, r):
def get_unicode_from_response(r):
"""Returns the requested content back in unicode.
:param r: Reponse object to get unicode content from.
:param r: Response object to get unicode content from.
Tried:

View File

@@ -8,6 +8,7 @@ import cookielib
import os
import unittest
import requests
import envoy
try:
@@ -15,11 +16,9 @@ try:
except ImportError:
import json
import requests
from requests.sessions import Session
PORT = os.environ.get('HTTPBIN_PORT', '7045')
# TODO: Detect an open port.
PORT = os.environ.get('HTTPBIN_PORT', '7077')
HTTPBIN_URL = 'http://0.0.0.0:%s/' % (PORT)
# HTTPBIN_URL = 'http://127.0.0.1:8000/'
@@ -47,8 +46,9 @@ class RequestsTestSuite(unittest.TestCase):
if not _httpbin:
self.httpbin = envoy.connect('gunicorn httpbin:app --bind=0.0.0.0:%s' % (PORT))
c = envoy.connect('gunicorn httpbin:app --bind=0.0.0.0:%s' % (PORT))
self.httpbin = c
_httpbin = True
time.sleep(1)
@@ -58,22 +58,34 @@ class RequestsTestSuite(unittest.TestCase):
"""Teardown."""
# self.httpbin.kill()
def test_entry_points(self):
import requests
requests.session
requests.session().get
requests.session().head
requests.get
requests.head
requests.put
requests.patch
requests.post
def test_invalid_url(self):
self.assertRaises(ValueError, requests.get, 'hiwpefhipowhefopw')
def test_HTTP_200_OK_GET(self):
r = requests.get(httpbin('/get'))
self.assertEqual(r.status_code, 200)
def test_HTTP_302_ALLOW_REDIRECT_GET(self):
r = requests.get(httpbin('redirect', '1'))
self.assertEqual(r.status_code, 200)
r = requests.get(httpbin('redirect', '1'))
self.assertEqual(r.status_code, 200)
def test_HTTP_302_GET(self):
r = requests.get(httpbin('redirect', '1'), allow_redirects=False)
self.assertEqual(r.status_code, 302)
r = requests.get(httpbin('redirect', '1'), allow_redirects=False)
self.assertEqual(r.status_code, 302)
def test_HTTP_200_OK_GET_WITH_PARAMS(self):
@@ -127,7 +139,7 @@ class RequestsTestSuite(unittest.TestCase):
self.assertEqual(r.status_code, 200)
def test_AUTH_HTTP_200_OK_GET(self):
def test_BASICAUTH_HTTP_200_OK_GET(self):
for service in SERVICES:
@@ -135,14 +147,35 @@ class RequestsTestSuite(unittest.TestCase):
url = service('basic-auth', 'user', 'pass')
r = requests.get(url, auth=auth)
# print r.__dict__
self.assertEqual(r.status_code, 200)
r = requests.get(url)
self.assertEqual(r.status_code, 401)
s = requests.session(auth=auth)
r = s.get(url)
self.assertEqual(r.status_code, 200)
def test_DIGESTAUTH_HTTP_200_OK_GET(self):
for service in SERVICES:
auth = ('digest', 'user', 'pass')
url = service('digest-auth', 'auth', 'user', 'pass')
r = requests.get(url, auth=auth)
self.assertEqual(r.status_code, 200)
r = requests.get(url)
self.assertEqual(r.status_code, 401)
s = requests.session(auth=auth)
r = s.get(url)
self.assertEqual(r.status_code, 200)
def test_POSTBIN_GET_POST_FILES(self):
for service in SERVICES:
@@ -442,7 +475,7 @@ class RequestsTestSuite(unittest.TestCase):
def test_session_HTTP_200_OK_GET(self):
s = Session()
s = requests.session()
r = s.get(httpbin('/get'))
self.assertEqual(r.status_code, 200)
@@ -451,7 +484,7 @@ class RequestsTestSuite(unittest.TestCase):
heads = {'User-agent': 'Mozilla/5.0'}
s = Session()
s = requests.session()
s.headers = heads
# Make 2 requests from Session object, should send header both times
@@ -468,5 +501,33 @@ class RequestsTestSuite(unittest.TestCase):
self.assertEqual(r2.status_code, 200)
def test_session_persistent_params(self):
params = {'a': 'a_test'}
s = requests.session()
s.params = params
# Make 2 requests from Session object, should send header both times
r1 = s.get(httpbin('get'))
assert params['a'] in r1.content
params2 = {'b': 'b_test'}
r2 = s.get(httpbin('get'), params=params2)
assert params['a'] in r2.content
assert params2['b'] in r2.content
params3 = {'b': 'b_test', 'a': None, 'c': 'c_test'}
r3 = s.get(httpbin('get'), params=params3)
assert not params['a'] in r3.content
assert params3['b'] in r3.content
assert params3['c'] in r3.content
if __name__ == '__main__':
unittest.main()