45be9733e56d8c6c4f34915d26e0c741f795dcf2
[sdc/sdc-distribution-client.git] /
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 .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
26
27 from .adapters import HTTPAdapter
28
29 from .utils import (
30     requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies,
31     get_auth_from_url
32 )
33
34 from .status_codes import codes
35
36 # formerly defined here, reexposed here for backward compatibility
37 from .models import REDIRECT_STATI
38
39 REDIRECT_CACHE_SIZE = 1000
40
41
42 def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
43     """
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`
47     """
48
49     if session_setting is None:
50         return request_setting
51
52     if request_setting is None:
53         return session_setting
54
55     # Bypass if not a dictionary (e.g. verify)
56     if not (
57             isinstance(session_setting, Mapping) and
58             isinstance(request_setting, Mapping)
59     ):
60         return request_setting
61
62     merged_setting = dict_class(to_key_val_list(session_setting))
63     merged_setting.update(to_key_val_list(request_setting))
64
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]
68     for key in none_keys:
69         del merged_setting[key]
70
71     return merged_setting
72
73
74 def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
75     """
76     Properly merges both requests and session hooks.
77
78     This is necessary because when request_hooks == {'response': []}, the
79     merge breaks Session hooks entirely.
80     """
81     if session_hooks is None or session_hooks.get('response') == []:
82         return request_hooks
83
84     if request_hooks is None or request_hooks.get('response') == []:
85         return session_hooks
86
87     return merge_setting(request_hooks, session_hooks, dict_class)
88
89
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."""
94
95         i = 0
96         hist = [] # keep track of history
97
98         while resp.is_redirect:
99             prepared_request = req.copy()
100
101             if i > 0:
102                 # Update history and keep track of redirects.
103                 hist.append(resp)
104                 new_hist = list(hist)
105                 resp.history = new_hist
106
107             try:
108                 resp.content  # Consume socket so it can be released
109             except (ChunkedEncodingError, ContentDecodingError, RuntimeError):
110                 resp.raw.read(decode_content=False)
111
112             if i >= self.max_redirects:
113                 raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects, response=resp)
114
115             # Release the connection back into the pool.
116             resp.close()
117
118             url = resp.headers['location']
119
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)
124
125             # The scheme should be lower case...
126             parsed = urlparse(url)
127             url = parsed.geturl()
128
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))
134             else:
135                 url = requote_uri(url)
136
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
141
142             self.rebuild_method(prepared_request, resp)
143
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']
148
149                 prepared_request.body = None
150
151             headers = prepared_request.headers
152             try:
153                 del headers['Cookie']
154             except KeyError:
155                 pass
156
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)
163
164             # Rebuild auth and proxy information.
165             proxies = self.rebuild_proxies(prepared_request, proxies)
166             self.rebuild_auth(prepared_request, resp)
167
168             # Override the original request.
169             req = prepared_request
170
171             resp = self.send(
172                 req,
173                 stream=stream,
174                 timeout=timeout,
175                 verify=verify,
176                 cert=cert,
177                 proxies=proxies,
178                 allow_redirects=False,
179                 **adapter_kwargs
180             )
181
182             extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
183
184             i += 1
185             yield resp
186
187     def rebuild_auth(self, prepared_request, response):
188         """
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.
192         """
193         headers = prepared_request.headers
194         url = prepared_request.url
195
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)
201
202             if (original_parsed.hostname != redirect_parsed.hostname):
203                 del headers['Authorization']
204
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)
209
210         return
211
212     def rebuild_proxies(self, prepared_request, proxies):
213         """
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
218         redirect).
219
220         This method also replaces the Proxy-Authorization header where
221         necessary.
222         """
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 {}
227
228         if self.trust_env and not should_bypass_proxies(url):
229             environ_proxies = get_environ_proxies(url)
230
231             proxy = environ_proxies.get(scheme)
232
233             if proxy:
234                 new_proxies.setdefault(scheme, environ_proxies[scheme])
235
236         if 'Proxy-Authorization' in headers:
237             del headers['Proxy-Authorization']
238
239         try:
240             username, password = get_auth_from_url(new_proxies[scheme])
241         except KeyError:
242             username, password = None, None
243
244         if username and password:
245             headers['Proxy-Authorization'] = _basic_auth_str(username, password)
246
247         return new_proxies
248
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.
252         """
253         method = prepared_request.method
254
255         # http://tools.ietf.org/html/rfc7231#section-6.4.4
256         if response.status_code == codes.see_other and method != 'HEAD':
257             method = 'GET'
258
259         # Do what the browsers do, despite standards...
260         # First, turn 302s into GETs.
261         if response.status_code == codes.found and method != 'HEAD':
262             method = 'GET'
263
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':
267             method = 'GET'
268
269         prepared_request.method = method
270
271
272 class Session(SessionRedirectMixin):
273     """A Requests session.
274
275     Provides cookie persistence, connection-pooling, and configuration.
276
277     Basic Usage::
278
279       >>> import requests
280       >>> s = requests.Session()
281       >>> s.get('http://httpbin.org/get')
282       <Response [200]>
283
284     Or as a context manager::
285
286       >>> with requests.Session() as s:
287       >>>     s.get('http://httpbin.org/get')
288       <Response [200]>
289     """
290
291     __attrs__ = [
292         'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',
293         'cert', 'prefetch', 'adapters', 'stream', 'trust_env',
294         'max_redirects',
295     ]
296
297     def __init__(self):
298
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()
303
304         #: Default Authentication tuple or object to attach to
305         #: :class:`Request <Request>`.
306         self.auth = None
307
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>`.
311         self.proxies = {}
312
313         #: Event-handling hooks.
314         self.hooks = default_hooks()
315
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.
319         self.params = {}
320
321         #: Stream response content default.
322         self.stream = False
323
324         #: SSL Verification default.
325         self.verify = True
326
327         #: SSL certificate default.
328         self.cert = None
329
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
333
334         #: Trust environment settings for proxy configuration, default
335         #: authentication and similar.
336         self.trust_env = True
337
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({})
343
344         # Default connection adapters.
345         self.adapters = OrderedDict()
346         self.mount('https://', HTTPAdapter())
347         self.mount('http://', HTTPAdapter())
348
349         # Only store 1000 redirects to prevent using infinite memory
350         self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
351
352     def __enter__(self):
353         return self
354
355     def __exit__(self, *args):
356         self.close()
357
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
362         :class:`Session`.
363
364         :param request: :class:`Request` instance to prepare with this
365             session's settings.
366         """
367         cookies = request.cookies or {}
368
369         # Bootstrap CookieJar.
370         if not isinstance(cookies, cookielib.CookieJar):
371             cookies = cookiejar_from_dict(cookies)
372
373         # Merge with session cookies
374         merged_cookies = merge_cookies(
375             merge_cookies(RequestsCookieJar(), self.cookies), cookies)
376
377
378         # Set environment's basic authentication if not explicitly set.
379         auth = request.auth
380         if self.trust_env and not auth and not self.auth:
381             auth = get_netrc_auth(request.url)
382
383         p = PreparedRequest()
384         p.prepare(
385             method=request.method.upper(),
386             url=request.url,
387             files=request.files,
388             data=request.data,
389             json=request.json,
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),
395         )
396         return p
397
398     def request(self, method, url,
399         params=None,
400         data=None,
401         headers=None,
402         cookies=None,
403         files=None,
404         auth=None,
405         timeout=None,
406         allow_redirects=True,
407         proxies=None,
408         hooks=None,
409         stream=None,
410         verify=None,
411         cert=None,
412         json=None):
413         """Constructs a :class:`Request <Request>`, prepares it and sends it.
414         Returns :class:`Response <Response>` object.
415
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
423             :class:`Request`.
424         :param headers: (optional) Dictionary of HTTP Headers to send with the
425             :class:`Request`.
426         :param cookies: (optional) Dict or CookieJar object to send with the
427             :class:`Request`.
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
447         """
448         # Create the Request.
449         req = Request(
450             method = method.upper(),
451             url = url,
452             headers = headers,
453             files = files,
454             data = data or {},
455             json = json,
456             params = params or {},
457             auth = auth,
458             cookies = cookies,
459             hooks = hooks,
460         )
461         prep = self.prepare_request(req)
462
463         proxies = proxies or {}
464
465         settings = self.merge_environment_settings(
466             prep.url, proxies, stream, verify, cert
467         )
468
469         # Send the request.
470         send_kwargs = {
471             'timeout': timeout,
472             'allow_redirects': allow_redirects,
473         }
474         send_kwargs.update(settings)
475         resp = self.send(prep, **send_kwargs)
476
477         return resp
478
479     def get(self, url, **kwargs):
480         """Sends a GET request. Returns :class:`Response` object.
481
482         :param url: URL for the new :class:`Request` object.
483         :param \*\*kwargs: Optional arguments that ``request`` takes.
484         """
485
486         kwargs.setdefault('allow_redirects', True)
487         return self.request('GET', url, **kwargs)
488
489     def options(self, url, **kwargs):
490         """Sends a OPTIONS request. Returns :class:`Response` object.
491
492         :param url: URL for the new :class:`Request` object.
493         :param \*\*kwargs: Optional arguments that ``request`` takes.
494         """
495
496         kwargs.setdefault('allow_redirects', True)
497         return self.request('OPTIONS', url, **kwargs)
498
499     def head(self, url, **kwargs):
500         """Sends a HEAD request. Returns :class:`Response` object.
501
502         :param url: URL for the new :class:`Request` object.
503         :param \*\*kwargs: Optional arguments that ``request`` takes.
504         """
505
506         kwargs.setdefault('allow_redirects', False)
507         return self.request('HEAD', url, **kwargs)
508
509     def post(self, url, data=None, json=None, **kwargs):
510         """Sends a POST request. Returns :class:`Response` object.
511
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.
516         """
517
518         return self.request('POST', url, data=data, json=json, **kwargs)
519
520     def put(self, url, data=None, **kwargs):
521         """Sends a PUT request. Returns :class:`Response` object.
522
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.
526         """
527
528         return self.request('PUT', url, data=data, **kwargs)
529
530     def patch(self, url, data=None, **kwargs):
531         """Sends a PATCH request. Returns :class:`Response` object.
532
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.
536         """
537
538         return self.request('PATCH', url,  data=data, **kwargs)
539
540     def delete(self, url, **kwargs):
541         """Sends a DELETE request. Returns :class:`Response` object.
542
543         :param url: URL for the new :class:`Request` object.
544         :param \*\*kwargs: Optional arguments that ``request`` takes.
545         """
546
547         return self.request('DELETE', url, **kwargs)
548
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)
557
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.')
562
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
567
568         # Resolve URL in redirect cache, if available.
569         if allow_redirects:
570             checked_urls = set()
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:
575                     break
576                 request.url = new_url
577
578         # Get the appropriate adapter to use
579         adapter = self.get_adapter(url=request.url)
580
581         # Start time (approximately) of the request
582         start = datetime.utcnow()
583
584         # Send the request
585         r = adapter.send(request, **kwargs)
586
587         # Total elapsed time of the request (approximately)
588         r.elapsed = datetime.utcnow() - start
589
590         # Response manipulation hooks
591         r = dispatch_hook('response', hooks, r, **kwargs)
592
593         # Persist cookies
594         if r.history:
595
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)
599
600         extract_cookies_to_jar(self.cookies, request, r.raw)
601
602         # Redirect resolving generator.
603         gen = self.resolve_redirects(r, request, **kwargs)
604
605         # Resolve redirects if allowed.
606         history = [resp for resp in gen] if allow_redirects else []
607
608         # Shuffle things around if there's history.
609         if history:
610             # Insert the first (original) request at the start
611             history.insert(0, r)
612             # Get the last request made
613             r = history.pop()
614             r.history = history
615
616         if not stream:
617             r.content
618
619         return r
620
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.
624         if self.trust_env:
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)
629
630             # Look for requests environment configuration and be compatible
631             # with cURL.
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'))
635
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)
641
642         return {'verify': verify, 'proxies': proxies, 'stream': stream,
643                 'cert': cert}
644
645     def get_adapter(self, url):
646         """Returns the appropriate connection adapter for the given URL."""
647         for (prefix, adapter) in self.adapters.items():
648
649             if url.lower().startswith(prefix):
650                 return adapter
651
652         # Nothing matches :-/
653         raise InvalidSchema("No connection adapters were found for '%s'" % url)
654
655     def close(self):
656         """Closes all adapters and as such the session"""
657         for v in self.adapters.values():
658             v.close()
659
660     def mount(self, prefix, adapter):
661         """Registers a connection adapter to a prefix.
662
663         Adapters are sorted in descending order by key length."""
664
665         self.adapters[prefix] = adapter
666         keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
667
668         for key in keys_to_move:
669             self.adapters[key] = self.adapters.pop(key)
670
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)
674         return state
675
676     def __setstate__(self, state):
677         redirect_cache = state.pop('redirect_cache', {})
678         for attr, value in state.items():
679             setattr(self, attr, value)
680
681         self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
682         for redirect, to in redirect_cache.items():
683             self.redirect_cache[redirect] = to
684
685
686 def session():
687     """Returns a :class:`Session` for context-management."""
688
689     return Session()