1# -*- coding: utf-8 -*- 2 3""" 4requests.session 5~~~~~~~~~~~~~~~~ 6 7This module provides a Session object to manage and persist settings across 8requests (cookies, auth, proxies). 9""" 10import os 11import platform 12import time 13from collections import Mapping 14from datetime import timedelta 15 16from .auth import _basic_auth_str 17from .compat import cookielib, is_py3, OrderedDict, urljoin, urlparse 18from .cookies import ( 19 cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies) 20from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT 21from .hooks import default_hooks, dispatch_hook 22from ._internal_utils import to_native_string 23from .utils import to_key_val_list, default_headers 24from .exceptions import ( 25 TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError) 26 27from .structures import CaseInsensitiveDict 28from .adapters import HTTPAdapter 29 30from .utils import ( 31 requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies, 32 get_auth_from_url, rewind_body 33) 34 35from .status_codes import codes 36 37# formerly defined here, reexposed here for backward compatibility 38from .models import REDIRECT_STATI 39 40# Preferred clock, based on which one is more accurate on a given system. 41if platform.system() == 'Windows': 42 try: # Python 3.3+ 43 preferred_clock = time.perf_counter 44 except AttributeError: # Earlier than Python 3. 45 preferred_clock = time.clock 46else: 47 preferred_clock = time.time 48 49 50def merge_setting(request_setting, session_setting, dict_class=OrderedDict): 51 """Determines appropriate setting for a given request, taking into account 52 the explicit setting on that request, and the setting in the session. If a 53 setting is a dictionary, they will be merged together using `dict_class` 54 """ 55 56 if session_setting is None: 57 return request_setting 58 59 if request_setting is None: 60 return session_setting 61 62 # Bypass if not a dictionary (e.g. verify) 63 if not ( 64 isinstance(session_setting, Mapping) and 65 isinstance(request_setting, Mapping) 66 ): 67 return request_setting 68 69 merged_setting = dict_class(to_key_val_list(session_setting)) 70 merged_setting.update(to_key_val_list(request_setting)) 71 72 # Remove keys that are set to None. Extract keys first to avoid altering 73 # the dictionary during iteration. 74 none_keys = [k for (k, v) in merged_setting.items() if v is None] 75 for key in none_keys: 76 del merged_setting[key] 77 78 return merged_setting 79 80 81def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict): 82 """Properly merges both requests and session hooks. 83 84 This is necessary because when request_hooks == {'response': []}, the 85 merge breaks Session hooks entirely. 86 """ 87 if session_hooks is None or session_hooks.get('response') == []: 88 return request_hooks 89 90 if request_hooks is None or request_hooks.get('response') == []: 91 return session_hooks 92 93 return merge_setting(request_hooks, session_hooks, dict_class) 94 95 96class SessionRedirectMixin(object): 97 98 def get_redirect_target(self, resp): 99 """Receives a Response. Returns a redirect URI or ``None``""" 100 # Due to the nature of how requests processes redirects this method will 101 # be called at least once upon the original response and at least twice 102 # on each subsequent redirect response (if any). 103 # If a custom mixin is used to handle this logic, it may be advantageous 104 # to cache the redirect location onto the response object as a private 105 # attribute. 106 if resp.is_redirect: 107 location = resp.headers['location'] 108 # Currently the underlying http module on py3 decode headers 109 # in latin1, but empirical evidence suggests that latin1 is very 110 # rarely used with non-ASCII characters in HTTP headers. 111 # It is more likely to get UTF8 header rather than latin1. 112 # This causes incorrect handling of UTF8 encoded location headers. 113 # To solve this, we re-encode the location in latin1. 114 if is_py3: 115 location = location.encode('latin1') 116 return to_native_string(location, 'utf8') 117 return None 118 119 def resolve_redirects(self, resp, req, stream=False, timeout=None, 120 verify=True, cert=None, proxies=None, yield_requests=False, **adapter_kwargs): 121 """Receives a Response. Returns a generator of Responses or Requests.""" 122 123 hist = [] # keep track of history 124 125 url = self.get_redirect_target(resp) 126 while url: 127 prepared_request = req.copy() 128 129 # Update history and keep track of redirects. 130 # resp.history must ignore the original request in this loop 131 hist.append(resp) 132 resp.history = hist[1:] 133 134 try: 135 resp.content # Consume socket so it can be released 136 except (ChunkedEncodingError, ContentDecodingError, RuntimeError): 137 resp.raw.read(decode_content=False) 138 139 if len(resp.history) >= self.max_redirects: 140 raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects, response=resp) 141 142 # Release the connection back into the pool. 143 resp.close() 144 145 # Handle redirection without scheme (see: RFC 1808 Section 4) 146 if url.startswith('//'): 147 parsed_rurl = urlparse(resp.url) 148 url = '%s:%s' % (to_native_string(parsed_rurl.scheme), url) 149 150 # The scheme should be lower case... 151 parsed = urlparse(url) 152 url = parsed.geturl() 153 154 # Facilitate relative 'location' headers, as allowed by RFC 7231. 155 # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') 156 # Compliant with RFC3986, we percent encode the url. 157 if not parsed.netloc: 158 url = urljoin(resp.url, requote_uri(url)) 159 else: 160 url = requote_uri(url) 161 162 prepared_request.url = to_native_string(url) 163 164 self.rebuild_method(prepared_request, resp) 165 166 # https://github.com/requests/requests/issues/1084 167 if resp.status_code not in (codes.temporary_redirect, codes.permanent_redirect): 168 # https://github.com/requests/requests/issues/3490 169 purged_headers = ('Content-Length', 'Content-Type', 'Transfer-Encoding') 170 for header in purged_headers: 171 prepared_request.headers.pop(header, None) 172 prepared_request.body = None 173 174 headers = prepared_request.headers 175 try: 176 del headers['Cookie'] 177 except KeyError: 178 pass 179 180 # Extract any cookies sent on the response to the cookiejar 181 # in the new request. Because we've mutated our copied prepared 182 # request, use the old one that we haven't yet touched. 183 extract_cookies_to_jar(prepared_request._cookies, req, resp.raw) 184 merge_cookies(prepared_request._cookies, self.cookies) 185 prepared_request.prepare_cookies(prepared_request._cookies) 186 187 # Rebuild auth and proxy information. 188 proxies = self.rebuild_proxies(prepared_request, proxies) 189 self.rebuild_auth(prepared_request, resp) 190 191 # A failed tell() sets `_body_position` to `object()`. This non-None 192 # value ensures `rewindable` will be True, allowing us to raise an 193 # UnrewindableBodyError, instead of hanging the connection. 194 rewindable = ( 195 prepared_request._body_position is not None and 196 ('Content-Length' in headers or 'Transfer-Encoding' in headers) 197 ) 198 199 # Attempt to rewind consumed file-like object. 200 if rewindable: 201 rewind_body(prepared_request) 202 203 # Override the original request. 204 req = prepared_request 205 206 if yield_requests: 207 yield req 208 else: 209 210 resp = self.send( 211 req, 212 stream=stream, 213 timeout=timeout, 214 verify=verify, 215 cert=cert, 216 proxies=proxies, 217 allow_redirects=False, 218 **adapter_kwargs 219 ) 220 221 extract_cookies_to_jar(self.cookies, prepared_request, resp.raw) 222 223 # extract redirect url, if any, for the next loop 224 url = self.get_redirect_target(resp) 225 yield resp 226 227 def rebuild_auth(self, prepared_request, response): 228 """When being redirected we may want to strip authentication from the 229 request to avoid leaking credentials. This method intelligently removes 230 and reapplies authentication where possible to avoid credential loss. 231 """ 232 headers = prepared_request.headers 233 url = prepared_request.url 234 235 if 'Authorization' in headers: 236 # If we get redirected to a new host, we should strip out any 237 # authentication headers. 238 original_parsed = urlparse(response.request.url) 239 redirect_parsed = urlparse(url) 240 241 if (original_parsed.hostname != redirect_parsed.hostname): 242 del headers['Authorization'] 243 244 # .netrc might have more auth for us on our new host. 245 new_auth = get_netrc_auth(url) if self.trust_env else None 246 if new_auth is not None: 247 prepared_request.prepare_auth(new_auth) 248 249 return 250 251 def rebuild_proxies(self, prepared_request, proxies): 252 """This method re-evaluates the proxy configuration by considering the 253 environment variables. If we are redirected to a URL covered by 254 NO_PROXY, we strip the proxy configuration. Otherwise, we set missing 255 proxy keys for this URL (in case they were stripped by a previous 256 redirect). 257 258 This method also replaces the Proxy-Authorization header where 259 necessary. 260 261 :rtype: dict 262 """ 263 proxies = proxies if proxies is not None else {} 264 headers = prepared_request.headers 265 url = prepared_request.url 266 scheme = urlparse(url).scheme 267 new_proxies = proxies.copy() 268 no_proxy = proxies.get('no_proxy') 269 270 bypass_proxy = should_bypass_proxies(url, no_proxy=no_proxy) 271 if self.trust_env and not bypass_proxy: 272 environ_proxies = get_environ_proxies(url, no_proxy=no_proxy) 273 274 proxy = environ_proxies.get(scheme, environ_proxies.get('all')) 275 276 if proxy: 277 new_proxies.setdefault(scheme, proxy) 278 279 if 'Proxy-Authorization' in headers: 280 del headers['Proxy-Authorization'] 281 282 try: 283 username, password = get_auth_from_url(new_proxies[scheme]) 284 except KeyError: 285 username, password = None, None 286 287 if username and password: 288 headers['Proxy-Authorization'] = _basic_auth_str(username, password) 289 290 return new_proxies 291 292 def rebuild_method(self, prepared_request, response): 293 """When being redirected we may want to change the method of the request 294 based on certain specs or browser behavior. 295 """ 296 method = prepared_request.method 297 298 # http://tools.ietf.org/html/rfc7231#section-6.4.4 299 if response.status_code == codes.see_other and method != 'HEAD': 300 method = 'GET' 301 302 # Do what the browsers do, despite standards... 303 # First, turn 302s into GETs. 304 if response.status_code == codes.found and method != 'HEAD': 305 method = 'GET' 306 307 # Second, if a POST is responded to with a 301, turn it into a GET. 308 # This bizarre behaviour is explained in Issue 1704. 309 if response.status_code == codes.moved and method == 'POST': 310 method = 'GET' 311 312 prepared_request.method = method 313 314 315class Session(SessionRedirectMixin): 316 """A Requests session. 317 318 Provides cookie persistence, connection-pooling, and configuration. 319 320 Basic Usage:: 321 322 >>> import requests 323 >>> s = requests.Session() 324 >>> s.get('http://httpbin.org/get') 325 <Response [200]> 326 327 Or as a context manager:: 328 329 >>> with requests.Session() as s: 330 >>> s.get('http://httpbin.org/get') 331 <Response [200]> 332 """ 333 334 __attrs__ = [ 335 'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify', 336 'cert', 'prefetch', 'adapters', 'stream', 'trust_env', 337 'max_redirects', 338 ] 339 340 def __init__(self): 341 342 #: A case-insensitive dictionary of headers to be sent on each 343 #: :class:`Request <Request>` sent from this 344 #: :class:`Session <Session>`. 345 self.headers = default_headers() 346 347 #: Default Authentication tuple or object to attach to 348 #: :class:`Request <Request>`. 349 self.auth = None 350 351 #: Dictionary mapping protocol or protocol and host to the URL of the proxy 352 #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to 353 #: be used on each :class:`Request <Request>`. 354 self.proxies = {} 355 356 #: Event-handling hooks. 357 self.hooks = default_hooks() 358 359 #: Dictionary of querystring data to attach to each 360 #: :class:`Request <Request>`. The dictionary values may be lists for 361 #: representing multivalued query parameters. 362 self.params = {} 363 364 #: Stream response content default. 365 self.stream = False 366 367 #: SSL Verification default. 368 self.verify = True 369 370 #: SSL client certificate default, if String, path to ssl client 371 #: cert file (.pem). If Tuple, ('cert', 'key') pair. 372 self.cert = None 373 374 #: Maximum number of redirects allowed. If the request exceeds this 375 #: limit, a :class:`TooManyRedirects` exception is raised. 376 #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is 377 #: 30. 378 self.max_redirects = DEFAULT_REDIRECT_LIMIT 379 380 #: Trust environment settings for proxy configuration, default 381 #: authentication and similar. 382 self.trust_env = True 383 384 #: A CookieJar containing all currently outstanding cookies set on this 385 #: session. By default it is a 386 #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but 387 #: may be any other ``cookielib.CookieJar`` compatible object. 388 self.cookies = cookiejar_from_dict({}) 389 390 # Default connection adapters. 391 self.adapters = OrderedDict() 392 self.mount('https://', HTTPAdapter()) 393 self.mount('http://', HTTPAdapter()) 394 395 def __enter__(self): 396 return self 397 398 def __exit__(self, *args): 399 self.close() 400 401 def prepare_request(self, request): 402 """Constructs a :class:`PreparedRequest <PreparedRequest>` for 403 transmission and returns it. The :class:`PreparedRequest` has settings 404 merged from the :class:`Request <Request>` instance and those of the 405 :class:`Session`. 406 407 :param request: :class:`Request` instance to prepare with this 408 session's settings. 409 :rtype: requests.PreparedRequest 410 """ 411 cookies = request.cookies or {} 412 413 # Bootstrap CookieJar. 414 if not isinstance(cookies, cookielib.CookieJar): 415 cookies = cookiejar_from_dict(cookies) 416 417 # Merge with session cookies 418 merged_cookies = merge_cookies( 419 merge_cookies(RequestsCookieJar(), self.cookies), cookies) 420 421 # Set environment's basic authentication if not explicitly set. 422 auth = request.auth 423 if self.trust_env and not auth and not self.auth: 424 auth = get_netrc_auth(request.url) 425 426 p = PreparedRequest() 427 p.prepare( 428 method=request.method.upper(), 429 url=request.url, 430 files=request.files, 431 data=request.data, 432 json=request.json, 433 headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict), 434 params=merge_setting(request.params, self.params), 435 auth=merge_setting(auth, self.auth), 436 cookies=merged_cookies, 437 hooks=merge_hooks(request.hooks, self.hooks), 438 ) 439 return p 440 441 def request(self, method, url, 442 params=None, data=None, headers=None, cookies=None, files=None, 443 auth=None, timeout=None, allow_redirects=True, proxies=None, 444 hooks=None, stream=None, verify=None, cert=None, json=None): 445 """Constructs a :class:`Request <Request>`, prepares it and sends it. 446 Returns :class:`Response <Response>` object. 447 448 :param method: method for the new :class:`Request` object. 449 :param url: URL for the new :class:`Request` object. 450 :param params: (optional) Dictionary or bytes to be sent in the query 451 string for the :class:`Request`. 452 :param data: (optional) Dictionary, bytes, or file-like object to send 453 in the body of the :class:`Request`. 454 :param json: (optional) json to send in the body of the 455 :class:`Request`. 456 :param headers: (optional) Dictionary of HTTP Headers to send with the 457 :class:`Request`. 458 :param cookies: (optional) Dict or CookieJar object to send with the 459 :class:`Request`. 460 :param files: (optional) Dictionary of ``'filename': file-like-objects`` 461 for multipart encoding upload. 462 :param auth: (optional) Auth tuple or callable to enable 463 Basic/Digest/Custom HTTP Auth. 464 :param timeout: (optional) How long to wait for the server to send 465 data before giving up, as a float, or a :ref:`(connect timeout, 466 read timeout) <timeouts>` tuple. 467 :type timeout: float or tuple 468 :param allow_redirects: (optional) Set to True by default. 469 :type allow_redirects: bool 470 :param proxies: (optional) Dictionary mapping protocol or protocol and 471 hostname to the URL of the proxy. 472 :param stream: (optional) whether to immediately download the response 473 content. Defaults to ``False``. 474 :param verify: (optional) Either a boolean, in which case it controls whether we verify 475 the server's TLS certificate, or a string, in which case it must be a path 476 to a CA bundle to use. Defaults to ``True``. 477 :param cert: (optional) if String, path to ssl client cert file (.pem). 478 If Tuple, ('cert', 'key') pair. 479 :rtype: requests.Response 480 """ 481 # Create the Request. 482 req = Request( 483 method=method.upper(), 484 url=url, 485 headers=headers, 486 files=files, 487 data=data or {}, 488 json=json, 489 params=params or {}, 490 auth=auth, 491 cookies=cookies, 492 hooks=hooks, 493 ) 494 prep = self.prepare_request(req) 495 496 proxies = proxies or {} 497 498 settings = self.merge_environment_settings( 499 prep.url, proxies, stream, verify, cert 500 ) 501 502 # Send the request. 503 send_kwargs = { 504 'timeout': timeout, 505 'allow_redirects': allow_redirects, 506 } 507 send_kwargs.update(settings) 508 resp = self.send(prep, **send_kwargs) 509 510 return resp 511 512 def get(self, url, **kwargs): 513 r"""Sends a GET request. Returns :class:`Response` object. 514 515 :param url: URL for the new :class:`Request` object. 516 :param \*\*kwargs: Optional arguments that ``request`` takes. 517 :rtype: requests.Response 518 """ 519 520 kwargs.setdefault('allow_redirects', True) 521 return self.request('GET', url, **kwargs) 522 523 def options(self, url, **kwargs): 524 r"""Sends a OPTIONS request. Returns :class:`Response` object. 525 526 :param url: URL for the new :class:`Request` object. 527 :param \*\*kwargs: Optional arguments that ``request`` takes. 528 :rtype: requests.Response 529 """ 530 531 kwargs.setdefault('allow_redirects', True) 532 return self.request('OPTIONS', url, **kwargs) 533 534 def head(self, url, **kwargs): 535 r"""Sends a HEAD request. Returns :class:`Response` object. 536 537 :param url: URL for the new :class:`Request` object. 538 :param \*\*kwargs: Optional arguments that ``request`` takes. 539 :rtype: requests.Response 540 """ 541 542 kwargs.setdefault('allow_redirects', False) 543 return self.request('HEAD', url, **kwargs) 544 545 def post(self, url, data=None, json=None, **kwargs): 546 r"""Sends a POST request. Returns :class:`Response` object. 547 548 :param url: URL for the new :class:`Request` object. 549 :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. 550 :param json: (optional) json to send in the body of the :class:`Request`. 551 :param \*\*kwargs: Optional arguments that ``request`` takes. 552 :rtype: requests.Response 553 """ 554 555 return self.request('POST', url, data=data, json=json, **kwargs) 556 557 def put(self, url, data=None, **kwargs): 558 r"""Sends a PUT request. Returns :class:`Response` object. 559 560 :param url: URL for the new :class:`Request` object. 561 :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. 562 :param \*\*kwargs: Optional arguments that ``request`` takes. 563 :rtype: requests.Response 564 """ 565 566 return self.request('PUT', url, data=data, **kwargs) 567 568 def patch(self, url, data=None, **kwargs): 569 r"""Sends a PATCH request. Returns :class:`Response` object. 570 571 :param url: URL for the new :class:`Request` object. 572 :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. 573 :param \*\*kwargs: Optional arguments that ``request`` takes. 574 :rtype: requests.Response 575 """ 576 577 return self.request('PATCH', url, data=data, **kwargs) 578 579 def delete(self, url, **kwargs): 580 r"""Sends a DELETE request. Returns :class:`Response` object. 581 582 :param url: URL for the new :class:`Request` object. 583 :param \*\*kwargs: Optional arguments that ``request`` takes. 584 :rtype: requests.Response 585 """ 586 587 return self.request('DELETE', url, **kwargs) 588 589 def send(self, request, **kwargs): 590 """Send a given PreparedRequest. 591 592 :rtype: requests.Response 593 """ 594 # Set defaults that the hooks can utilize to ensure they always have 595 # the correct parameters to reproduce the previous request. 596 kwargs.setdefault('stream', self.stream) 597 kwargs.setdefault('verify', self.verify) 598 kwargs.setdefault('cert', self.cert) 599 kwargs.setdefault('proxies', self.proxies) 600 601 # It's possible that users might accidentally send a Request object. 602 # Guard against that specific failure case. 603 if isinstance(request, Request): 604 raise ValueError('You can only send PreparedRequests.') 605 606 # Set up variables needed for resolve_redirects and dispatching of hooks 607 allow_redirects = kwargs.pop('allow_redirects', True) 608 stream = kwargs.get('stream') 609 hooks = request.hooks 610 611 # Get the appropriate adapter to use 612 adapter = self.get_adapter(url=request.url) 613 614 # Start time (approximately) of the request 615 start = preferred_clock() 616 617 # Send the request 618 r = adapter.send(request, **kwargs) 619 620 # Total elapsed time of the request (approximately) 621 elapsed = preferred_clock() - start 622 r.elapsed = timedelta(seconds=elapsed) 623 624 # Response manipulation hooks 625 r = dispatch_hook('response', hooks, r, **kwargs) 626 627 # Persist cookies 628 if r.history: 629 630 # If the hooks create history then we want those cookies too 631 for resp in r.history: 632 extract_cookies_to_jar(self.cookies, resp.request, resp.raw) 633 634 extract_cookies_to_jar(self.cookies, request, r.raw) 635 636 # Redirect resolving generator. 637 gen = self.resolve_redirects(r, request, **kwargs) 638 639 # Resolve redirects if allowed. 640 history = [resp for resp in gen] if allow_redirects else [] 641 642 # Shuffle things around if there's history. 643 if history: 644 # Insert the first (original) request at the start 645 history.insert(0, r) 646 # Get the last request made 647 r = history.pop() 648 r.history = history 649 650 # If redirects aren't being followed, store the response on the Request for Response.next(). 651 if not allow_redirects: 652 try: 653 r._next = next(self.resolve_redirects(r, request, yield_requests=True, **kwargs)) 654 except StopIteration: 655 pass 656 657 if not stream: 658 r.content 659 660 return r 661 662 def merge_environment_settings(self, url, proxies, stream, verify, cert): 663 """ 664 Check the environment and merge it with some settings. 665 666 :rtype: dict 667 """ 668 # Gather clues from the surrounding environment. 669 if self.trust_env: 670 # Set environment's proxies. 671 no_proxy = proxies.get('no_proxy') if proxies is not None else None 672 env_proxies = get_environ_proxies(url, no_proxy=no_proxy) 673 for (k, v) in env_proxies.items(): 674 proxies.setdefault(k, v) 675 676 # Look for requests environment configuration and be compatible 677 # with cURL. 678 if verify is True or verify is None: 679 verify = (os.environ.get('REQUESTS_CA_BUNDLE') or 680 os.environ.get('CURL_CA_BUNDLE')) 681 682 # Merge all the kwargs. 683 proxies = merge_setting(proxies, self.proxies) 684 stream = merge_setting(stream, self.stream) 685 verify = merge_setting(verify, self.verify) 686 cert = merge_setting(cert, self.cert) 687 688 return {'verify': verify, 'proxies': proxies, 'stream': stream, 689 'cert': cert} 690 691 def get_adapter(self, url): 692 """ 693 Returns the appropriate connection adapter for the given URL. 694 695 :rtype: requests.adapters.BaseAdapter 696 """ 697 for (prefix, adapter) in self.adapters.items(): 698 699 if url.lower().startswith(prefix): 700 return adapter 701 702 # Nothing matches :-/ 703 raise InvalidSchema("No connection adapters were found for '%s'" % url) 704 705 def close(self): 706 """Closes all adapters and as such the session""" 707 for v in self.adapters.values(): 708 v.close() 709 710 def mount(self, prefix, adapter): 711 """Registers a connection adapter to a prefix. 712 713 Adapters are sorted in descending order by prefix length. 714 """ 715 self.adapters[prefix] = adapter 716 keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] 717 718 for key in keys_to_move: 719 self.adapters[key] = self.adapters.pop(key) 720 721 def __getstate__(self): 722 state = dict((attr, getattr(self, attr, None)) for attr in self.__attrs__) 723 return state 724 725 def __setstate__(self, state): 726 for attr, value in state.items(): 727 setattr(self, attr, value) 728 729 730def session(): 731 """ 732 Returns a :class:`Session` for context-management. 733 734 :rtype: Session 735 """ 736 737 return Session() 738