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