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