| 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 | |
| 75 # ((resp.status_code is codes.see_other)) | |
| 76 while (('location' in resp.headers and resp.status_code in REDIRECT_STAT
I)): | |
| 77 prepared_request = req.copy() | |
| 78 | |
| 79 resp.content # Consume socket so it can be released | |
| 80 | |
| 81 if i >= self.max_redirects: | |
| 82 raise TooManyRedirects('Exceeded %s redirects.' % self.max_redir
ects) | |
| 83 | |
| 84 # Release the connection back into the pool. | |
| 85 resp.close() | |
| 86 | |
| 87 url = resp.headers['location'] | |
| 88 method = req.method | |
| 89 | |
| 90 # Handle redirection without scheme (see: RFC 1808 Section 4) | |
| 91 if url.startswith('//'): | |
| 92 parsed_rurl = urlparse(resp.url) | |
| 93 url = '%s:%s' % (parsed_rurl.scheme, url) | |
| 94 | |
| 95 # The scheme should be lower case... | |
| 96 if '://' in url: | |
| 97 scheme, uri = url.split('://', 1) | |
| 98 url = '%s://%s' % (scheme.lower(), uri) | |
| 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 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 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 persistence, 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 #: A CookieJar containing all currently outstanding cookies set on this | |
| 212 #: session. By default it is a | |
| 213 #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but | |
| 214 #: may be any other ``cookielib.CookieJar`` compatible object. | |
| 215 self.cookies = cookiejar_from_dict({}) | |
| 216 | |
| 217 # Default connection adapters. | |
| 218 self.adapters = OrderedDict() | |
| 219 self.mount('https://', HTTPAdapter()) | |
| 220 self.mount('http://', HTTPAdapter()) | |
| 221 | |
| 222 def __enter__(self): | |
| 223 return self | |
| 224 | |
| 225 def __exit__(self, *args): | |
| 226 self.close() | |
| 227 | |
| 228 def prepare_request(self, request): | |
| 229 """Constructs a :class:`PreparedRequest <PreparedRequest>` for | |
| 230 transmission and returns it. The :class:`PreparedRequest` has settings | |
| 231 merged from the :class:`Request <Request>` instance and those of the | |
| 232 :class:`Session`. | |
| 233 | |
| 234 :param request: :class:`Request` instance to prepare with this | |
| 235 session's settings. | |
| 236 """ | |
| 237 cookies = request.cookies or {} | |
| 238 | |
| 239 # Bootstrap CookieJar. | |
| 240 if not isinstance(cookies, cookielib.CookieJar): | |
| 241 cookies = cookiejar_from_dict(cookies) | |
| 242 | |
| 243 # Merge with session cookies | |
| 244 merged_cookies = RequestsCookieJar() | |
| 245 merged_cookies.update(self.cookies) | |
| 246 merged_cookies.update(cookies) | |
| 247 | |
| 248 | |
| 249 # Set environment's basic authentication if not explicitly set. | |
| 250 auth = request.auth | |
| 251 if self.trust_env and not auth and not self.auth: | |
| 252 auth = get_netrc_auth(request.url) | |
| 253 | |
| 254 p = PreparedRequest() | |
| 255 p.prepare( | |
| 256 method=request.method.upper(), | |
| 257 url=request.url, | |
| 258 files=request.files, | |
| 259 data=request.data, | |
| 260 headers=merge_setting(request.headers, self.headers, dict_class=Case
InsensitiveDict), | |
| 261 params=merge_setting(request.params, self.params), | |
| 262 auth=merge_setting(auth, self.auth), | |
| 263 cookies=merged_cookies, | |
| 264 hooks=merge_setting(request.hooks, self.hooks), | |
| 265 ) | |
| 266 return p | |
| 267 | |
| 268 def request(self, method, url, | |
| 269 params=None, | |
| 270 data=None, | |
| 271 headers=None, | |
| 272 cookies=None, | |
| 273 files=None, | |
| 274 auth=None, | |
| 275 timeout=None, | |
| 276 allow_redirects=True, | |
| 277 proxies=None, | |
| 278 hooks=None, | |
| 279 stream=None, | |
| 280 verify=None, | |
| 281 cert=None): | |
| 282 """Constructs a :class:`Request <Request>`, prepares it and sends it. | |
| 283 Returns :class:`Response <Response>` object. | |
| 284 | |
| 285 :param method: method for the new :class:`Request` object. | |
| 286 :param url: URL for the new :class:`Request` object. | |
| 287 :param params: (optional) Dictionary or bytes to be sent in the query | |
| 288 string for the :class:`Request`. | |
| 289 :param data: (optional) Dictionary or bytes to send in the body of the | |
| 290 :class:`Request`. | |
| 291 :param headers: (optional) Dictionary of HTTP Headers to send with the | |
| 292 :class:`Request`. | |
| 293 :param cookies: (optional) Dict or CookieJar object to send with the | |
| 294 :class:`Request`. | |
| 295 :param files: (optional) Dictionary of 'filename': file-like-objects | |
| 296 for multipart encoding upload. | |
| 297 :param auth: (optional) Auth tuple or callable to enable | |
| 298 Basic/Digest/Custom HTTP Auth. | |
| 299 :param timeout: (optional) Float describing the timeout of the | |
| 300 request. | |
| 301 :param allow_redirects: (optional) Boolean. Set to True by default. | |
| 302 :param proxies: (optional) Dictionary mapping protocol to the URL of | |
| 303 the proxy. | |
| 304 :param stream: (optional) whether to immediately download the response | |
| 305 content. Defaults to ``False``. | |
| 306 :param verify: (optional) if ``True``, the SSL cert will be verified. | |
| 307 A CA_BUNDLE path can also be provided. | |
| 308 :param cert: (optional) if String, path to ssl client cert file (.pem). | |
| 309 If Tuple, ('cert', 'key') pair. | |
| 310 """ | |
| 311 # Create the Request. | |
| 312 req = Request( | |
| 313 method = method.upper(), | |
| 314 url = url, | |
| 315 headers = headers, | |
| 316 files = files, | |
| 317 data = data or {}, | |
| 318 params = params or {}, | |
| 319 auth = auth, | |
| 320 cookies = cookies, | |
| 321 hooks = hooks, | |
| 322 ) | |
| 323 prep = self.prepare_request(req) | |
| 324 | |
| 325 proxies = proxies or {} | |
| 326 | |
| 327 # Gather clues from the surrounding environment. | |
| 328 if self.trust_env: | |
| 329 # Set environment's proxies. | |
| 330 env_proxies = get_environ_proxies(url) or {} | |
| 331 for (k, v) in env_proxies.items(): | |
| 332 proxies.setdefault(k, v) | |
| 333 | |
| 334 # Look for configuration. | |
| 335 if not verify and verify is not False: | |
| 336 verify = os.environ.get('REQUESTS_CA_BUNDLE') | |
| 337 | |
| 338 # Curl compatibility. | |
| 339 if not verify and verify is not False: | |
| 340 verify = os.environ.get('CURL_CA_BUNDLE') | |
| 341 | |
| 342 # Merge all the kwargs. | |
| 343 proxies = merge_setting(proxies, self.proxies) | |
| 344 stream = merge_setting(stream, self.stream) | |
| 345 verify = merge_setting(verify, self.verify) | |
| 346 cert = merge_setting(cert, self.cert) | |
| 347 | |
| 348 # Send the request. | |
| 349 send_kwargs = { | |
| 350 'stream': stream, | |
| 351 'timeout': timeout, | |
| 352 'verify': verify, | |
| 353 'cert': cert, | |
| 354 'proxies': proxies, | |
| 355 'allow_redirects': allow_redirects, | |
| 356 } | |
| 357 resp = self.send(prep, **send_kwargs) | |
| 358 | |
| 359 return resp | |
| 360 | |
| 361 def get(self, url, **kwargs): | |
| 362 """Sends a GET request. Returns :class:`Response` object. | |
| 363 | |
| 364 :param url: URL for the new :class:`Request` object. | |
| 365 :param \*\*kwargs: Optional arguments that ``request`` takes. | |
| 366 """ | |
| 367 | |
| 368 kwargs.setdefault('allow_redirects', True) | |
| 369 return self.request('GET', url, **kwargs) | |
| 370 | |
| 371 def options(self, url, **kwargs): | |
| 372 """Sends a OPTIONS request. Returns :class:`Response` object. | |
| 373 | |
| 374 :param url: URL for the new :class:`Request` object. | |
| 375 :param \*\*kwargs: Optional arguments that ``request`` takes. | |
| 376 """ | |
| 377 | |
| 378 kwargs.setdefault('allow_redirects', True) | |
| 379 return self.request('OPTIONS', url, **kwargs) | |
| 380 | |
| 381 def head(self, url, **kwargs): | |
| 382 """Sends a HEAD request. Returns :class:`Response` object. | |
| 383 | |
| 384 :param url: URL for the new :class:`Request` object. | |
| 385 :param \*\*kwargs: Optional arguments that ``request`` takes. | |
| 386 """ | |
| 387 | |
| 388 kwargs.setdefault('allow_redirects', False) | |
| 389 return self.request('HEAD', url, **kwargs) | |
| 390 | |
| 391 def post(self, url, data=None, **kwargs): | |
| 392 """Sends a POST request. Returns :class:`Response` object. | |
| 393 | |
| 394 :param url: URL for the new :class:`Request` object. | |
| 395 :param data: (optional) Dictionary, bytes, or file-like object to send i
n the body of the :class:`Request`. | |
| 396 :param \*\*kwargs: Optional arguments that ``request`` takes. | |
| 397 """ | |
| 398 | |
| 399 return self.request('POST', url, data=data, **kwargs) | |
| 400 | |
| 401 def put(self, url, data=None, **kwargs): | |
| 402 """Sends a PUT request. Returns :class:`Response` object. | |
| 403 | |
| 404 :param url: URL for the new :class:`Request` object. | |
| 405 :param data: (optional) Dictionary, bytes, or file-like object to send i
n the body of the :class:`Request`. | |
| 406 :param \*\*kwargs: Optional arguments that ``request`` takes. | |
| 407 """ | |
| 408 | |
| 409 return self.request('PUT', url, data=data, **kwargs) | |
| 410 | |
| 411 def patch(self, url, data=None, **kwargs): | |
| 412 """Sends a PATCH request. Returns :class:`Response` object. | |
| 413 | |
| 414 :param url: URL for the new :class:`Request` object. | |
| 415 :param data: (optional) Dictionary, bytes, or file-like object to send i
n the body of the :class:`Request`. | |
| 416 :param \*\*kwargs: Optional arguments that ``request`` takes. | |
| 417 """ | |
| 418 | |
| 419 return self.request('PATCH', url, data=data, **kwargs) | |
| 420 | |
| 421 def delete(self, url, **kwargs): | |
| 422 """Sends a DELETE request. Returns :class:`Response` object. | |
| 423 | |
| 424 :param url: URL for the new :class:`Request` object. | |
| 425 :param \*\*kwargs: Optional arguments that ``request`` takes. | |
| 426 """ | |
| 427 | |
| 428 return self.request('DELETE', url, **kwargs) | |
| 429 | |
| 430 def send(self, request, **kwargs): | |
| 431 """Send a given PreparedRequest.""" | |
| 432 # Set defaults that the hooks can utilize to ensure they always have | |
| 433 # the correct parameters to reproduce the previous request. | |
| 434 kwargs.setdefault('stream', self.stream) | |
| 435 kwargs.setdefault('verify', self.verify) | |
| 436 kwargs.setdefault('cert', self.cert) | |
| 437 kwargs.setdefault('proxies', self.proxies) | |
| 438 | |
| 439 # It's possible that users might accidentally send a Request object. | |
| 440 # Guard against that specific failure case. | |
| 441 if not isinstance(request, PreparedRequest): | |
| 442 raise ValueError('You can only send PreparedRequests.') | |
| 443 | |
| 444 # Set up variables needed for resolve_redirects and dispatching of | |
| 445 # hooks | |
| 446 allow_redirects = kwargs.pop('allow_redirects', True) | |
| 447 stream = kwargs.get('stream') | |
| 448 timeout = kwargs.get('timeout') | |
| 449 verify = kwargs.get('verify') | |
| 450 cert = kwargs.get('cert') | |
| 451 proxies = kwargs.get('proxies') | |
| 452 hooks = request.hooks | |
| 453 | |
| 454 # Get the appropriate adapter to use | |
| 455 adapter = self.get_adapter(url=request.url) | |
| 456 | |
| 457 # Start time (approximately) of the request | |
| 458 start = datetime.utcnow() | |
| 459 # Send the request | |
| 460 r = adapter.send(request, **kwargs) | |
| 461 # Total elapsed time of the request (approximately) | |
| 462 r.elapsed = datetime.utcnow() - start | |
| 463 | |
| 464 # Response manipulation hooks | |
| 465 r = dispatch_hook('response', hooks, r, **kwargs) | |
| 466 | |
| 467 # Persist cookies | |
| 468 if r.history: | |
| 469 # If the hooks create history then we want those cookies too | |
| 470 for resp in r.history: | |
| 471 extract_cookies_to_jar(self.cookies, resp.request, resp.raw) | |
| 472 extract_cookies_to_jar(self.cookies, request, r.raw) | |
| 473 | |
| 474 # Redirect resolving generator. | |
| 475 gen = self.resolve_redirects(r, request, stream=stream, | |
| 476 timeout=timeout, verify=verify, cert=cert, | |
| 477 proxies=proxies) | |
| 478 | |
| 479 # Resolve redirects if allowed. | |
| 480 history = [resp for resp in gen] if allow_redirects else [] | |
| 481 | |
| 482 # Shuffle things around if there's history. | |
| 483 if history: | |
| 484 # Insert the first (original) request at the start | |
| 485 history.insert(0, r) | |
| 486 # Get the last request made | |
| 487 r = history.pop() | |
| 488 r.history = tuple(history) | |
| 489 | |
| 490 return r | |
| 491 | |
| 492 def get_adapter(self, url): | |
| 493 """Returns the appropriate connnection adapter for the given URL.""" | |
| 494 for (prefix, adapter) in self.adapters.items(): | |
| 495 | |
| 496 if url.lower().startswith(prefix): | |
| 497 return adapter | |
| 498 | |
| 499 # Nothing matches :-/ | |
| 500 raise InvalidSchema("No connection adapters were found for '%s'" % url) | |
| 501 | |
| 502 def close(self): | |
| 503 """Closes all adapters and as such the session""" | |
| 504 for v in self.adapters.values(): | |
| 505 v.close() | |
| 506 | |
| 507 def mount(self, prefix, adapter): | |
| 508 """Registers a connection adapter to a prefix. | |
| 509 | |
| 510 Adapters are sorted in descending order by key length.""" | |
| 511 self.adapters[prefix] = adapter | |
| 512 keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] | |
| 513 for key in keys_to_move: | |
| 514 self.adapters[key] = self.adapters.pop(key) | |
| 515 | |
| 516 def __getstate__(self): | |
| 517 return dict((attr, getattr(self, attr, None)) for attr in self.__attrs__
) | |
| 518 | |
| 519 def __setstate__(self, state): | |
| 520 for attr, value in state.items(): | |
| 521 setattr(self, attr, value) | |
| 522 | |
| 523 | |
| 524 def session(): | |
| 525 """Returns a :class:`Session` for context-management.""" | |
| 526 | |
| 527 return Session() | |
| OLD | NEW |