OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 |
| 3 """ |
| 4 Compatibility code to be able to use `cookielib.CookieJar` with requests. |
| 5 |
| 6 requests.utils imports from here, so be careful with imports. |
| 7 """ |
| 8 |
| 9 import collections |
| 10 from .compat import cookielib, urlparse, Morsel |
| 11 |
| 12 try: |
| 13 import threading |
| 14 # grr, pyflakes: this fixes "redefinition of unused 'threading'" |
| 15 threading |
| 16 except ImportError: |
| 17 import dummy_threading as threading |
| 18 |
| 19 |
| 20 class MockRequest(object): |
| 21 """Wraps a `requests.Request` to mimic a `urllib2.Request`. |
| 22 |
| 23 The code in `cookielib.CookieJar` expects this interface in order to correct
ly |
| 24 manage cookie policies, i.e., determine whether a cookie can be set, given t
he |
| 25 domains of the request and the cookie. |
| 26 |
| 27 The original request object is read-only. The client is responsible for coll
ecting |
| 28 the new headers via `get_new_headers()` and interpreting them appropriately.
You |
| 29 probably want `get_cookie_header`, defined below. |
| 30 """ |
| 31 |
| 32 def __init__(self, request): |
| 33 self._r = request |
| 34 self._new_headers = {} |
| 35 self.type = urlparse(self._r.url).scheme |
| 36 |
| 37 def get_type(self): |
| 38 return self.type |
| 39 |
| 40 def get_host(self): |
| 41 return urlparse(self._r.url).netloc |
| 42 |
| 43 def get_origin_req_host(self): |
| 44 return self.get_host() |
| 45 |
| 46 def get_full_url(self): |
| 47 return self._r.url |
| 48 |
| 49 def is_unverifiable(self): |
| 50 return True |
| 51 |
| 52 def has_header(self, name): |
| 53 return name in self._r.headers or name in self._new_headers |
| 54 |
| 55 def get_header(self, name, default=None): |
| 56 return self._r.headers.get(name, self._new_headers.get(name, default)) |
| 57 |
| 58 def add_header(self, key, val): |
| 59 """cookielib has no legitimate use for this method; add it back if you f
ind one.""" |
| 60 raise NotImplementedError("Cookie headers should be added with add_unred
irected_header()") |
| 61 |
| 62 def add_unredirected_header(self, name, value): |
| 63 self._new_headers[name] = value |
| 64 |
| 65 def get_new_headers(self): |
| 66 return self._new_headers |
| 67 |
| 68 @property |
| 69 def unverifiable(self): |
| 70 return self.is_unverifiable() |
| 71 |
| 72 @property |
| 73 def origin_req_host(self): |
| 74 return self.get_origin_req_host() |
| 75 |
| 76 |
| 77 class MockResponse(object): |
| 78 """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`. |
| 79 |
| 80 ...what? Basically, expose the parsed HTTP headers from the server response |
| 81 the way `cookielib` expects to see them. |
| 82 """ |
| 83 |
| 84 def __init__(self, headers): |
| 85 """Make a MockResponse for `cookielib` to read. |
| 86 |
| 87 :param headers: a httplib.HTTPMessage or analogous carrying the headers |
| 88 """ |
| 89 self._headers = headers |
| 90 |
| 91 def info(self): |
| 92 return self._headers |
| 93 |
| 94 def getheaders(self, name): |
| 95 self._headers.getheaders(name) |
| 96 |
| 97 |
| 98 def extract_cookies_to_jar(jar, request, response): |
| 99 """Extract the cookies from the response into a CookieJar. |
| 100 |
| 101 :param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar) |
| 102 :param request: our own requests.Request object |
| 103 :param response: urllib3.HTTPResponse object |
| 104 """ |
| 105 # the _original_response field is the wrapped httplib.HTTPResponse object, |
| 106 req = MockRequest(request) |
| 107 # pull out the HTTPMessage with the headers and put it in the mock: |
| 108 res = MockResponse(response._original_response.msg) |
| 109 jar.extract_cookies(res, req) |
| 110 |
| 111 |
| 112 def get_cookie_header(jar, request): |
| 113 """Produce an appropriate Cookie header string to be sent with `request`, or
None.""" |
| 114 r = MockRequest(request) |
| 115 jar.add_cookie_header(r) |
| 116 return r.get_new_headers().get('Cookie') |
| 117 |
| 118 |
| 119 def remove_cookie_by_name(cookiejar, name, domain=None, path=None): |
| 120 """Unsets a cookie by name, by default over all domains and paths. |
| 121 |
| 122 Wraps CookieJar.clear(), is O(n). |
| 123 """ |
| 124 clearables = [] |
| 125 for cookie in cookiejar: |
| 126 if cookie.name == name: |
| 127 if domain is None or domain == cookie.domain: |
| 128 if path is None or path == cookie.path: |
| 129 clearables.append((cookie.domain, cookie.path, cookie.name)) |
| 130 |
| 131 for domain, path, name in clearables: |
| 132 cookiejar.clear(domain, path, name) |
| 133 |
| 134 |
| 135 class CookieConflictError(RuntimeError): |
| 136 """There are two cookies that meet the criteria specified in the cookie jar. |
| 137 Use .get and .set and include domain and path args in order to be more speci
fic.""" |
| 138 |
| 139 |
| 140 class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping): |
| 141 """Compatibility class; is a cookielib.CookieJar, but exposes a dict interfa
ce. |
| 142 |
| 143 This is the CookieJar we create by default for requests and sessions that |
| 144 don't specify one, since some clients may expect response.cookies and |
| 145 session.cookies to support dict operations. |
| 146 |
| 147 Don't use the dict interface internally; it's just for compatibility with |
| 148 with external client code. All `requests` code should work out of the box |
| 149 with externally provided instances of CookieJar, e.g., LWPCookieJar and |
| 150 FileCookieJar. |
| 151 |
| 152 Caution: dictionary operations that are normally O(1) may be O(n). |
| 153 |
| 154 Unlike a regular CookieJar, this class is pickleable. |
| 155 """ |
| 156 |
| 157 def get(self, name, default=None, domain=None, path=None): |
| 158 """Dict-like get() that also supports optional domain and path args in |
| 159 order to resolve naming collisions from using one cookie jar over |
| 160 multiple domains. Caution: operation is O(n), not O(1).""" |
| 161 try: |
| 162 return self._find_no_duplicates(name, domain, path) |
| 163 except KeyError: |
| 164 return default |
| 165 |
| 166 def set(self, name, value, **kwargs): |
| 167 """Dict-like set() that also supports optional domain and path args in |
| 168 order to resolve naming collisions from using one cookie jar over |
| 169 multiple domains.""" |
| 170 # support client code that unsets cookies by assignment of a None value: |
| 171 if value is None: |
| 172 remove_cookie_by_name(self, name, domain=kwargs.get('domain'), path=
kwargs.get('path')) |
| 173 return |
| 174 |
| 175 if isinstance(value, Morsel): |
| 176 c = morsel_to_cookie(value) |
| 177 else: |
| 178 c = create_cookie(name, value, **kwargs) |
| 179 self.set_cookie(c) |
| 180 return c |
| 181 |
| 182 def keys(self): |
| 183 """Dict-like keys() that returns a list of names of cookies from the jar
. |
| 184 See values() and items().""" |
| 185 keys = [] |
| 186 for cookie in iter(self): |
| 187 keys.append(cookie.name) |
| 188 return keys |
| 189 |
| 190 def values(self): |
| 191 """Dict-like values() that returns a list of values of cookies from the
jar. |
| 192 See keys() and items().""" |
| 193 values = [] |
| 194 for cookie in iter(self): |
| 195 values.append(cookie.value) |
| 196 return values |
| 197 |
| 198 def items(self): |
| 199 """Dict-like items() that returns a list of name-value tuples from the j
ar. |
| 200 See keys() and values(). Allows client-code to call "dict(RequestsCookie
Jar) |
| 201 and get a vanilla python dict of key value pairs.""" |
| 202 items = [] |
| 203 for cookie in iter(self): |
| 204 items.append((cookie.name, cookie.value)) |
| 205 return items |
| 206 |
| 207 def list_domains(self): |
| 208 """Utility method to list all the domains in the jar.""" |
| 209 domains = [] |
| 210 for cookie in iter(self): |
| 211 if cookie.domain not in domains: |
| 212 domains.append(cookie.domain) |
| 213 return domains |
| 214 |
| 215 def list_paths(self): |
| 216 """Utility method to list all the paths in the jar.""" |
| 217 paths = [] |
| 218 for cookie in iter(self): |
| 219 if cookie.path not in paths: |
| 220 paths.append(cookie.path) |
| 221 return paths |
| 222 |
| 223 def multiple_domains(self): |
| 224 """Returns True if there are multiple domains in the jar. |
| 225 Returns False otherwise.""" |
| 226 domains = [] |
| 227 for cookie in iter(self): |
| 228 if cookie.domain is not None and cookie.domain in domains: |
| 229 return True |
| 230 domains.append(cookie.domain) |
| 231 return False # there is only one domain in jar |
| 232 |
| 233 def get_dict(self, domain=None, path=None): |
| 234 """Takes as an argument an optional domain and path and returns a plain
old |
| 235 Python dict of name-value pairs of cookies that meet the requirements.""
" |
| 236 dictionary = {} |
| 237 for cookie in iter(self): |
| 238 if (domain is None or cookie.domain == domain) and (path is None |
| 239 or cookie.path == path): |
| 240 dictionary[cookie.name] = cookie.value |
| 241 return dictionary |
| 242 |
| 243 def __getitem__(self, name): |
| 244 """Dict-like __getitem__() for compatibility with client code. Throws ex
ception |
| 245 if there are more than one cookie with name. In that case, use the more |
| 246 explicit get() method instead. Caution: operation is O(n), not O(1).""" |
| 247 |
| 248 return self._find_no_duplicates(name) |
| 249 |
| 250 def __setitem__(self, name, value): |
| 251 """Dict-like __setitem__ for compatibility with client code. Throws exce
ption |
| 252 if there is already a cookie of that name in the jar. In that case, use
the more |
| 253 explicit set() method instead.""" |
| 254 |
| 255 self.set(name, value) |
| 256 |
| 257 def __delitem__(self, name): |
| 258 """Deletes a cookie given a name. Wraps cookielib.CookieJar's remove_coo
kie_by_name().""" |
| 259 remove_cookie_by_name(self, name) |
| 260 |
| 261 def update(self, other): |
| 262 """Updates this jar with cookies from another CookieJar or dict-like""" |
| 263 if isinstance(other, cookielib.CookieJar): |
| 264 for cookie in other: |
| 265 self.set_cookie(cookie) |
| 266 else: |
| 267 super(RequestsCookieJar, self).update(other) |
| 268 |
| 269 def _find(self, name, domain=None, path=None): |
| 270 """Requests uses this method internally to get cookie values. Takes as a
rgs name |
| 271 and optional domain and path. Returns a cookie.value. If there are confl
icting cookies, |
| 272 _find arbitrarily chooses one. See _find_no_duplicates if you want an ex
ception thrown |
| 273 if there are conflicting cookies.""" |
| 274 for cookie in iter(self): |
| 275 if cookie.name == name: |
| 276 if domain is None or cookie.domain == domain: |
| 277 if path is None or cookie.path == path: |
| 278 return cookie.value |
| 279 |
| 280 raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path)) |
| 281 |
| 282 def _find_no_duplicates(self, name, domain=None, path=None): |
| 283 """__get_item__ and get call _find_no_duplicates -- never used in Reques
ts internally. |
| 284 Takes as args name and optional domain and path. Returns a cookie.value. |
| 285 Throws KeyError if cookie is not found and CookieConflictError if there
are |
| 286 multiple cookies that match name and optionally domain and path.""" |
| 287 toReturn = None |
| 288 for cookie in iter(self): |
| 289 if cookie.name == name: |
| 290 if domain is None or cookie.domain == domain: |
| 291 if path is None or cookie.path == path: |
| 292 if toReturn is not None: # if there are multiple cookie
s that meet passed in criteria |
| 293 raise CookieConflictError('There are multiple cookie
s with name, %r' % (name)) |
| 294 toReturn = cookie.value # we will eventually return thi
s as long as no cookie conflict |
| 295 |
| 296 if toReturn: |
| 297 return toReturn |
| 298 raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path)) |
| 299 |
| 300 def __getstate__(self): |
| 301 """Unlike a normal CookieJar, this class is pickleable.""" |
| 302 state = self.__dict__.copy() |
| 303 # remove the unpickleable RLock object |
| 304 state.pop('_cookies_lock') |
| 305 return state |
| 306 |
| 307 def __setstate__(self, state): |
| 308 """Unlike a normal CookieJar, this class is pickleable.""" |
| 309 self.__dict__.update(state) |
| 310 if '_cookies_lock' not in self.__dict__: |
| 311 self._cookies_lock = threading.RLock() |
| 312 |
| 313 def copy(self): |
| 314 """Return a copy of this RequestsCookieJar.""" |
| 315 new_cj = RequestsCookieJar() |
| 316 new_cj.update(self) |
| 317 return new_cj |
| 318 |
| 319 |
| 320 def create_cookie(name, value, **kwargs): |
| 321 """Make a cookie from underspecified parameters. |
| 322 |
| 323 By default, the pair of `name` and `value` will be set for the domain '' |
| 324 and sent on every request (this is sometimes called a "supercookie"). |
| 325 """ |
| 326 result = dict( |
| 327 version=0, |
| 328 name=name, |
| 329 value=value, |
| 330 port=None, |
| 331 domain='', |
| 332 path='/', |
| 333 secure=False, |
| 334 expires=None, |
| 335 discard=True, |
| 336 comment=None, |
| 337 comment_url=None, |
| 338 rest={'HttpOnly': None}, |
| 339 rfc2109=False,) |
| 340 |
| 341 badargs = set(kwargs) - set(result) |
| 342 if badargs: |
| 343 err = 'create_cookie() got unexpected keyword arguments: %s' |
| 344 raise TypeError(err % list(badargs)) |
| 345 |
| 346 result.update(kwargs) |
| 347 result['port_specified'] = bool(result['port']) |
| 348 result['domain_specified'] = bool(result['domain']) |
| 349 result['domain_initial_dot'] = result['domain'].startswith('.') |
| 350 result['path_specified'] = bool(result['path']) |
| 351 |
| 352 return cookielib.Cookie(**result) |
| 353 |
| 354 |
| 355 def morsel_to_cookie(morsel): |
| 356 """Convert a Morsel object into a Cookie containing the one k/v pair.""" |
| 357 c = create_cookie( |
| 358 name=morsel.key, |
| 359 value=morsel.value, |
| 360 version=morsel['version'] or 0, |
| 361 port=None, |
| 362 port_specified=False, |
| 363 domain=morsel['domain'], |
| 364 domain_specified=bool(morsel['domain']), |
| 365 domain_initial_dot=morsel['domain'].startswith('.'), |
| 366 path=morsel['path'], |
| 367 path_specified=bool(morsel['path']), |
| 368 secure=bool(morsel['secure']), |
| 369 expires=morsel['max-age'] or morsel['expires'], |
| 370 discard=False, |
| 371 comment=morsel['comment'], |
| 372 comment_url=bool(morsel['comment']), |
| 373 rest={'HttpOnly': morsel['httponly']}, |
| 374 rfc2109=False,) |
| 375 return c |
| 376 |
| 377 |
| 378 def cookiejar_from_dict(cookie_dict, cookiejar=None): |
| 379 """Returns a CookieJar from a key/value dictionary. |
| 380 |
| 381 :param cookie_dict: Dict of key/values to insert into CookieJar. |
| 382 """ |
| 383 if cookiejar is None: |
| 384 cookiejar = RequestsCookieJar() |
| 385 |
| 386 if cookie_dict is not None: |
| 387 for name in cookie_dict: |
| 388 cookiejar.set_cookie(create_cookie(name, cookie_dict[name])) |
| 389 return cookiejar |
OLD | NEW |