OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 |
| 3 """ |
| 4 requests.session |
| 5 ~~~~~~~~~~~~~~~~ |
| 6 |
| 7 This module provides a Session object to manage and persist settings across |
| 8 requests (cookies, auth, proxies). |
| 9 |
| 10 """ |
| 11 import os |
| 12 from collections import Mapping |
| 13 from datetime import datetime |
| 14 |
| 15 from .compat import cookielib, OrderedDict, urljoin, urlparse |
| 16 from .cookies import cookiejar_from_dict, extract_cookies_to_jar, RequestsCookie
Jar |
| 17 from .models import Request, PreparedRequest |
| 18 from .hooks import default_hooks, dispatch_hook |
| 19 from .utils import to_key_val_list, default_headers |
| 20 from .exceptions import TooManyRedirects, InvalidSchema |
| 21 from .structures import CaseInsensitiveDict |
| 22 |
| 23 from .adapters import HTTPAdapter |
| 24 |
| 25 from .utils import requote_uri, get_environ_proxies, get_netrc_auth |
| 26 |
| 27 from .status_codes import codes |
| 28 REDIRECT_STATI = ( |
| 29 codes.moved, # 301 |
| 30 codes.found, # 302 |
| 31 codes.other, # 303 |
| 32 codes.temporary_moved, # 307 |
| 33 ) |
| 34 DEFAULT_REDIRECT_LIMIT = 30 |
| 35 |
| 36 |
| 37 def merge_setting(request_setting, session_setting, dict_class=OrderedDict): |
| 38 """ |
| 39 Determines appropriate setting for a given request, taking into account the |
| 40 explicit setting on that request, and the setting in the session. If a |
| 41 setting is a dictionary, they will be merged together using `dict_class` |
| 42 """ |
| 43 |
| 44 if session_setting is None: |
| 45 return request_setting |
| 46 |
| 47 if request_setting is None: |
| 48 return session_setting |
| 49 |
| 50 # Bypass if not a dictionary (e.g. verify) |
| 51 if not ( |
| 52 isinstance(session_setting, Mapping) and |
| 53 isinstance(request_setting, Mapping) |
| 54 ): |
| 55 return request_setting |
| 56 |
| 57 merged_setting = dict_class(to_key_val_list(session_setting)) |
| 58 merged_setting.update(to_key_val_list(request_setting)) |
| 59 |
| 60 # Remove keys that are set to None. |
| 61 for (k, v) in request_setting.items(): |
| 62 if v is None: |
| 63 del merged_setting[k] |
| 64 |
| 65 return merged_setting |
| 66 |
| 67 |
| 68 class SessionRedirectMixin(object): |
| 69 def resolve_redirects(self, resp, req, stream=False, timeout=None, |
| 70 verify=True, cert=None, proxies=None): |
| 71 """Receives a Response. Returns a generator of Responses.""" |
| 72 |
| 73 i = 0 |
| 74 prepared_request = PreparedRequest() |
| 75 prepared_request.body = req.body |
| 76 prepared_request.headers = req.headers.copy() |
| 77 prepared_request.hooks = req.hooks |
| 78 prepared_request.method = req.method |
| 79 prepared_request.url = req.url |
| 80 |
| 81 # ((resp.status_code is codes.see_other)) |
| 82 while (('location' in resp.headers and resp.status_code in REDIRECT_STAT
I)): |
| 83 |
| 84 resp.content # Consume socket so it can be released |
| 85 |
| 86 if i >= self.max_redirects: |
| 87 raise TooManyRedirects('Exceeded %s redirects.' % self.max_redir
ects) |
| 88 |
| 89 # Release the connection back into the pool. |
| 90 resp.close() |
| 91 |
| 92 url = resp.headers['location'] |
| 93 method = prepared_request.method |
| 94 |
| 95 # Handle redirection without scheme (see: RFC 1808 Section 4) |
| 96 if url.startswith('//'): |
| 97 parsed_rurl = urlparse(resp.url) |
| 98 url = '%s:%s' % (parsed_rurl.scheme, url) |
| 99 |
| 100 # Facilitate non-RFC2616-compliant 'location' headers |
| 101 # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/re
source') |
| 102 # Compliant with RFC3986, we percent encode the url. |
| 103 if not urlparse(url).netloc: |
| 104 url = urljoin(resp.url, requote_uri(url)) |
| 105 else: |
| 106 url = requote_uri(url) |
| 107 |
| 108 prepared_request.url = url |
| 109 |
| 110 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 |
| 111 if (resp.status_code == codes.see_other and |
| 112 prepared_request.method != 'HEAD'): |
| 113 method = 'GET' |
| 114 |
| 115 # Do what the browsers do, despite standards... |
| 116 if (resp.status_code in (codes.moved, codes.found) and |
| 117 prepared_request.method not in ('GET', 'HEAD')): |
| 118 method = 'GET' |
| 119 |
| 120 prepared_request.method = method |
| 121 |
| 122 # https://github.com/kennethreitz/requests/issues/1084 |
| 123 if resp.status_code not in (codes.temporary, codes.resume): |
| 124 if 'Content-Length' in prepared_request.headers: |
| 125 del prepared_request.headers['Content-Length'] |
| 126 |
| 127 prepared_request.body = None |
| 128 |
| 129 headers = prepared_request.headers |
| 130 try: |
| 131 del headers['Cookie'] |
| 132 except KeyError: |
| 133 pass |
| 134 |
| 135 prepared_request.prepare_cookies(self.cookies) |
| 136 |
| 137 resp = self.send( |
| 138 prepared_request, |
| 139 stream=stream, |
| 140 timeout=timeout, |
| 141 verify=verify, |
| 142 cert=cert, |
| 143 proxies=proxies, |
| 144 allow_redirects=False, |
| 145 ) |
| 146 |
| 147 extract_cookies_to_jar(self.cookies, prepared_request, resp.raw) |
| 148 |
| 149 i += 1 |
| 150 yield resp |
| 151 |
| 152 |
| 153 class Session(SessionRedirectMixin): |
| 154 """A Requests session. |
| 155 |
| 156 Provides cookie persistience, connection-pooling, and configuration. |
| 157 |
| 158 Basic Usage:: |
| 159 |
| 160 >>> import requests |
| 161 >>> s = requests.Session() |
| 162 >>> s.get('http://httpbin.org/get') |
| 163 200 |
| 164 """ |
| 165 |
| 166 __attrs__ = [ |
| 167 'headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks', |
| 168 'params', 'verify', 'cert', 'prefetch', 'adapters', 'stream', |
| 169 'trust_env', 'max_redirects'] |
| 170 |
| 171 def __init__(self): |
| 172 |
| 173 #: A case-insensitive dictionary of headers to be sent on each |
| 174 #: :class:`Request <Request>` sent from this |
| 175 #: :class:`Session <Session>`. |
| 176 self.headers = default_headers() |
| 177 |
| 178 #: Default Authentication tuple or object to attach to |
| 179 #: :class:`Request <Request>`. |
| 180 self.auth = None |
| 181 |
| 182 #: Dictionary mapping protocol to the URL of the proxy (e.g. |
| 183 #: {'http': 'foo.bar:3128'}) to be used on each |
| 184 #: :class:`Request <Request>`. |
| 185 self.proxies = {} |
| 186 |
| 187 #: Event-handling hooks. |
| 188 self.hooks = default_hooks() |
| 189 |
| 190 #: Dictionary of querystring data to attach to each |
| 191 #: :class:`Request <Request>`. The dictionary values may be lists for |
| 192 #: representing multivalued query parameters. |
| 193 self.params = {} |
| 194 |
| 195 #: Stream response content default. |
| 196 self.stream = False |
| 197 |
| 198 #: SSL Verification default. |
| 199 self.verify = True |
| 200 |
| 201 #: SSL certificate default. |
| 202 self.cert = None |
| 203 |
| 204 #: Maximum number of redirects allowed. If the request exceeds this |
| 205 #: limit, a :class:`TooManyRedirects` exception is raised. |
| 206 self.max_redirects = DEFAULT_REDIRECT_LIMIT |
| 207 |
| 208 #: Should we trust the environment? |
| 209 self.trust_env = True |
| 210 |
| 211 # Set up a CookieJar to be used by default |
| 212 self.cookies = cookiejar_from_dict({}) |
| 213 |
| 214 # Default connection adapters. |
| 215 self.adapters = OrderedDict() |
| 216 self.mount('https://', HTTPAdapter()) |
| 217 self.mount('http://', HTTPAdapter()) |
| 218 |
| 219 def __enter__(self): |
| 220 return self |
| 221 |
| 222 def __exit__(self, *args): |
| 223 self.close() |
| 224 |
| 225 def request(self, method, url, |
| 226 params=None, |
| 227 data=None, |
| 228 headers=None, |
| 229 cookies=None, |
| 230 files=None, |
| 231 auth=None, |
| 232 timeout=None, |
| 233 allow_redirects=True, |
| 234 proxies=None, |
| 235 hooks=None, |
| 236 stream=None, |
| 237 verify=None, |
| 238 cert=None): |
| 239 """Constructs a :class:`Request <Request>`, prepares it and sends it. |
| 240 Returns :class:`Response <Response>` object. |
| 241 |
| 242 :param method: method for the new :class:`Request` object. |
| 243 :param url: URL for the new :class:`Request` object. |
| 244 :param params: (optional) Dictionary or bytes to be sent in the query |
| 245 string for the :class:`Request`. |
| 246 :param data: (optional) Dictionary or bytes to send in the body of the |
| 247 :class:`Request`. |
| 248 :param headers: (optional) Dictionary of HTTP Headers to send with the |
| 249 :class:`Request`. |
| 250 :param cookies: (optional) Dict or CookieJar object to send with the |
| 251 :class:`Request`. |
| 252 :param files: (optional) Dictionary of 'filename': file-like-objects |
| 253 for multipart encoding upload. |
| 254 :param auth: (optional) Auth tuple or callable to enable |
| 255 Basic/Digest/Custom HTTP Auth. |
| 256 :param timeout: (optional) Float describing the timeout of the |
| 257 request. |
| 258 :param allow_redirects: (optional) Boolean. Set to True by default. |
| 259 :param proxies: (optional) Dictionary mapping protocol to the URL of |
| 260 the proxy. |
| 261 :param stream: (optional) whether to immediately download the response |
| 262 content. Defaults to ``False``. |
| 263 :param verify: (optional) if ``True``, the SSL cert will be verified. |
| 264 A CA_BUNDLE path can also be provided. |
| 265 :param cert: (optional) if String, path to ssl client cert file (.pem). |
| 266 If Tuple, ('cert', 'key') pair. |
| 267 """ |
| 268 |
| 269 cookies = cookies or {} |
| 270 proxies = proxies or {} |
| 271 |
| 272 # Bootstrap CookieJar. |
| 273 if not isinstance(cookies, cookielib.CookieJar): |
| 274 cookies = cookiejar_from_dict(cookies) |
| 275 |
| 276 # Merge with session cookies |
| 277 merged_cookies = RequestsCookieJar() |
| 278 merged_cookies.update(self.cookies) |
| 279 merged_cookies.update(cookies) |
| 280 cookies = merged_cookies |
| 281 |
| 282 # Gather clues from the surrounding environment. |
| 283 if self.trust_env: |
| 284 # Set environment's proxies. |
| 285 env_proxies = get_environ_proxies(url) or {} |
| 286 for (k, v) in env_proxies.items(): |
| 287 proxies.setdefault(k, v) |
| 288 |
| 289 # Set environment's basic authentication. |
| 290 if not auth: |
| 291 auth = get_netrc_auth(url) |
| 292 |
| 293 # Look for configuration. |
| 294 if not verify and verify is not False: |
| 295 verify = os.environ.get('REQUESTS_CA_BUNDLE') |
| 296 |
| 297 # Curl compatibility. |
| 298 if not verify and verify is not False: |
| 299 verify = os.environ.get('CURL_CA_BUNDLE') |
| 300 |
| 301 # Merge all the kwargs. |
| 302 params = merge_setting(params, self.params) |
| 303 headers = merge_setting(headers, self.headers, dict_class=CaseInsensitiv
eDict) |
| 304 auth = merge_setting(auth, self.auth) |
| 305 proxies = merge_setting(proxies, self.proxies) |
| 306 hooks = merge_setting(hooks, self.hooks) |
| 307 stream = merge_setting(stream, self.stream) |
| 308 verify = merge_setting(verify, self.verify) |
| 309 cert = merge_setting(cert, self.cert) |
| 310 |
| 311 # Create the Request. |
| 312 req = Request() |
| 313 req.method = method.upper() |
| 314 req.url = url |
| 315 req.headers = headers |
| 316 req.files = files |
| 317 req.data = data |
| 318 req.params = params |
| 319 req.auth = auth |
| 320 req.cookies = cookies |
| 321 req.hooks = hooks |
| 322 |
| 323 # Prepare the Request. |
| 324 prep = req.prepare() |
| 325 |
| 326 # Send the request. |
| 327 send_kwargs = { |
| 328 'stream': stream, |
| 329 'timeout': timeout, |
| 330 'verify': verify, |
| 331 'cert': cert, |
| 332 'proxies': proxies, |
| 333 'allow_redirects': allow_redirects, |
| 334 } |
| 335 resp = self.send(prep, **send_kwargs) |
| 336 |
| 337 return resp |
| 338 |
| 339 def get(self, url, **kwargs): |
| 340 """Sends a GET request. Returns :class:`Response` object. |
| 341 |
| 342 :param url: URL for the new :class:`Request` object. |
| 343 :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 344 """ |
| 345 |
| 346 kwargs.setdefault('allow_redirects', True) |
| 347 return self.request('GET', url, **kwargs) |
| 348 |
| 349 def options(self, url, **kwargs): |
| 350 """Sends a OPTIONS request. Returns :class:`Response` object. |
| 351 |
| 352 :param url: URL for the new :class:`Request` object. |
| 353 :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 354 """ |
| 355 |
| 356 kwargs.setdefault('allow_redirects', True) |
| 357 return self.request('OPTIONS', url, **kwargs) |
| 358 |
| 359 def head(self, url, **kwargs): |
| 360 """Sends a HEAD request. Returns :class:`Response` object. |
| 361 |
| 362 :param url: URL for the new :class:`Request` object. |
| 363 :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 364 """ |
| 365 |
| 366 kwargs.setdefault('allow_redirects', False) |
| 367 return self.request('HEAD', url, **kwargs) |
| 368 |
| 369 def post(self, url, data=None, **kwargs): |
| 370 """Sends a POST request. Returns :class:`Response` object. |
| 371 |
| 372 :param url: URL for the new :class:`Request` object. |
| 373 :param data: (optional) Dictionary, bytes, or file-like object to send i
n the body of the :class:`Request`. |
| 374 :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 375 """ |
| 376 |
| 377 return self.request('POST', url, data=data, **kwargs) |
| 378 |
| 379 def put(self, url, data=None, **kwargs): |
| 380 """Sends a PUT request. Returns :class:`Response` object. |
| 381 |
| 382 :param url: URL for the new :class:`Request` object. |
| 383 :param data: (optional) Dictionary, bytes, or file-like object to send i
n the body of the :class:`Request`. |
| 384 :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 385 """ |
| 386 |
| 387 return self.request('PUT', url, data=data, **kwargs) |
| 388 |
| 389 def patch(self, url, data=None, **kwargs): |
| 390 """Sends a PATCH request. Returns :class:`Response` object. |
| 391 |
| 392 :param url: URL for the new :class:`Request` object. |
| 393 :param data: (optional) Dictionary, bytes, or file-like object to send i
n the body of the :class:`Request`. |
| 394 :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 395 """ |
| 396 |
| 397 return self.request('PATCH', url, data=data, **kwargs) |
| 398 |
| 399 def delete(self, url, **kwargs): |
| 400 """Sends a DELETE request. Returns :class:`Response` object. |
| 401 |
| 402 :param url: URL for the new :class:`Request` object. |
| 403 :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 404 """ |
| 405 |
| 406 return self.request('DELETE', url, **kwargs) |
| 407 |
| 408 def send(self, request, **kwargs): |
| 409 """Send a given PreparedRequest.""" |
| 410 # Set defaults that the hooks can utilize to ensure they always have |
| 411 # the correct parameters to reproduce the previous request. |
| 412 kwargs.setdefault('stream', self.stream) |
| 413 kwargs.setdefault('verify', self.verify) |
| 414 kwargs.setdefault('cert', self.cert) |
| 415 kwargs.setdefault('proxies', self.proxies) |
| 416 |
| 417 # It's possible that users might accidentally send a Request object. |
| 418 # Guard against that specific failure case. |
| 419 if getattr(request, 'prepare', None): |
| 420 raise ValueError('You can only send PreparedRequests.') |
| 421 |
| 422 # Set up variables needed for resolve_redirects and dispatching of |
| 423 # hooks |
| 424 allow_redirects = kwargs.pop('allow_redirects', True) |
| 425 stream = kwargs.get('stream') |
| 426 timeout = kwargs.get('timeout') |
| 427 verify = kwargs.get('verify') |
| 428 cert = kwargs.get('cert') |
| 429 proxies = kwargs.get('proxies') |
| 430 hooks = request.hooks |
| 431 |
| 432 # Get the appropriate adapter to use |
| 433 adapter = self.get_adapter(url=request.url) |
| 434 |
| 435 # Start time (approximately) of the request |
| 436 start = datetime.utcnow() |
| 437 # Send the request |
| 438 r = adapter.send(request, **kwargs) |
| 439 # Total elapsed time of the request (approximately) |
| 440 r.elapsed = datetime.utcnow() - start |
| 441 |
| 442 # Response manipulation hooks |
| 443 r = dispatch_hook('response', hooks, r, **kwargs) |
| 444 |
| 445 # Persist cookies |
| 446 extract_cookies_to_jar(self.cookies, request, r.raw) |
| 447 |
| 448 # Redirect resolving generator. |
| 449 gen = self.resolve_redirects(r, request, stream=stream, |
| 450 timeout=timeout, verify=verify, cert=cert, |
| 451 proxies=proxies) |
| 452 |
| 453 # Resolve redirects if allowed. |
| 454 history = [resp for resp in gen] if allow_redirects else [] |
| 455 |
| 456 # Shuffle things around if there's history. |
| 457 if history: |
| 458 # Insert the first (original) request at the start |
| 459 history.insert(0, r) |
| 460 # Get the last request made |
| 461 r = history.pop() |
| 462 r.history = tuple(history) |
| 463 |
| 464 return r |
| 465 |
| 466 def get_adapter(self, url): |
| 467 """Returns the appropriate connnection adapter for the given URL.""" |
| 468 for (prefix, adapter) in self.adapters.items(): |
| 469 |
| 470 if url.startswith(prefix): |
| 471 return adapter |
| 472 |
| 473 # Nothing matches :-/ |
| 474 raise InvalidSchema("No connection adapters were found for '%s'" % url) |
| 475 |
| 476 def close(self): |
| 477 """Closes all adapters and as such the session""" |
| 478 for _, v in self.adapters.items(): |
| 479 v.close() |
| 480 |
| 481 def mount(self, prefix, adapter): |
| 482 """Registers a connection adapter to a prefix. |
| 483 |
| 484 Adapters are sorted in descending order by key length.""" |
| 485 self.adapters[prefix] = adapter |
| 486 keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] |
| 487 for key in keys_to_move: |
| 488 self.adapters[key] = self.adapters.pop(key) |
| 489 |
| 490 def __getstate__(self): |
| 491 return dict((attr, getattr(self, attr, None)) for attr in self.__attrs__
) |
| 492 |
| 493 def __setstate__(self, state): |
| 494 for attr, value in state.items(): |
| 495 setattr(self, attr, value) |
| 496 |
| 497 |
| 498 def session(): |
| 499 """Returns a :class:`Session` for context-management.""" |
| 500 |
| 501 return Session() |
OLD | NEW |