# -*- coding: utf-8 -*- """ requests.session ~~~~~~~~~~~~~~~~ This module provides a Session object to manage and persist settings across requests (cookies, auth, proxies). """ from copy import deepcopy from .compat import cookielib from .cookies import cookiejar_from_dict, remove_cookie_by_name from .models import Request from .hooks import dispatch_hook from .utils import header_expand, from_key_val_list, default_headers from .packages.urllib3.poolmanager import PoolManager from .exceptions import TooManyRedirects from .compat import urlparse, urljoin from .adapters import HTTPAdapter from .utils import requote_uri from .status_codes import codes REDIRECT_STATI = (codes.moved, codes.found, codes.other, codes.temporary_moved) DEFAULT_REDIRECT_LIMIT = 30 def merge_kwargs(local_kwarg, default_kwarg): """Merges kwarg dictionaries. If a local key in the dictionary is set to None, it will be removed. """ if default_kwarg is None: return local_kwarg if isinstance(local_kwarg, str): return local_kwarg if local_kwarg is None: return default_kwarg # Bypass if not a dictionary (e.g. timeout) if not hasattr(default_kwarg, 'items'): return local_kwarg default_kwarg = from_key_val_list(default_kwarg) local_kwarg = from_key_val_list(local_kwarg) # Update new values. kwargs = default_kwarg.copy() kwargs.update(local_kwarg) # Remove keys that are set to None. for (k, v) in local_kwarg.items(): if v is None: del kwargs[k] return kwargs class SessionMixin(object): def resolve_redirects(self, resp, req, prefetch=True, timeout=None, verify=True, cert=None): """Receives a Response. Returns a generator of Responses.""" i = 0 # ((resp.status_code is codes.see_other)) while (('location' in resp.headers and resp.status_code in REDIRECT_STATI)): resp.content # Consume socket so it can be released if i > self.max_redirects: raise TooManyRedirects() # Release the connection back into the pool. resp.close() url = resp.headers['location'] method = req.method # Handle redirection without scheme (see: RFC 1808 Section 4) if url.startswith('//'): parsed_rurl = urlparse(resp.url) url = '%s:%s' % (parsed_rurl.scheme, url) # Facilitate non-RFC2616-compliant 'location' headers # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') if not urlparse(url).netloc: # Compliant with RFC3986, we percent encode the url. url = urljoin(resp.url, requote_uri(url)) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 if resp.status_code is codes.see_other: method = 'GET' # Do what the browsers do, despite standards... if resp.status_code in (codes.moved, codes.found) and req.method == 'POST': method = 'GET' if (resp.status_code == 303) and req.method != 'HEAD': method = 'GET' # Remove the cookie headers that were sent. # headers = req.headers # try: # del headers['Cookie'] # except KeyError: # pass resp = self.request( url=url, method=method, params=req.params, auth=req.auth, cookies=req.cookies, allow_redirects=False, prefetch=prefetch, timeout=timeout, verify=verify, cert=cert ) yield resp i += 1 class Session(SessionMixin): """A Requests session.""" __attrs__ = [ 'headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks', 'params', 'config', 'verify', 'cert', 'prefetch'] def __init__(self, headers=None, cookies=None, auth=None, timeout=None, proxies=None, hooks=None, params=None, config=None, prefetch=True, verify=True, cert=None): #: A case-insensitive dictionary of headers to be sent on each #: :class:`Request ` sent from this #: :class:`Session `. self.headers = from_key_val_list(headers or []) #: Authentication tuple or object to attach to #: :class:`Request `. self.auth = auth #: Float describing the timeout of the each :class:`Request `. self.timeout = timeout #: Dictionary mapping protocol to the URL of the proxy (e.g. #: {'http': 'foo.bar:3128'}) to be used on each #: :class:`Request `. self.proxies = from_key_val_list(proxies or []) #: Event-handling hooks. self.hooks = from_key_val_list(hooks or {}) #: Dictionary of querystring data to attach to each #: :class:`Request `. The dictionary values may be lists for #: representing multivalued query parameters. self.params = from_key_val_list(params or []) #: Dictionary of configuration parameters for this #: :class:`Session `. self.config = from_key_val_list(config or {}) #: Prefetch response content. self.prefetch = prefetch #: SSL Verification. self.verify = verify #: SSL certificate. self.cert = cert #: Maximum number of redirects to follow. self.max_redirects = DEFAULT_REDIRECT_LIMIT #: Should we trust the environment self.trust_env = True self.default_headers = default_headers() # Set up a CookieJar to be used by default if isinstance(cookies, cookielib.CookieJar): self.cookies = cookies else: self.cookies = cookiejar_from_dict(cookies) def __enter__(self): return self def __exit__(self, *args): self.close() def close(self): pass def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=True, proxies=None, hooks=None, return_response=True, config=None, prefetch=None, verify=None, cert=None): req = Request() req.method = method req.url = url req.headers = headers req.files = files req.data = data req.params = params req.auth = auth req.cookies = cookies # TODO: move to attached # req.allow_redirects = allow_redirects req.proxies = proxies req.hooks = hooks prep = req.prepare() # TODO: prepare cookies. resp = self.send(prep, prefetch, timeout, verify, cert) # Redirect resolving generator. gen = self.resolve_redirects(resp, req, prefetch, timeout, verify, cert) # Resolve redirects if allowed. history = [r for r in gen] if allow_redirects else [] # Shuffle things around if there's history. if history: history.insert(0, resp) resp = history.pop() resp.history = tuple(history) return resp 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 options(self, url, **kwargs): """Sends a OPTIONS 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('OPTIONS', 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', False) return self.request('HEAD', url, **kwargs) def post(self, url, data=None, **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=None, **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=None, **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=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) def send(self, request, prefetch=True, timeout=None, verify=True, cert=None): """Send a given PreparedRequest.""" adapter = HTTPAdapter() r = adapter.send(request, prefetch, timeout, verify, cert) return r def __getstate__(self): return dict((attr, getattr(self, attr, None)) for attr in self.__attrs__) def __setstate__(self, state): for attr, value in state.items(): setattr(self, attr, value) self.init_poolmanager() def session(**kwargs): """Returns a :class:`Session` for context-management.""" return Session(**kwargs)