1 # -*- coding: utf-8 -*-
4 Compatibility code to be able to use `cookielib.CookieJar` with requests.
6 requests.utils imports from here, so be careful with imports.
13 from .compat import cookielib, urlparse, urlunparse, Morsel
17 # grr, pyflakes: this fixes "redefinition of unused 'threading'"
20 import dummy_threading as threading
23 class MockRequest(object):
24 """Wraps a `requests.Request` to mimic a `urllib2.Request`.
26 The code in `cookielib.CookieJar` expects this interface in order to correctly
27 manage cookie policies, i.e., determine whether a cookie can be set, given the
28 domains of the request and the cookie.
30 The original request object is read-only. The client is responsible for collecting
31 the new headers via `get_new_headers()` and interpreting them appropriately. You
32 probably want `get_cookie_header`, defined below.
35 def __init__(self, request):
37 self._new_headers = {}
38 self.type = urlparse(self._r.url).scheme
44 return urlparse(self._r.url).netloc
46 def get_origin_req_host(self):
47 return self.get_host()
49 def get_full_url(self):
50 # Only return the response's URL if the user hadn't set the Host
52 if not self._r.headers.get('Host'):
54 # If they did set it, retrieve it and reconstruct the expected domain
55 host = self._r.headers['Host']
56 parsed = urlparse(self._r.url)
57 # Reconstruct the URL as we expect it
59 parsed.scheme, host, parsed.path, parsed.params, parsed.query,
63 def is_unverifiable(self):
66 def has_header(self, name):
67 return name in self._r.headers or name in self._new_headers
69 def get_header(self, name, default=None):
70 return self._r.headers.get(name, self._new_headers.get(name, default))
72 def add_header(self, key, val):
73 """cookielib has no legitimate use for this method; add it back if you find one."""
74 raise NotImplementedError("Cookie headers should be added with add_unredirected_header()")
76 def add_unredirected_header(self, name, value):
77 self._new_headers[name] = value
79 def get_new_headers(self):
80 return self._new_headers
83 def unverifiable(self):
84 return self.is_unverifiable()
87 def origin_req_host(self):
88 return self.get_origin_req_host()
92 return self.get_host()
95 class MockResponse(object):
96 """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
98 ...what? Basically, expose the parsed HTTP headers from the server response
99 the way `cookielib` expects to see them.
102 def __init__(self, headers):
103 """Make a MockResponse for `cookielib` to read.
105 :param headers: a httplib.HTTPMessage or analogous carrying the headers
107 self._headers = headers
112 def getheaders(self, name):
113 self._headers.getheaders(name)
116 def extract_cookies_to_jar(jar, request, response):
117 """Extract the cookies from the response into a CookieJar.
119 :param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar)
120 :param request: our own requests.Request object
121 :param response: urllib3.HTTPResponse object
123 if not (hasattr(response, '_original_response') and
124 response._original_response):
126 # the _original_response field is the wrapped httplib.HTTPResponse object,
127 req = MockRequest(request)
128 # pull out the HTTPMessage with the headers and put it in the mock:
129 res = MockResponse(response._original_response.msg)
130 jar.extract_cookies(res, req)
133 def get_cookie_header(jar, request):
134 """Produce an appropriate Cookie header string to be sent with `request`, or None."""
135 r = MockRequest(request)
136 jar.add_cookie_header(r)
137 return r.get_new_headers().get('Cookie')
140 def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
141 """Unsets a cookie by name, by default over all domains and paths.
143 Wraps CookieJar.clear(), is O(n).
146 for cookie in cookiejar:
147 if cookie.name != name:
149 if domain is not None and domain != cookie.domain:
151 if path is not None and path != cookie.path:
153 clearables.append((cookie.domain, cookie.path, cookie.name))
155 for domain, path, name in clearables:
156 cookiejar.clear(domain, path, name)
159 class CookieConflictError(RuntimeError):
160 """There are two cookies that meet the criteria specified in the cookie jar.
161 Use .get and .set and include domain and path args in order to be more specific."""
164 class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
165 """Compatibility class; is a cookielib.CookieJar, but exposes a dict
168 This is the CookieJar we create by default for requests and sessions that
169 don't specify one, since some clients may expect response.cookies and
170 session.cookies to support dict operations.
172 Requests does not use the dict interface internally; it's just for
173 compatibility with external client code. All requests code should work
174 out of the box with externally provided instances of ``CookieJar``, e.g.
175 ``LWPCookieJar`` and ``FileCookieJar``.
177 Unlike a regular CookieJar, this class is pickleable.
179 .. warning:: dictionary operations that are normally O(1) may be O(n).
181 def get(self, name, default=None, domain=None, path=None):
182 """Dict-like get() that also supports optional domain and path args in
183 order to resolve naming collisions from using one cookie jar over
186 .. warning:: operation is O(n), not O(1)."""
188 return self._find_no_duplicates(name, domain, path)
192 def set(self, name, value, **kwargs):
193 """Dict-like set() that also supports optional domain and path args in
194 order to resolve naming collisions from using one cookie jar over
196 # support client code that unsets cookies by assignment of a None value:
198 remove_cookie_by_name(self, name, domain=kwargs.get('domain'), path=kwargs.get('path'))
201 if isinstance(value, Morsel):
202 c = morsel_to_cookie(value)
204 c = create_cookie(name, value, **kwargs)
209 """Dict-like iterkeys() that returns an iterator of names of cookies
210 from the jar. See itervalues() and iteritems()."""
211 for cookie in iter(self):
215 """Dict-like keys() that returns a list of names of cookies from the
216 jar. See values() and items()."""
217 return list(self.iterkeys())
219 def itervalues(self):
220 """Dict-like itervalues() that returns an iterator of values of cookies
221 from the jar. See iterkeys() and iteritems()."""
222 for cookie in iter(self):
226 """Dict-like values() that returns a list of values of cookies from the
227 jar. See keys() and items()."""
228 return list(self.itervalues())
231 """Dict-like iteritems() that returns an iterator of name-value tuples
232 from the jar. See iterkeys() and itervalues()."""
233 for cookie in iter(self):
234 yield cookie.name, cookie.value
237 """Dict-like items() that returns a list of name-value tuples from the
238 jar. See keys() and values(). Allows client-code to call
239 ``dict(RequestsCookieJar)`` and get a vanilla python dict of key value
241 return list(self.iteritems())
243 def list_domains(self):
244 """Utility method to list all the domains in the jar."""
246 for cookie in iter(self):
247 if cookie.domain not in domains:
248 domains.append(cookie.domain)
251 def list_paths(self):
252 """Utility method to list all the paths in the jar."""
254 for cookie in iter(self):
255 if cookie.path not in paths:
256 paths.append(cookie.path)
259 def multiple_domains(self):
260 """Returns True if there are multiple domains in the jar.
261 Returns False otherwise."""
263 for cookie in iter(self):
264 if cookie.domain is not None and cookie.domain in domains:
266 domains.append(cookie.domain)
267 return False # there is only one domain in jar
269 def get_dict(self, domain=None, path=None):
270 """Takes as an argument an optional domain and path and returns a plain
271 old Python dict of name-value pairs of cookies that meet the
274 for cookie in iter(self):
275 if (domain is None or cookie.domain == domain) and (path is None
276 or cookie.path == path):
277 dictionary[cookie.name] = cookie.value
280 def __contains__(self, name):
282 return super(RequestsCookieJar, self).__contains__(name)
283 except CookieConflictError:
286 def __getitem__(self, name):
287 """Dict-like __getitem__() for compatibility with client code. Throws
288 exception if there are more than one cookie with name. In that case,
289 use the more explicit get() method instead.
291 .. warning:: operation is O(n), not O(1)."""
293 return self._find_no_duplicates(name)
295 def __setitem__(self, name, value):
296 """Dict-like __setitem__ for compatibility with client code. Throws
297 exception if there is already a cookie of that name in the jar. In that
298 case, use the more explicit set() method instead."""
300 self.set(name, value)
302 def __delitem__(self, name):
303 """Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s
304 ``remove_cookie_by_name()``."""
305 remove_cookie_by_name(self, name)
307 def set_cookie(self, cookie, *args, **kwargs):
308 if hasattr(cookie.value, 'startswith') and cookie.value.startswith('"') and cookie.value.endswith('"'):
309 cookie.value = cookie.value.replace('\\"', '')
310 return super(RequestsCookieJar, self).set_cookie(cookie, *args, **kwargs)
312 def update(self, other):
313 """Updates this jar with cookies from another CookieJar or dict-like"""
314 if isinstance(other, cookielib.CookieJar):
316 self.set_cookie(copy.copy(cookie))
318 super(RequestsCookieJar, self).update(other)
320 def _find(self, name, domain=None, path=None):
321 """Requests uses this method internally to get cookie values. Takes as
322 args name and optional domain and path. Returns a cookie.value. If
323 there are conflicting cookies, _find arbitrarily chooses one. See
324 _find_no_duplicates if you want an exception thrown if there are
325 conflicting cookies."""
326 for cookie in iter(self):
327 if cookie.name == name:
328 if domain is None or cookie.domain == domain:
329 if path is None or cookie.path == path:
332 raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
334 def _find_no_duplicates(self, name, domain=None, path=None):
335 """Both ``__get_item__`` and ``get`` call this function: it's never
336 used elsewhere in Requests. Takes as args name and optional domain and
337 path. Returns a cookie.value. Throws KeyError if cookie is not found
338 and CookieConflictError if there are multiple cookies that match name
339 and optionally domain and path."""
341 for cookie in iter(self):
342 if cookie.name == name:
343 if domain is None or cookie.domain == domain:
344 if path is None or cookie.path == path:
345 if toReturn is not None: # if there are multiple cookies that meet passed in criteria
346 raise CookieConflictError('There are multiple cookies with name, %r' % (name))
347 toReturn = cookie.value # we will eventually return this as long as no cookie conflict
351 raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
353 def __getstate__(self):
354 """Unlike a normal CookieJar, this class is pickleable."""
355 state = self.__dict__.copy()
356 # remove the unpickleable RLock object
357 state.pop('_cookies_lock')
360 def __setstate__(self, state):
361 """Unlike a normal CookieJar, this class is pickleable."""
362 self.__dict__.update(state)
363 if '_cookies_lock' not in self.__dict__:
364 self._cookies_lock = threading.RLock()
367 """Return a copy of this RequestsCookieJar."""
368 new_cj = RequestsCookieJar()
373 def _copy_cookie_jar(jar):
377 if hasattr(jar, 'copy'):
378 # We're dealing with an instance of RequestsCookieJar
380 # We're dealing with a generic CookieJar instance
381 new_jar = copy.copy(jar)
384 new_jar.set_cookie(copy.copy(cookie))
388 def create_cookie(name, value, **kwargs):
389 """Make a cookie from underspecified parameters.
391 By default, the pair of `name` and `value` will be set for the domain ''
392 and sent on every request (this is sometimes called a "supercookie").
406 rest={'HttpOnly': None},
409 badargs = set(kwargs) - set(result)
411 err = 'create_cookie() got unexpected keyword arguments: %s'
412 raise TypeError(err % list(badargs))
414 result.update(kwargs)
415 result['port_specified'] = bool(result['port'])
416 result['domain_specified'] = bool(result['domain'])
417 result['domain_initial_dot'] = result['domain'].startswith('.')
418 result['path_specified'] = bool(result['path'])
420 return cookielib.Cookie(**result)
423 def morsel_to_cookie(morsel):
424 """Convert a Morsel object into a Cookie containing the one k/v pair."""
427 if morsel['max-age']:
429 expires = int(time.time() + int(morsel['max-age']))
431 raise TypeError('max-age: %s must be integer' % morsel['max-age'])
432 elif morsel['expires']:
433 time_template = '%a, %d-%b-%Y %H:%M:%S GMT'
434 expires = calendar.timegm(
435 time.strptime(morsel['expires'], time_template)
437 return create_cookie(
438 comment=morsel['comment'],
439 comment_url=bool(morsel['comment']),
441 domain=morsel['domain'],
446 rest={'HttpOnly': morsel['httponly']},
448 secure=bool(morsel['secure']),
450 version=morsel['version'] or 0,
454 def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
455 """Returns a CookieJar from a key/value dictionary.
457 :param cookie_dict: Dict of key/values to insert into CookieJar.
458 :param cookiejar: (optional) A cookiejar to add the cookies to.
459 :param overwrite: (optional) If False, will not replace cookies
460 already in the jar with new ones.
462 if cookiejar is None:
463 cookiejar = RequestsCookieJar()
465 if cookie_dict is not None:
466 names_from_jar = [cookie.name for cookie in cookiejar]
467 for name in cookie_dict:
468 if overwrite or (name not in names_from_jar):
469 cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))
474 def merge_cookies(cookiejar, cookies):
475 """Add cookies to cookiejar and returns a merged CookieJar.
477 :param cookiejar: CookieJar object to add the cookies to.
478 :param cookies: Dictionary or CookieJar object to be added.
480 if not isinstance(cookiejar, cookielib.CookieJar):
481 raise ValueError('You can only merge into CookieJar')
483 if isinstance(cookies, dict):
484 cookiejar = cookiejar_from_dict(
485 cookies, cookiejar=cookiejar, overwrite=False)
486 elif isinstance(cookies, cookielib.CookieJar):
488 cookiejar.update(cookies)
489 except AttributeError:
490 for cookie_in_jar in cookies:
491 cookiejar.set_cookie(cookie_in_jar)