1 # -*- coding: utf-8 -*-
7 This module provides a Session object to manage and persist settings across
8 requests (cookies, auth, proxies).
12 from collections import Mapping
13 from datetime import datetime
15 from .auth import _basic_auth_str
16 from .compat import cookielib, OrderedDict, urljoin, urlparse
17 from .cookies import (
18 cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies)
19 from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT
20 from .hooks import default_hooks, dispatch_hook
21 from .utils import to_key_val_list, default_headers, to_native_string
22 from .exceptions import (
23 TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError)
24 from .packages.urllib3._collections import RecentlyUsedContainer
25 from .structures import CaseInsensitiveDict
27 from .adapters import HTTPAdapter
30 requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies,
34 from .status_codes import codes
36 # formerly defined here, reexposed here for backward compatibility
37 from .models import REDIRECT_STATI
39 REDIRECT_CACHE_SIZE = 1000
42 def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
44 Determines appropriate setting for a given request, taking into account the
45 explicit setting on that request, and the setting in the session. If a
46 setting is a dictionary, they will be merged together using `dict_class`
49 if session_setting is None:
50 return request_setting
52 if request_setting is None:
53 return session_setting
55 # Bypass if not a dictionary (e.g. verify)
57 isinstance(session_setting, Mapping) and
58 isinstance(request_setting, Mapping)
60 return request_setting
62 merged_setting = dict_class(to_key_val_list(session_setting))
63 merged_setting.update(to_key_val_list(request_setting))
65 # Remove keys that are set to None. Extract keys first to avoid altering
66 # the dictionary during iteration.
67 none_keys = [k for (k, v) in merged_setting.items() if v is None]
69 del merged_setting[key]
74 def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
76 Properly merges both requests and session hooks.
78 This is necessary because when request_hooks == {'response': []}, the
79 merge breaks Session hooks entirely.
81 if session_hooks is None or session_hooks.get('response') == []:
84 if request_hooks is None or request_hooks.get('response') == []:
87 return merge_setting(request_hooks, session_hooks, dict_class)
90 class SessionRedirectMixin(object):
91 def resolve_redirects(self, resp, req, stream=False, timeout=None,
92 verify=True, cert=None, proxies=None, **adapter_kwargs):
93 """Receives a Response. Returns a generator of Responses."""
96 hist = [] # keep track of history
98 while resp.is_redirect:
99 prepared_request = req.copy()
102 # Update history and keep track of redirects.
104 new_hist = list(hist)
105 resp.history = new_hist
108 resp.content # Consume socket so it can be released
109 except (ChunkedEncodingError, ContentDecodingError, RuntimeError):
110 resp.raw.read(decode_content=False)
112 if i >= self.max_redirects:
113 raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects, response=resp)
115 # Release the connection back into the pool.
118 url = resp.headers['location']
120 # Handle redirection without scheme (see: RFC 1808 Section 4)
121 if url.startswith('//'):
122 parsed_rurl = urlparse(resp.url)
123 url = '%s:%s' % (parsed_rurl.scheme, url)
125 # The scheme should be lower case...
126 parsed = urlparse(url)
127 url = parsed.geturl()
129 # Facilitate relative 'location' headers, as allowed by RFC 7231.
130 # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
131 # Compliant with RFC3986, we percent encode the url.
132 if not parsed.netloc:
133 url = urljoin(resp.url, requote_uri(url))
135 url = requote_uri(url)
137 prepared_request.url = to_native_string(url)
138 # Cache the url, unless it redirects to itself.
139 if resp.is_permanent_redirect and req.url != prepared_request.url:
140 self.redirect_cache[req.url] = prepared_request.url
142 self.rebuild_method(prepared_request, resp)
144 # https://github.com/kennethreitz/requests/issues/1084
145 if resp.status_code not in (codes.temporary_redirect, codes.permanent_redirect):
146 if 'Content-Length' in prepared_request.headers:
147 del prepared_request.headers['Content-Length']
149 prepared_request.body = None
151 headers = prepared_request.headers
153 del headers['Cookie']
157 # Extract any cookies sent on the response to the cookiejar
158 # in the new request. Because we've mutated our copied prepared
159 # request, use the old one that we haven't yet touched.
160 extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
161 prepared_request._cookies.update(self.cookies)
162 prepared_request.prepare_cookies(prepared_request._cookies)
164 # Rebuild auth and proxy information.
165 proxies = self.rebuild_proxies(prepared_request, proxies)
166 self.rebuild_auth(prepared_request, resp)
168 # Override the original request.
169 req = prepared_request
178 allow_redirects=False,
182 extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
187 def rebuild_auth(self, prepared_request, response):
189 When being redirected we may want to strip authentication from the
190 request to avoid leaking credentials. This method intelligently removes
191 and reapplies authentication where possible to avoid credential loss.
193 headers = prepared_request.headers
194 url = prepared_request.url
196 if 'Authorization' in headers:
197 # If we get redirected to a new host, we should strip out any
198 # authentication headers.
199 original_parsed = urlparse(response.request.url)
200 redirect_parsed = urlparse(url)
202 if (original_parsed.hostname != redirect_parsed.hostname):
203 del headers['Authorization']
205 # .netrc might have more auth for us on our new host.
206 new_auth = get_netrc_auth(url) if self.trust_env else None
207 if new_auth is not None:
208 prepared_request.prepare_auth(new_auth)
212 def rebuild_proxies(self, prepared_request, proxies):
214 This method re-evaluates the proxy configuration by considering the
215 environment variables. If we are redirected to a URL covered by
216 NO_PROXY, we strip the proxy configuration. Otherwise, we set missing
217 proxy keys for this URL (in case they were stripped by a previous
220 This method also replaces the Proxy-Authorization header where
223 headers = prepared_request.headers
224 url = prepared_request.url
225 scheme = urlparse(url).scheme
226 new_proxies = proxies.copy() if proxies is not None else {}
228 if self.trust_env and not should_bypass_proxies(url):
229 environ_proxies = get_environ_proxies(url)
231 proxy = environ_proxies.get(scheme)
234 new_proxies.setdefault(scheme, environ_proxies[scheme])
236 if 'Proxy-Authorization' in headers:
237 del headers['Proxy-Authorization']
240 username, password = get_auth_from_url(new_proxies[scheme])
242 username, password = None, None
244 if username and password:
245 headers['Proxy-Authorization'] = _basic_auth_str(username, password)
249 def rebuild_method(self, prepared_request, response):
250 """When being redirected we may want to change the method of the request
251 based on certain specs or browser behavior.
253 method = prepared_request.method
255 # http://tools.ietf.org/html/rfc7231#section-6.4.4
256 if response.status_code == codes.see_other and method != 'HEAD':
259 # Do what the browsers do, despite standards...
260 # First, turn 302s into GETs.
261 if response.status_code == codes.found and method != 'HEAD':
264 # Second, if a POST is responded to with a 301, turn it into a GET.
265 # This bizarre behaviour is explained in Issue 1704.
266 if response.status_code == codes.moved and method == 'POST':
269 prepared_request.method = method
272 class Session(SessionRedirectMixin):
273 """A Requests session.
275 Provides cookie persistence, connection-pooling, and configuration.
280 >>> s = requests.Session()
281 >>> s.get('http://httpbin.org/get')
284 Or as a context manager::
286 >>> with requests.Session() as s:
287 >>> s.get('http://httpbin.org/get')
292 'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',
293 'cert', 'prefetch', 'adapters', 'stream', 'trust_env',
299 #: A case-insensitive dictionary of headers to be sent on each
300 #: :class:`Request <Request>` sent from this
301 #: :class:`Session <Session>`.
302 self.headers = default_headers()
304 #: Default Authentication tuple or object to attach to
305 #: :class:`Request <Request>`.
308 #: Dictionary mapping protocol or protocol and host to the URL of the proxy
309 #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
310 #: be used on each :class:`Request <Request>`.
313 #: Event-handling hooks.
314 self.hooks = default_hooks()
316 #: Dictionary of querystring data to attach to each
317 #: :class:`Request <Request>`. The dictionary values may be lists for
318 #: representing multivalued query parameters.
321 #: Stream response content default.
324 #: SSL Verification default.
327 #: SSL certificate default.
330 #: Maximum number of redirects allowed. If the request exceeds this
331 #: limit, a :class:`TooManyRedirects` exception is raised.
332 self.max_redirects = DEFAULT_REDIRECT_LIMIT
334 #: Trust environment settings for proxy configuration, default
335 #: authentication and similar.
336 self.trust_env = True
338 #: A CookieJar containing all currently outstanding cookies set on this
339 #: session. By default it is a
340 #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but
341 #: may be any other ``cookielib.CookieJar`` compatible object.
342 self.cookies = cookiejar_from_dict({})
344 # Default connection adapters.
345 self.adapters = OrderedDict()
346 self.mount('https://', HTTPAdapter())
347 self.mount('http://', HTTPAdapter())
349 # Only store 1000 redirects to prevent using infinite memory
350 self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
355 def __exit__(self, *args):
358 def prepare_request(self, request):
359 """Constructs a :class:`PreparedRequest <PreparedRequest>` for
360 transmission and returns it. The :class:`PreparedRequest` has settings
361 merged from the :class:`Request <Request>` instance and those of the
364 :param request: :class:`Request` instance to prepare with this
367 cookies = request.cookies or {}
369 # Bootstrap CookieJar.
370 if not isinstance(cookies, cookielib.CookieJar):
371 cookies = cookiejar_from_dict(cookies)
373 # Merge with session cookies
374 merged_cookies = merge_cookies(
375 merge_cookies(RequestsCookieJar(), self.cookies), cookies)
378 # Set environment's basic authentication if not explicitly set.
380 if self.trust_env and not auth and not self.auth:
381 auth = get_netrc_auth(request.url)
383 p = PreparedRequest()
385 method=request.method.upper(),
390 headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
391 params=merge_setting(request.params, self.params),
392 auth=merge_setting(auth, self.auth),
393 cookies=merged_cookies,
394 hooks=merge_hooks(request.hooks, self.hooks),
398 def request(self, method, url,
406 allow_redirects=True,
413 """Constructs a :class:`Request <Request>`, prepares it and sends it.
414 Returns :class:`Response <Response>` object.
416 :param method: method for the new :class:`Request` object.
417 :param url: URL for the new :class:`Request` object.
418 :param params: (optional) Dictionary or bytes to be sent in the query
419 string for the :class:`Request`.
420 :param data: (optional) Dictionary, bytes, or file-like object to send
421 in the body of the :class:`Request`.
422 :param json: (optional) json to send in the body of the
424 :param headers: (optional) Dictionary of HTTP Headers to send with the
426 :param cookies: (optional) Dict or CookieJar object to send with the
428 :param files: (optional) Dictionary of ``'filename': file-like-objects``
429 for multipart encoding upload.
430 :param auth: (optional) Auth tuple or callable to enable
431 Basic/Digest/Custom HTTP Auth.
432 :param timeout: (optional) How long to wait for the server to send
433 data before giving up, as a float, or a :ref:`(connect timeout,
434 read timeout) <timeouts>` tuple.
435 :type timeout: float or tuple
436 :param allow_redirects: (optional) Set to True by default.
437 :type allow_redirects: bool
438 :param proxies: (optional) Dictionary mapping protocol or protocol and
439 hostname to the URL of the proxy.
440 :param stream: (optional) whether to immediately download the response
441 content. Defaults to ``False``.
442 :param verify: (optional) whether the SSL cert will be verified.
443 A CA_BUNDLE path can also be provided. Defaults to ``True``.
444 :param cert: (optional) if String, path to ssl client cert file (.pem).
445 If Tuple, ('cert', 'key') pair.
446 :rtype: requests.Response
448 # Create the Request.
450 method = method.upper(),
456 params = params or {},
461 prep = self.prepare_request(req)
463 proxies = proxies or {}
465 settings = self.merge_environment_settings(
466 prep.url, proxies, stream, verify, cert
472 'allow_redirects': allow_redirects,
474 send_kwargs.update(settings)
475 resp = self.send(prep, **send_kwargs)
479 def get(self, url, **kwargs):
480 """Sends a GET request. Returns :class:`Response` object.
482 :param url: URL for the new :class:`Request` object.
483 :param \*\*kwargs: Optional arguments that ``request`` takes.
486 kwargs.setdefault('allow_redirects', True)
487 return self.request('GET', url, **kwargs)
489 def options(self, url, **kwargs):
490 """Sends a OPTIONS request. Returns :class:`Response` object.
492 :param url: URL for the new :class:`Request` object.
493 :param \*\*kwargs: Optional arguments that ``request`` takes.
496 kwargs.setdefault('allow_redirects', True)
497 return self.request('OPTIONS', url, **kwargs)
499 def head(self, url, **kwargs):
500 """Sends a HEAD request. Returns :class:`Response` object.
502 :param url: URL for the new :class:`Request` object.
503 :param \*\*kwargs: Optional arguments that ``request`` takes.
506 kwargs.setdefault('allow_redirects', False)
507 return self.request('HEAD', url, **kwargs)
509 def post(self, url, data=None, json=None, **kwargs):
510 """Sends a POST request. Returns :class:`Response` object.
512 :param url: URL for the new :class:`Request` object.
513 :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
514 :param json: (optional) json to send in the body of the :class:`Request`.
515 :param \*\*kwargs: Optional arguments that ``request`` takes.
518 return self.request('POST', url, data=data, json=json, **kwargs)
520 def put(self, url, data=None, **kwargs):
521 """Sends a PUT request. Returns :class:`Response` object.
523 :param url: URL for the new :class:`Request` object.
524 :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
525 :param \*\*kwargs: Optional arguments that ``request`` takes.
528 return self.request('PUT', url, data=data, **kwargs)
530 def patch(self, url, data=None, **kwargs):
531 """Sends a PATCH request. Returns :class:`Response` object.
533 :param url: URL for the new :class:`Request` object.
534 :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
535 :param \*\*kwargs: Optional arguments that ``request`` takes.
538 return self.request('PATCH', url, data=data, **kwargs)
540 def delete(self, url, **kwargs):
541 """Sends a DELETE request. Returns :class:`Response` object.
543 :param url: URL for the new :class:`Request` object.
544 :param \*\*kwargs: Optional arguments that ``request`` takes.
547 return self.request('DELETE', url, **kwargs)
549 def send(self, request, **kwargs):
550 """Send a given PreparedRequest."""
551 # Set defaults that the hooks can utilize to ensure they always have
552 # the correct parameters to reproduce the previous request.
553 kwargs.setdefault('stream', self.stream)
554 kwargs.setdefault('verify', self.verify)
555 kwargs.setdefault('cert', self.cert)
556 kwargs.setdefault('proxies', self.proxies)
558 # It's possible that users might accidentally send a Request object.
559 # Guard against that specific failure case.
560 if isinstance(request, Request):
561 raise ValueError('You can only send PreparedRequests.')
563 # Set up variables needed for resolve_redirects and dispatching of hooks
564 allow_redirects = kwargs.pop('allow_redirects', True)
565 stream = kwargs.get('stream')
566 hooks = request.hooks
568 # Resolve URL in redirect cache, if available.
571 while request.url in self.redirect_cache:
572 checked_urls.add(request.url)
573 new_url = self.redirect_cache.get(request.url)
574 if new_url in checked_urls:
576 request.url = new_url
578 # Get the appropriate adapter to use
579 adapter = self.get_adapter(url=request.url)
581 # Start time (approximately) of the request
582 start = datetime.utcnow()
585 r = adapter.send(request, **kwargs)
587 # Total elapsed time of the request (approximately)
588 r.elapsed = datetime.utcnow() - start
590 # Response manipulation hooks
591 r = dispatch_hook('response', hooks, r, **kwargs)
596 # If the hooks create history then we want those cookies too
597 for resp in r.history:
598 extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
600 extract_cookies_to_jar(self.cookies, request, r.raw)
602 # Redirect resolving generator.
603 gen = self.resolve_redirects(r, request, **kwargs)
605 # Resolve redirects if allowed.
606 history = [resp for resp in gen] if allow_redirects else []
608 # Shuffle things around if there's history.
610 # Insert the first (original) request at the start
612 # Get the last request made
621 def merge_environment_settings(self, url, proxies, stream, verify, cert):
622 """Check the environment and merge it with some settings."""
623 # Gather clues from the surrounding environment.
625 # Set environment's proxies.
626 env_proxies = get_environ_proxies(url) or {}
627 for (k, v) in env_proxies.items():
628 proxies.setdefault(k, v)
630 # Look for requests environment configuration and be compatible
632 if verify is True or verify is None:
633 verify = (os.environ.get('REQUESTS_CA_BUNDLE') or
634 os.environ.get('CURL_CA_BUNDLE'))
636 # Merge all the kwargs.
637 proxies = merge_setting(proxies, self.proxies)
638 stream = merge_setting(stream, self.stream)
639 verify = merge_setting(verify, self.verify)
640 cert = merge_setting(cert, self.cert)
642 return {'verify': verify, 'proxies': proxies, 'stream': stream,
645 def get_adapter(self, url):
646 """Returns the appropriate connection adapter for the given URL."""
647 for (prefix, adapter) in self.adapters.items():
649 if url.lower().startswith(prefix):
652 # Nothing matches :-/
653 raise InvalidSchema("No connection adapters were found for '%s'" % url)
656 """Closes all adapters and as such the session"""
657 for v in self.adapters.values():
660 def mount(self, prefix, adapter):
661 """Registers a connection adapter to a prefix.
663 Adapters are sorted in descending order by key length."""
665 self.adapters[prefix] = adapter
666 keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
668 for key in keys_to_move:
669 self.adapters[key] = self.adapters.pop(key)
671 def __getstate__(self):
672 state = dict((attr, getattr(self, attr, None)) for attr in self.__attrs__)
673 state['redirect_cache'] = dict(self.redirect_cache)
676 def __setstate__(self, state):
677 redirect_cache = state.pop('redirect_cache', {})
678 for attr, value in state.items():
679 setattr(self, attr, value)
681 self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
682 for redirect, to in redirect_cache.items():
683 self.redirect_cache[redirect] = to
687 """Returns a :class:`Session` for context-management."""