Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / requests / sessions.py @ 564

History | View | Annotate | Download (24 KB)

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)
114

    
115
            # Release the connection back into the pool.
116
            resp.close()
117

    
118
            url = resp.headers['location']
119
            method = req.method
120

    
121
            # Handle redirection without scheme (see: RFC 1808 Section 4)
122
            if url.startswith('//'):
123
                parsed_rurl = urlparse(resp.url)
124
                url = '%s:%s' % (parsed_rurl.scheme, url)
125

    
126
            # The scheme should be lower case...
127
            parsed = urlparse(url)
128
            url = parsed.geturl()
129

    
130
            # Facilitate relative 'location' headers, as allowed by RFC 7231.
131
            # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
132
            # Compliant with RFC3986, we percent encode the url.
133
            if not parsed.netloc:
134
                url = urljoin(resp.url, requote_uri(url))
135
            else:
136
                url = requote_uri(url)
137

    
138
            prepared_request.url = to_native_string(url)
139
            # Cache the url, unless it redirects to itself.
140
            if resp.is_permanent_redirect and req.url != prepared_request.url:
141
                self.redirect_cache[req.url] = prepared_request.url
142

    
143
            # http://tools.ietf.org/html/rfc7231#section-6.4.4
144
            if (resp.status_code == codes.see_other and
145
                    method != 'HEAD'):
146
                method = 'GET'
147

    
148
            # Do what the browsers do, despite standards...
149
            # First, turn 302s into GETs.
150
            if resp.status_code == codes.found and method != 'HEAD':
151
                method = 'GET'
152

    
153
            # Second, if a POST is responded to with a 301, turn it into a GET.
154
            # This bizarre behaviour is explained in Issue 1704.
155
            if resp.status_code == codes.moved and method == 'POST':
156
                method = 'GET'
157

    
158
            prepared_request.method = method
159

    
160
            # https://github.com/kennethreitz/requests/issues/1084
161
            if resp.status_code not in (codes.temporary_redirect, codes.permanent_redirect):
162
                if 'Content-Length' in prepared_request.headers:
163
                    del prepared_request.headers['Content-Length']
164

    
165
                prepared_request.body = None
166

    
167
            headers = prepared_request.headers
168
            try:
169
                del headers['Cookie']
170
            except KeyError:
171
                pass
172

    
173
            # Extract any cookies sent on the response to the cookiejar
174
            # in the new request. Because we've mutated our copied prepared
175
            # request, use the old one that we haven't yet touched.
176
            extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
177
            prepared_request._cookies.update(self.cookies)
178
            prepared_request.prepare_cookies(prepared_request._cookies)
179

    
180
            # Rebuild auth and proxy information.
181
            proxies = self.rebuild_proxies(prepared_request, proxies)
182
            self.rebuild_auth(prepared_request, resp)
183

    
184
            # Override the original request.
185
            req = prepared_request
186

    
187
            resp = self.send(
188
                req,
189
                stream=stream,
190
                timeout=timeout,
191
                verify=verify,
192
                cert=cert,
193
                proxies=proxies,
194
                allow_redirects=False,
195
                **adapter_kwargs
196
            )
197

    
198
            extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
199

    
200
            i += 1
201
            yield resp
202

    
203
    def rebuild_auth(self, prepared_request, response):
204
        """
205
        When being redirected we may want to strip authentication from the
206
        request to avoid leaking credentials. This method intelligently removes
207
        and reapplies authentication where possible to avoid credential loss.
208
        """
209
        headers = prepared_request.headers
210
        url = prepared_request.url
211

    
212
        if 'Authorization' in headers:
213
            # If we get redirected to a new host, we should strip out any
214
            # authentication headers.
215
            original_parsed = urlparse(response.request.url)
216
            redirect_parsed = urlparse(url)
217

    
218
            if (original_parsed.hostname != redirect_parsed.hostname):
219
                del headers['Authorization']
220

    
221
        # .netrc might have more auth for us on our new host.
222
        new_auth = get_netrc_auth(url) if self.trust_env else None
223
        if new_auth is not None:
224
            prepared_request.prepare_auth(new_auth)
225

    
226
        return
227

    
228
    def rebuild_proxies(self, prepared_request, proxies):
229
        """
230
        This method re-evaluates the proxy configuration by considering the
231
        environment variables. If we are redirected to a URL covered by
232
        NO_PROXY, we strip the proxy configuration. Otherwise, we set missing
233
        proxy keys for this URL (in case they were stripped by a previous
234
        redirect).
235

236
        This method also replaces the Proxy-Authorization header where
237
        necessary.
238
        """
239
        headers = prepared_request.headers
240
        url = prepared_request.url
241
        scheme = urlparse(url).scheme
242
        new_proxies = proxies.copy() if proxies is not None else {}
243

    
244
        if self.trust_env and not should_bypass_proxies(url):
245
            environ_proxies = get_environ_proxies(url)
246

    
247
            proxy = environ_proxies.get(scheme)
248

    
249
            if proxy:
250
                new_proxies.setdefault(scheme, environ_proxies[scheme])
251

    
252
        if 'Proxy-Authorization' in headers:
253
            del headers['Proxy-Authorization']
254

    
255
        try:
256
            username, password = get_auth_from_url(new_proxies[scheme])
257
        except KeyError:
258
            username, password = None, None
259

    
260
        if username and password:
261
            headers['Proxy-Authorization'] = _basic_auth_str(username, password)
262

    
263
        return new_proxies
264

    
265

    
266
class Session(SessionRedirectMixin):
267
    """A Requests session.
268

269
    Provides cookie persistence, connection-pooling, and configuration.
270

271
    Basic Usage::
272

273
      >>> import requests
274
      >>> s = requests.Session()
275
      >>> s.get('http://httpbin.org/get')
276
      <Response [200]>
277

278
    Or as a context manager::
279

280
      >>> with requests.Session() as s:
281
      >>>     s.get('http://httpbin.org/get')
282
      <Response [200]>
283
    """
284

    
285
    __attrs__ = [
286
        'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',
287
        'cert', 'prefetch', 'adapters', 'stream', 'trust_env',
288
        'max_redirects',
289
    ]
290

    
291
    def __init__(self):
292

    
293
        #: A case-insensitive dictionary of headers to be sent on each
294
        #: :class:`Request <Request>` sent from this
295
        #: :class:`Session <Session>`.
296
        self.headers = default_headers()
297

    
298
        #: Default Authentication tuple or object to attach to
299
        #: :class:`Request <Request>`.
300
        self.auth = None
301

    
302
        #: Dictionary mapping protocol or protocol and host to the URL of the proxy
303
        #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
304
        #: be used on each :class:`Request <Request>`.
305
        self.proxies = {}
306

    
307
        #: Event-handling hooks.
308
        self.hooks = default_hooks()
309

    
310
        #: Dictionary of querystring data to attach to each
311
        #: :class:`Request <Request>`. The dictionary values may be lists for
312
        #: representing multivalued query parameters.
313
        self.params = {}
314

    
315
        #: Stream response content default.
316
        self.stream = False
317

    
318
        #: SSL Verification default.
319
        self.verify = True
320

    
321
        #: SSL certificate default.
322
        self.cert = None
323

    
324
        #: Maximum number of redirects allowed. If the request exceeds this
325
        #: limit, a :class:`TooManyRedirects` exception is raised.
326
        self.max_redirects = DEFAULT_REDIRECT_LIMIT
327

    
328
        #: Trust environment settings for proxy configuration, default
329
        #: authentication and similar.
330
        self.trust_env = True
331

    
332
        #: A CookieJar containing all currently outstanding cookies set on this
333
        #: session. By default it is a
334
        #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but
335
        #: may be any other ``cookielib.CookieJar`` compatible object.
336
        self.cookies = cookiejar_from_dict({})
337

    
338
        # Default connection adapters.
339
        self.adapters = OrderedDict()
340
        self.mount('https://', HTTPAdapter())
341
        self.mount('http://', HTTPAdapter())
342

    
343
        # Only store 1000 redirects to prevent using infinite memory
344
        self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
345

    
346
    def __enter__(self):
347
        return self
348

    
349
    def __exit__(self, *args):
350
        self.close()
351

    
352
    def prepare_request(self, request):
353
        """Constructs a :class:`PreparedRequest <PreparedRequest>` for
354
        transmission and returns it. The :class:`PreparedRequest` has settings
355
        merged from the :class:`Request <Request>` instance and those of the
356
        :class:`Session`.
357

358
        :param request: :class:`Request` instance to prepare with this
359
            session's settings.
360
        """
361
        cookies = request.cookies or {}
362

    
363
        # Bootstrap CookieJar.
364
        if not isinstance(cookies, cookielib.CookieJar):
365
            cookies = cookiejar_from_dict(cookies)
366

    
367
        # Merge with session cookies
368
        merged_cookies = merge_cookies(
369
            merge_cookies(RequestsCookieJar(), self.cookies), cookies)
370

    
371

    
372
        # Set environment's basic authentication if not explicitly set.
373
        auth = request.auth
374
        if self.trust_env and not auth and not self.auth:
375
            auth = get_netrc_auth(request.url)
376

    
377
        p = PreparedRequest()
378
        p.prepare(
379
            method=request.method.upper(),
380
            url=request.url,
381
            files=request.files,
382
            data=request.data,
383
            json=request.json,
384
            headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
385
            params=merge_setting(request.params, self.params),
386
            auth=merge_setting(auth, self.auth),
387
            cookies=merged_cookies,
388
            hooks=merge_hooks(request.hooks, self.hooks),
389
        )
390
        return p
391

    
392
    def request(self, method, url,
393
        params=None,
394
        data=None,
395
        headers=None,
396
        cookies=None,
397
        files=None,
398
        auth=None,
399
        timeout=None,
400
        allow_redirects=True,
401
        proxies=None,
402
        hooks=None,
403
        stream=None,
404
        verify=None,
405
        cert=None,
406
        json=None):
407
        """Constructs a :class:`Request <Request>`, prepares it and sends it.
408
        Returns :class:`Response <Response>` object.
409

410
        :param method: method for the new :class:`Request` object.
411
        :param url: URL for the new :class:`Request` object.
412
        :param params: (optional) Dictionary or bytes to be sent in the query
413
            string for the :class:`Request`.
414
        :param data: (optional) Dictionary, bytes, or file-like object to send
415
            in the body of the :class:`Request`.
416
        :param json: (optional) json to send in the body of the
417
            :class:`Request`.
418
        :param headers: (optional) Dictionary of HTTP Headers to send with the
419
            :class:`Request`.
420
        :param cookies: (optional) Dict or CookieJar object to send with the
421
            :class:`Request`.
422
        :param files: (optional) Dictionary of ``'filename': file-like-objects``
423
            for multipart encoding upload.
424
        :param auth: (optional) Auth tuple or callable to enable
425
            Basic/Digest/Custom HTTP Auth.
426
        :param timeout: (optional) How long to wait for the server to send
427
            data before giving up, as a float, or a :ref:`(connect timeout,
428
            read timeout) <timeouts>` tuple.
429
        :type timeout: float or tuple
430
        :param allow_redirects: (optional) Set to True by default.
431
        :type allow_redirects: bool
432
        :param proxies: (optional) Dictionary mapping protocol or protocol and
433
            hostname to the URL of the proxy.
434
        :param stream: (optional) whether to immediately download the response
435
            content. Defaults to ``False``.
436
        :param verify: (optional) whether the SSL cert will be verified.
437
            A CA_BUNDLE path can also be provided. Defaults to ``True``.
438
        :param cert: (optional) if String, path to ssl client cert file (.pem).
439
            If Tuple, ('cert', 'key') pair.
440
        """
441
        # Create the Request.
442
        req = Request(
443
            method = method.upper(),
444
            url = url,
445
            headers = headers,
446
            files = files,
447
            data = data or {},
448
            json = json,
449
            params = params or {},
450
            auth = auth,
451
            cookies = cookies,
452
            hooks = hooks,
453
        )
454
        prep = self.prepare_request(req)
455

    
456
        proxies = proxies or {}
457

    
458
        settings = self.merge_environment_settings(
459
            prep.url, proxies, stream, verify, cert
460
        )
461

    
462
        # Send the request.
463
        send_kwargs = {
464
            'timeout': timeout,
465
            'allow_redirects': allow_redirects,
466
        }
467
        send_kwargs.update(settings)
468
        resp = self.send(prep, **send_kwargs)
469

    
470
        return resp
471

    
472
    def get(self, url, **kwargs):
473
        """Sends a GET request. Returns :class:`Response` object.
474

475
        :param url: URL for the new :class:`Request` object.
476
        :param \*\*kwargs: Optional arguments that ``request`` takes.
477
        """
478

    
479
        kwargs.setdefault('allow_redirects', True)
480
        return self.request('GET', url, **kwargs)
481

    
482
    def options(self, url, **kwargs):
483
        """Sends a OPTIONS request. Returns :class:`Response` object.
484

485
        :param url: URL for the new :class:`Request` object.
486
        :param \*\*kwargs: Optional arguments that ``request`` takes.
487
        """
488

    
489
        kwargs.setdefault('allow_redirects', True)
490
        return self.request('OPTIONS', url, **kwargs)
491

    
492
    def head(self, url, **kwargs):
493
        """Sends a HEAD request. Returns :class:`Response` object.
494

495
        :param url: URL for the new :class:`Request` object.
496
        :param \*\*kwargs: Optional arguments that ``request`` takes.
497
        """
498

    
499
        kwargs.setdefault('allow_redirects', False)
500
        return self.request('HEAD', url, **kwargs)
501

    
502
    def post(self, url, data=None, json=None, **kwargs):
503
        """Sends a POST request. Returns :class:`Response` object.
504

505
        :param url: URL for the new :class:`Request` object.
506
        :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
507
        :param json: (optional) json to send in the body of the :class:`Request`.
508
        :param \*\*kwargs: Optional arguments that ``request`` takes.
509
        """
510

    
511
        return self.request('POST', url, data=data, json=json, **kwargs)
512

    
513
    def put(self, url, data=None, **kwargs):
514
        """Sends a PUT request. Returns :class:`Response` object.
515

516
        :param url: URL for the new :class:`Request` object.
517
        :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
518
        :param \*\*kwargs: Optional arguments that ``request`` takes.
519
        """
520

    
521
        return self.request('PUT', url, data=data, **kwargs)
522

    
523
    def patch(self, url, data=None, **kwargs):
524
        """Sends a PATCH request. Returns :class:`Response` object.
525

526
        :param url: URL for the new :class:`Request` object.
527
        :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
528
        :param \*\*kwargs: Optional arguments that ``request`` takes.
529
        """
530

    
531
        return self.request('PATCH', url,  data=data, **kwargs)
532

    
533
    def delete(self, url, **kwargs):
534
        """Sends a DELETE request. Returns :class:`Response` object.
535

536
        :param url: URL for the new :class:`Request` object.
537
        :param \*\*kwargs: Optional arguments that ``request`` takes.
538
        """
539

    
540
        return self.request('DELETE', url, **kwargs)
541

    
542
    def send(self, request, **kwargs):
543
        """Send a given PreparedRequest."""
544
        # Set defaults that the hooks can utilize to ensure they always have
545
        # the correct parameters to reproduce the previous request.
546
        kwargs.setdefault('stream', self.stream)
547
        kwargs.setdefault('verify', self.verify)
548
        kwargs.setdefault('cert', self.cert)
549
        kwargs.setdefault('proxies', self.proxies)
550

    
551
        # It's possible that users might accidentally send a Request object.
552
        # Guard against that specific failure case.
553
        if not isinstance(request, PreparedRequest):
554
            raise ValueError('You can only send PreparedRequests.')
555

    
556
        checked_urls = set()
557
        while request.url in self.redirect_cache:
558
            checked_urls.add(request.url)
559
            new_url = self.redirect_cache.get(request.url)
560
            if new_url in checked_urls:
561
                break
562
            request.url = new_url
563

    
564
        # Set up variables needed for resolve_redirects and dispatching of hooks
565
        allow_redirects = kwargs.pop('allow_redirects', True)
566
        stream = kwargs.get('stream')
567
        hooks = request.hooks
568

    
569
        # Get the appropriate adapter to use
570
        adapter = self.get_adapter(url=request.url)
571

    
572
        # Start time (approximately) of the request
573
        start = datetime.utcnow()
574

    
575
        # Send the request
576
        r = adapter.send(request, **kwargs)
577

    
578
        # Total elapsed time of the request (approximately)
579
        r.elapsed = datetime.utcnow() - start
580

    
581
        # Response manipulation hooks
582
        r = dispatch_hook('response', hooks, r, **kwargs)
583

    
584
        # Persist cookies
585
        if r.history:
586

    
587
            # If the hooks create history then we want those cookies too
588
            for resp in r.history:
589
                extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
590

    
591
        extract_cookies_to_jar(self.cookies, request, r.raw)
592

    
593
        # Redirect resolving generator.
594
        gen = self.resolve_redirects(r, request, **kwargs)
595

    
596
        # Resolve redirects if allowed.
597
        history = [resp for resp in gen] if allow_redirects else []
598

    
599
        # Shuffle things around if there's history.
600
        if history:
601
            # Insert the first (original) request at the start
602
            history.insert(0, r)
603
            # Get the last request made
604
            r = history.pop()
605
            r.history = history
606

    
607
        if not stream:
608
            r.content
609

    
610
        return r
611

    
612
    def merge_environment_settings(self, url, proxies, stream, verify, cert):
613
        """Check the environment and merge it with some settings."""
614
        # Gather clues from the surrounding environment.
615
        if self.trust_env:
616
            # Set environment's proxies.
617
            env_proxies = get_environ_proxies(url) or {}
618
            for (k, v) in env_proxies.items():
619
                proxies.setdefault(k, v)
620

    
621
            # Look for requests environment configuration and be compatible
622
            # with cURL.
623
            if verify is True or verify is None:
624
                verify = (os.environ.get('REQUESTS_CA_BUNDLE') or
625
                          os.environ.get('CURL_CA_BUNDLE'))
626

    
627
        # Merge all the kwargs.
628
        proxies = merge_setting(proxies, self.proxies)
629
        stream = merge_setting(stream, self.stream)
630
        verify = merge_setting(verify, self.verify)
631
        cert = merge_setting(cert, self.cert)
632

    
633
        return {'verify': verify, 'proxies': proxies, 'stream': stream,
634
                'cert': cert}
635

    
636
    def get_adapter(self, url):
637
        """Returns the appropriate connection adapter for the given URL."""
638
        for (prefix, adapter) in self.adapters.items():
639

    
640
            if url.lower().startswith(prefix):
641
                return adapter
642

    
643
        # Nothing matches :-/
644
        raise InvalidSchema("No connection adapters were found for '%s'" % url)
645

    
646
    def close(self):
647
        """Closes all adapters and as such the session"""
648
        for v in self.adapters.values():
649
            v.close()
650

    
651
    def mount(self, prefix, adapter):
652
        """Registers a connection adapter to a prefix.
653

654
        Adapters are sorted in descending order by key length."""
655

    
656
        self.adapters[prefix] = adapter
657
        keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
658

    
659
        for key in keys_to_move:
660
            self.adapters[key] = self.adapters.pop(key)
661

    
662
    def __getstate__(self):
663
        state = dict((attr, getattr(self, attr, None)) for attr in self.__attrs__)
664
        state['redirect_cache'] = dict(self.redirect_cache)
665
        return state
666

    
667
    def __setstate__(self, state):
668
        redirect_cache = state.pop('redirect_cache', {})
669
        for attr, value in state.items():
670
            setattr(self, attr, value)
671

    
672
        self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
673
        for redirect, to in redirect_cache.items():
674
            self.redirect_cache[redirect] = to
675

    
676

    
677
def session():
678
    """Returns a :class:`Session` for context-management."""
679

    
680
    return Session()