From 2dc556e0825bf3a235140c5af6b473945ebf4ab8 Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Tue, 23 Aug 2011 15:06:22 +0200 Subject: [PATCH 01/10] Added multiple hooks support. --- requests/api.py | 10 +++++----- requests/hooks.py | 26 +++++++++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/requests/api.py b/requests/api.py index 0928d8c8..9acc1e5f 100644 --- a/requests/api.py +++ b/requests/api.py @@ -14,7 +14,7 @@ This module impliments the Requests API. import config from .models import Request, Response, AuthObject from .status_codes import codes -from .hooks import dispatch_hook +from .hooks import dispatch_hooks from .utils import cookiejar_from_dict from urlparse import urlparse @@ -63,21 +63,21 @@ def request(method, url, ) # Arguments manipulation hook. - args = dispatch_hook('args', hooks, args) + args = dispatch_hooks('args', hooks, args) r = Request(**args) # Pre-request hook. - r = dispatch_hook('pre_request', hooks, r) + r = dispatch_hooks('pre_request', hooks, r) # Send the HTTP Request. r.send() # Post-request hook. - r = dispatch_hook('post_request', hooks, r) + r = dispatch_hooks('post_request', hooks, r) # Response manipulation hook. - r.response = dispatch_hook('response', hooks, r.response) + r.response = dispatch_hooks('response', hooks, r.response) return r.response diff --git a/requests/hooks.py b/requests/hooks.py index 2938029b..2fa97d0b 100644 --- a/requests/hooks.py +++ b/requests/hooks.py @@ -23,18 +23,26 @@ Available hooks: """ import warnings +from collections import Iterable +def dispatch_hooks(key, hooks, hook_data): + """Dispatches multiple hooks on a given piece of data. -def dispatch_hook(key, hooks, hook_data): - """Dipatches a hook dictionary on a given peice of data.""" - - hooks = hooks or dict() - - if key in hooks: + :param key: the hooks group to lookup + :type key: str + :param hooks: the hooks dictionary. The value of each key can be a callable + object, or a list of callable objects. + :type hooks: dict + :param hook_data: the object on witch the hooks should be applied + :type hook_data: object + """ + hook_list = hooks.get(key, []) if hooks else [] + dispatching = hook_list if isinstance(hook_list, Iterable) else [hook_list] + for hook in dispatching: try: - return hooks.get(key).__call__(hook_data) or hook_data - + # hook must be a callable + hook_data = hook(hook_data) except Exception, why: warnings.warn(str(why)) - return hook_data + From d12eac8669bc820c7fdd4efe47032112a6b41a1b Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Tue, 23 Aug 2011 23:30:50 +0200 Subject: [PATCH 02/10] * hooks is a package now. * Added hooks.setup_hooks to setup multiple and default hooks * Added hooks.response hooks to unicode encoding, and uncompress response. * Modified hooks.dispatch_hooks api: don't need a key any more. --- requests/api.py | 16 +++++++-------- requests/config.py | 4 +++- requests/{hooks.py => hooks/__init__.py} | 23 ++++++++++++++++----- requests/models.py | 26 +++++++++++------------- 4 files changed, 41 insertions(+), 28 deletions(-) rename requests/{hooks.py => hooks/__init__.py} (61%) diff --git a/requests/api.py b/requests/api.py index 9acc1e5f..9c78c17f 100644 --- a/requests/api.py +++ b/requests/api.py @@ -14,7 +14,7 @@ This module impliments the Requests API. import config from .models import Request, Response, AuthObject from .status_codes import codes -from .hooks import dispatch_hooks +from hooks import setup_hooks, dispatch_hooks from .utils import cookiejar_from_dict from urlparse import urlparse @@ -43,10 +43,9 @@ def request(method, url, method = str(method).upper() - if cookies is None: - cookies = {} + cookies = cookiejar_from_dict(cookies or dict()) - cookies = cookiejar_from_dict(cookies) + hooks = setup_hooks(hooks or dict()) args = dict( method = method, @@ -61,23 +60,24 @@ def request(method, url, allow_redirects = allow_redirects, proxies = proxies or config.settings.proxies, ) + # Arguments manipulation hook. - args = dispatch_hooks('args', hooks, args) + args = dispatch_hooks(hooks.get('args', []), args) r = Request(**args) # Pre-request hook. - r = dispatch_hooks('pre_request', hooks, r) + r = dispatch_hooks(hooks.get('pre_request', []), r) # Send the HTTP Request. r.send() # Post-request hook. - r = dispatch_hooks('post_request', hooks, r) + r = dispatch_hooks(hooks.get('post_request', []), hooks, r) # Response manipulation hook. - r.response = dispatch_hooks('response', hooks, r.response) + r.response = dispatch_hooks(hooks.get('response', []), r.response) return r.response diff --git a/requests/config.py b/requests/config.py index 794109c5..a92e1f57 100644 --- a/requests/config.py +++ b/requests/config.py @@ -62,7 +62,9 @@ settings.proxies = None settings.verbose = None settings.timeout = None settings.max_redirects = 30 -settings.decode_unicode = True +# settings.decode_unicode = True +settings.unicode_response = True +settings.decode_response = True #: Use socket.setdefaulttimeout() as fallback? settings.timeout_fallback = True diff --git a/requests/hooks.py b/requests/hooks/__init__.py similarity index 61% rename from requests/hooks.py rename to requests/hooks/__init__.py index 2fa97d0b..8f7c6b1c 100644 --- a/requests/hooks.py +++ b/requests/hooks/__init__.py @@ -24,8 +24,24 @@ Available hooks: import warnings from collections import Iterable +from .. import config +from response import unicode_response, decode_response -def dispatch_hooks(key, hooks, hook_data): +def setup_hooks(hooks): + """Setup hooks as a dictionary. Each value is a set of hooks.""" + + for key, values in hooks.items(): + hook_list = values if isinstance(values, Iterable) else [values] + hooks[key] = set(hook_list) + + # Also, based on settings, + if config.settings.unicode_response: + hooks.setdefault('response', set()).add(unicode_response) + if config.settings.decode_response: + hooks.setdefault('response', set()).add(decode_response) + return hooks + +def dispatch_hooks(hooks, hook_data): """Dispatches multiple hooks on a given piece of data. :param key: the hooks group to lookup @@ -36,13 +52,10 @@ def dispatch_hooks(key, hooks, hook_data): :param hook_data: the object on witch the hooks should be applied :type hook_data: object """ - hook_list = hooks.get(key, []) if hooks else [] - dispatching = hook_list if isinstance(hook_list, Iterable) else [hook_list] - for hook in dispatching: + for hook in hooks: try: # hook must be a callable hook_data = hook(hook_data) except Exception, why: warnings.warn(str(why)) return hook_data - diff --git a/requests/models.py b/requests/models.py index 5983c237..e199b3ca 100644 --- a/requests/models.py +++ b/requests/models.py @@ -444,22 +444,20 @@ class Response(object): (if available). """ - if self._content is not None: - return self._content + if self._content is None: + # Read the contents. + self._content = self.fo.read() - # Read the contents. - self._content = self.fo.read() + # # Decode GZip'd content. + # if 'gzip' in self.headers.get('content-encoding', ''): + # try: + # self._content = decode_gzip(self._content) + # except zlib.error: + # pass - # Decode GZip'd content. - if 'gzip' in self.headers.get('content-encoding', ''): - try: - self._content = decode_gzip(self._content) - except zlib.error: - pass - - # Decode unicode content. - if settings.decode_unicode: - self._content = get_unicode_from_response(self) + # # Decode unicode content. + # if settings.decode_unicode: + # self._content = get_unicode_from_response(self) return self._content From 660e0903157579fae39d72c1ac7af45a1f6dc9d1 Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Tue, 23 Aug 2011 23:39:07 +0200 Subject: [PATCH 03/10] Forgot to add the hooks sub-package --- requests/hooks/args.py | 7 ++++++ requests/hooks/post_request.py | 7 ++++++ requests/hooks/pre_request.py | 7 ++++++ requests/hooks/response.py | 43 ++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 requests/hooks/args.py create mode 100644 requests/hooks/post_request.py create mode 100644 requests/hooks/pre_request.py create mode 100644 requests/hooks/response.py diff --git a/requests/hooks/args.py b/requests/hooks/args.py new file mode 100644 index 00000000..6ac6fb6c --- /dev/null +++ b/requests/hooks/args.py @@ -0,0 +1,7 @@ +""" +request.hooks.args +~~~~~~~~~~~~~~~~~~ + +This module provide a collection of args hooks. +""" + diff --git a/requests/hooks/post_request.py b/requests/hooks/post_request.py new file mode 100644 index 00000000..3a4ff543 --- /dev/null +++ b/requests/hooks/post_request.py @@ -0,0 +1,7 @@ +""" +request.hooks.post_request +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module provide a collection of post_request hooks. +""" + diff --git a/requests/hooks/pre_request.py b/requests/hooks/pre_request.py new file mode 100644 index 00000000..ca1e715f --- /dev/null +++ b/requests/hooks/pre_request.py @@ -0,0 +1,7 @@ +""" +request.hooks.pre_request +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module provide a collection of pre_request hooks. +""" + diff --git a/requests/hooks/response.py b/requests/hooks/response.py new file mode 100644 index 00000000..fc3d26dc --- /dev/null +++ b/requests/hooks/response.py @@ -0,0 +1,43 @@ +""" +request.hooks.response +~~~~~~~~~~~~~~~~~~~~~~ + +This module provide a collection of response hooks. +""" +from functools import wraps +import zlib +from cgi import parse_header + +#: Dictionary of content decoders. +decoders = { + # No decoding applied. + 'identity': lambda r: r, + # Decode Response file object compressed with deflate. + 'deflate': lambda r: zlib.decompress(r.content), + # Decode Response file object compressed with gzip. + 'gzip': lambda r: zlib.decompress(r.content, 16+zlib.MAX_WBITS), +} + +# Decode Response file object compressed with compress. +decoders['compress'] = decoders['deflate'] + +try: + import bz2 +except ImportError: + pass +else: + # Decode Response file object compressed with bz2. + decoders['bzip2'] = lambda r: bz2.decompress(r.content) + +def unicode_response(r): + """Encode response file object in unicode.""" + content_type, params = parse_header(r.headers.get('content-type')) + charset = params.get('charset', '').strip("'\"") + r.content = unicode(r.content, charset) if charset else unicode(r.content) + return r + +def decode_response(r): + """Decode compressed response content using Contetn-Encoding header.""" + encoding = r.headers.get('content-encoding') + return decoders.get(encoding)(r) + From 56406b3442b20a3ea5536cabbd10477cc36244b8 Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Tue, 23 Aug 2011 23:43:40 +0200 Subject: [PATCH 04/10] hooks now comply hook interface --- requests/hooks/response.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requests/hooks/response.py b/requests/hooks/response.py index fc3d26dc..e9ce39b3 100644 --- a/requests/hooks/response.py +++ b/requests/hooks/response.py @@ -11,7 +11,7 @@ from cgi import parse_header #: Dictionary of content decoders. decoders = { # No decoding applied. - 'identity': lambda r: r, + 'identity': lambda r: r.content, # Decode Response file object compressed with deflate. 'deflate': lambda r: zlib.decompress(r.content), # Decode Response file object compressed with gzip. @@ -33,11 +33,12 @@ def unicode_response(r): """Encode response file object in unicode.""" content_type, params = parse_header(r.headers.get('content-type')) charset = params.get('charset', '').strip("'\"") - r.content = unicode(r.content, charset) if charset else unicode(r.content) + r._content = unicode(r.content, charset) if charset else unicode(r.content) return r def decode_response(r): """Decode compressed response content using Contetn-Encoding header.""" encoding = r.headers.get('content-encoding') - return decoders.get(encoding)(r) + r._content = decoders.get(encoding)(r) + return r From c243d0939828423bc5af92b07a4026ee406466ee Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Thu, 25 Aug 2011 16:17:38 +0200 Subject: [PATCH 05/10] Added gracefull_hooks, default_hooks and 'Accept-Encoding' base_header --- requests/config.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/requests/config.py b/requests/config.py index a92e1f57..e6adfca6 100644 --- a/requests/config.py +++ b/requests/config.py @@ -56,15 +56,25 @@ class Settings(object): settings = Settings() -settings.base_headers = {'User-Agent': 'python-requests.org'} +settings.base_headers = { + 'User-Agent': 'python-requests.org', + 'Accept-Encoding': ', '.join([ 'identity', 'deflate', 'compress', 'gzip' ]), +} settings.accept_gzip = True settings.proxies = None settings.verbose = None settings.timeout = None settings.max_redirects = 30 -# settings.decode_unicode = True -settings.unicode_response = True -settings.decode_response = True +settings.decode_unicode = False +settings.gracefull_hooks = True + +#: A dictionary of default hooks to be applied, based on settings. +settings.default_hooks = { + 'args': list(), + 'pre_request': list(), + 'post_request': list(), + 'response': list() +} #: Use socket.setdefaulttimeout() as fallback? settings.timeout_fallback = True From d8fb6d2a5a169d1bbfd0c1a8cc583a55ecd4e88b Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Thu, 25 Aug 2011 16:19:19 +0200 Subject: [PATCH 06/10] No more sets in default hooks, code cleanup, more docs and comments. --- requests/hooks/__init__.py | 69 ++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/requests/hooks/__init__.py b/requests/hooks/__init__.py index 8f7c6b1c..730e6bb3 100644 --- a/requests/hooks/__init__.py +++ b/requests/hooks/__init__.py @@ -25,23 +25,49 @@ Available hooks: import warnings from collections import Iterable from .. import config -from response import unicode_response, decode_response +from . import args +from . import pre_request +from . import post_request +from . import response -def setup_hooks(hooks): - """Setup hooks as a dictionary. Each value is a set of hooks.""" +def setup_hooks(supplied): + """Setup the supplied dictionary of hooks. + Each value is a list of hooks and will extend **default_hooks**. - for key, values in hooks.items(): + :param supplied: a dictionary of hooks. Each value can either be a callable + or a list of callables. + :type supplied: dict + :returns: a dictionary of hooks that extends the **default_hooks** dictionary. + :rtype: dict + """ + + # Copy the default hooks settings. + dispatching = dict([(k, v[:]) for k, v in config.settings.default_hooks]) + + # I abandoned the idea of a dictionary of sets because sets may not keep + # insertion order, while it may be important. Also, there is no real reason + # to force hooks to run once. + for hooks, values in supplied.items(): hook_list = values if isinstance(values, Iterable) else [values] - hooks[key] = set(hook_list) + dispatching[hooks].extends(hook_list) - # Also, based on settings, - if config.settings.unicode_response: - hooks.setdefault('response', set()).add(unicode_response) - if config.settings.decode_response: - hooks.setdefault('response', set()).add(decode_response) - return hooks + # If header is set, maybe response is encoded. Whatever hook you want to + # run on response, content decoding should be first. + if config.settings.base_headers.get('Accept-Encoding', ''): + dispatching['response'].insert(0, response.decode_encoding) -def dispatch_hooks(hooks, hook_data): + if config.settings.decode_unicode: + try: + # Try unicode encoding just after content decoding... + index = dispatching['response'].index(response.decode_encoding) + 1 + except ValueError: + # ... Or as first hook + index = 0 + dispatching['response'].insert(index, response.decode_unicode) + + return dispatching + +def dispatch_hooks(hooks, data): """Dispatches multiple hooks on a given piece of data. :param key: the hooks group to lookup @@ -49,13 +75,22 @@ def dispatch_hooks(hooks, hook_data): :param hooks: the hooks dictionary. The value of each key can be a callable object, or a list of callable objects. :type hooks: dict - :param hook_data: the object on witch the hooks should be applied - :type hook_data: object + :param data: the object on witch the hooks should be applied + :type data: object """ for hook in hooks: try: - # hook must be a callable - hook_data = hook(hook_data) + # hook must be a callable. + data = hook(data) + except Exception, why: + + # Letting users to choose a policy may be an idea. It can be as + # simple as "be gracefull, or not": + # + # config.settings.gracefull_hooks = True | False + if not config.settings.gracefull_hooks: raise + warnings.warn(str(why)) - return hook_data + + return data From 335bf7018d7b073ff9fe3ab68fa92dd12a4c57b8 Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Thu, 25 Aug 2011 16:24:55 +0200 Subject: [PATCH 07/10] default hooks copy fixed --- requests/hooks/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requests/hooks/__init__.py b/requests/hooks/__init__.py index 730e6bb3..e737bffb 100644 --- a/requests/hooks/__init__.py +++ b/requests/hooks/__init__.py @@ -42,7 +42,8 @@ def setup_hooks(supplied): """ # Copy the default hooks settings. - dispatching = dict([(k, v[:]) for k, v in config.settings.default_hooks]) + default = config.settings.default_hooks + dispatching = dict([(k, v[:]) for k, v in default.items()]) # I abandoned the idea of a dictionary of sets because sets may not keep # insertion order, while it may be important. Also, there is no real reason From 3bf6c63d522fa096bad1bf4873bc1a9accc8ed61 Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Thu, 25 Aug 2011 16:26:10 +0200 Subject: [PATCH 08/10] Applied new hooks api --- requests/api.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/requests/api.py b/requests/api.py index 9c78c17f..cc64fc41 100644 --- a/requests/api.py +++ b/requests/api.py @@ -43,9 +43,7 @@ def request(method, url, method = str(method).upper() - cookies = cookiejar_from_dict(cookies or dict()) - - hooks = setup_hooks(hooks or dict()) + cookies = cookiejar_from_dict(cookies if cookies is not None else dict()) args = dict( method = method, @@ -61,23 +59,24 @@ def request(method, url, proxies = proxies or config.settings.proxies, ) + hooks = setup_hooks(hooks if hooks is not None else dict()) # Arguments manipulation hook. - args = dispatch_hooks(hooks.get('args', []), args) + args = dispatch_hooks(hooks['args'], args) r = Request(**args) # Pre-request hook. - r = dispatch_hooks(hooks.get('pre_request', []), r) + r = dispatch_hooks(hooks['pre_request'], r) # Send the HTTP Request. r.send() # Post-request hook. - r = dispatch_hooks(hooks.get('post_request', []), hooks, r) + r = dispatch_hooks(hooks['post_request'], r) # Response manipulation hook. - r.response = dispatch_hooks(hooks.get('response', []), r.response) + r.response = dispatch_hooks(hooks['response'], r.response) return r.response From d8c923c3850a4f29a39f7da46a77169d8cfe9537 Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Thu, 25 Aug 2011 16:28:47 +0200 Subject: [PATCH 09/10] Added json_content and etree_content, more docs and comments. --- requests/hooks/response.py | 83 ++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/requests/hooks/response.py b/requests/hooks/response.py index e9ce39b3..beec248b 100644 --- a/requests/hooks/response.py +++ b/requests/hooks/response.py @@ -4,41 +4,82 @@ request.hooks.response This module provide a collection of response hooks. """ -from functools import wraps import zlib +import bz2 from cgi import parse_header +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + json = False +try: + from lxml import etree +except ImportError: + try: + import xml.etree.cElementTree as etree + except ImportError: + try: + import xml.etree.ElementTree as etree + except ImportError: + try: + import cElementTree as etree + except ImportError: + try: + import elementtree.ElementTree as etree + except ImportError: + etree = False + #: Dictionary of content decoders. -decoders = { +content_decoders = { # No decoding applied. - 'identity': lambda r: r.content, + 'identity': lambda content: content, # Decode Response file object compressed with deflate. - 'deflate': lambda r: zlib.decompress(r.content), + 'deflate': lambda content: zlib.decompress(content), # Decode Response file object compressed with gzip. - 'gzip': lambda r: zlib.decompress(r.content, 16+zlib.MAX_WBITS), + 'gzip': lambda content: zlib.decompress(content, 16+zlib.MAX_WBITS), + # Decode Response file object compressed with bz2. + # Not a standard Content-Encoding value, but.. + 'bzip2': lambda content: bz2.decompress(content), } # Decode Response file object compressed with compress. -decoders['compress'] = decoders['deflate'] +content_decoders['compress'] = content_decoders['deflate'] -try: - import bz2 -except ImportError: - pass -else: - # Decode Response file object compressed with bz2. - decoders['bzip2'] = lambda r: bz2.decompress(r.content) - -def unicode_response(r): - """Encode response file object in unicode.""" - content_type, params = parse_header(r.headers.get('content-type')) +def decode_unicode(r): + """Encode a :py:class:`requests.models.Response` file object in unicode.""" + content_type, params = parse_header(r.headers['content-type']) charset = params.get('charset', '').strip("'\"") r._content = unicode(r.content, charset) if charset else unicode(r.content) return r -def decode_response(r): - """Decode compressed response content using Contetn-Encoding header.""" - encoding = r.headers.get('content-encoding') - r._content = decoders.get(encoding)(r) +def decode_encoding(r): + """ + Decode a :py:class:`requests.models.Response` content using + Contetn-Encoding header. + """ + # Apply decoding only if the header is set. + encoding = r.headers['content-encoding'] + if encoding: + r._content = content_decoders[encoding](r.content) return r +if json: + def json_content(r): + """ + Turns :py:class:`requests.models.Response` content into a dumped + JSON structure. + """ + r._content = json.dumps(r.content) + return r + +if etree: + def etree_content(r): + """ + Turns :py:class:`requests.models.Response` content into an + ElementTree structure. + """ + r._content = etree.fromstring(r.content) + return r From 07dd324e2f78ee208e35d3faa963ebab237ff95f Mon Sep 17 00:00:00 2001 From: Luca De Vitis Date: Thu, 1 Sep 2011 18:22:12 +0200 Subject: [PATCH 10/10] Reverted hooks from sub-package to module. --- requests/hooks.py | 184 +++++++++++++++++++++++++++++++++ requests/hooks/__init__.py | 97 ----------------- requests/hooks/args.py | 7 -- requests/hooks/post_request.py | 7 -- requests/hooks/pre_request.py | 7 -- requests/hooks/response.py | 85 --------------- 6 files changed, 184 insertions(+), 203 deletions(-) create mode 100644 requests/hooks.py delete mode 100644 requests/hooks/__init__.py delete mode 100644 requests/hooks/args.py delete mode 100644 requests/hooks/post_request.py delete mode 100644 requests/hooks/pre_request.py delete mode 100644 requests/hooks/response.py diff --git a/requests/hooks.py b/requests/hooks.py new file mode 100644 index 00000000..e2980ac3 --- /dev/null +++ b/requests/hooks.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- + +""" +requests.hooks +~~~~~~~~~~~~~~ + +This module provides the capabilities for the Requests hooks system. + +Available hooks: + +``args``: + A dictionary of the arguments being sent to Request(). + +``pre_request``: + The Request object, directly before being sent. + +``post_request``: + The Request object, directly after being sent. + +``response``: + The response generated from a Request. + +""" + +import warnings +from collections import Iterable +import config +import zlib +import bz2 +from cgi import parse_header + +def setup_hooks(supplied): + """Setup a hooks mapping, based on the supplied argument. Eache mapping + value will be list of hooks that will extend the **default_hooks**. + + :param supplied: a dictionary of hooks. Each value can either be a callable + or a list of callables. + :type supplied: dict + :returns: a dictionary of hooks that extends the **default_hooks** dictionary. + :rtype: dict + """ + + # Copy the default hooks settings. + default = config.settings.default_hooks + dispatching = dict([(k, v[:]) for k, v in default.items()]) + + # I abandoned the idea of a dictionary of sets because sets may not keep + # insertion order, while it may be important. Also, there is no real reason + # to force hooks to run once. + for hooks, values in supplied.items(): + hook_list = values if isinstance(values, Iterable) else [values] + dispatching[hooks].extend(hook_list) + + # If header is set by config, maybe response is encoded. + if config.settings.base_headers.get('Accept-Encoding', ''): + if not decode_encoding in dispatching['response']: + # It's safer to put decoding as first hook. + dispatching['response'].insert(0, decode_encoding) + + if config.settings.decode_unicode: + try: + # Try unicode encoding just after content decoding... + index = dispatching['response'].index(decode_encoding) + 1 + except ValueError: + # ... Or as first hook + index = 0 + dispatching['response'].insert(index, decode_unicode) + + return dispatching + +def dispatch_hooks(hooks, data): + """Dispatches multiple hooks on a given piece of data. + + :param key: the hooks group to lookup + :type key: str + :param hooks: the hooks dictionary. The value of each key can be a callable + object, or a list of callable objects. + :type hooks: dict + :param data: the object on witch the hooks should be applied + :type data: object + """ + for hook in hooks: + try: + # hook must be a callable. + data = hook(data) + + except Exception, why: + + # Letting users to choose a policy may be an idea. It can be as + # simple as "be gracefull, or not": + # + # config.settings.gracefull_hooks = True | False + if not config.settings.gracefull_hooks: raise + + warnings.warn(str(why)) + + return data + +#: Example response hook that turns a JSON formatted +#: :py:class:`requests.models.Response.content` into a dumped data structure:: +#: +#: try: +#: import json +#: except ImportError: +#: try: +#: import simplejson as json +#: except ImportError: +#: json = False +#: +#: if json: +#: def json_content(r): +#: """Turns content into a dumped JSON structure.""" +#: r._content = json.dumps(r.content) +#: return r +#: +#: Example response hook that turns an XML formatted +#: :py:class:`requests.models.Response.content` into an ElementTree:: +#: +#: try: +#: from lxml import etree +#: except ImportError: +#: try: +#: import xml.etree.cElementTree as etree +#: except ImportError: +#: try: +#: import xml.etree.ElementTree as etree +#: except ImportError: +#: try: +#: import cElementTree as etree +#: except ImportError: +#: try: +#: import elementtree.ElementTree as etree +#: except ImportError: +#: etree = False +#: +#: if etree: +#: def etree_content(r): +#: """Turns content into an ElementTree structure.""" +#: r._content = etree.fromstring(r.content) +#: return r + +def decode_unicode(r): + """Encode content into unicode string. + + :param r: response object + :type r: :py:class:`requests.models.Response` + :returns: the same input object. + :rtype: :py:class:`requests.models.Response` + """ + content_type, params = parse_header(r.headers['content-type']) + charset = params.get('charset', '').strip("'\"") + r._content = unicode(r.content, charset) if charset else unicode(r.content) + return r + +def decode_encoding(r): + """Decode content using Contetn-Encoding header. + + :param r: response object + :type r: :py:class:`requests.models.Response` + :returns: the same input object. + :rtype: :py:class:`requests.models.Response` + """ + + # Dictionary of content decoders. + decode = { + # No decoding applied. + 'identity': lambda content: content, + # Decode Response content compressed with deflate. + 'deflate': lambda content: zlib.decompress(content), + # Decode Response content compressed with gzip. + 'gzip': lambda content: zlib.decompress(content, 16+zlib.MAX_WBITS), + # Decode Response content compressed with bz2. + # Not a standard Content-Encoding value, but.. + 'bzip2': lambda content: bz2.decompress(content), + } + # Decode Response content compressed with compress. + # If I understood zlib... + decode['compress'] = decode['deflate'] + + # Apply decoding only if the header is set. + encoding = r.headers['content-encoding'] + if encoding: + r._content = decode[encoding](r.content) + return r diff --git a/requests/hooks/__init__.py b/requests/hooks/__init__.py deleted file mode 100644 index e737bffb..00000000 --- a/requests/hooks/__init__.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -requests.hooks -~~~~~~~~~~~~~~ - -This module provides the capabilities for the Requests hooks system. - -Available hooks: - -``args``: - A dictionary of the arguments being sent to Request(). - -``pre_request``: - The Request object, directly before being sent. - -``post_request``: - The Request object, directly after being sent. - -``response``: - The response generated from a Request. - -""" - -import warnings -from collections import Iterable -from .. import config -from . import args -from . import pre_request -from . import post_request -from . import response - -def setup_hooks(supplied): - """Setup the supplied dictionary of hooks. - Each value is a list of hooks and will extend **default_hooks**. - - :param supplied: a dictionary of hooks. Each value can either be a callable - or a list of callables. - :type supplied: dict - :returns: a dictionary of hooks that extends the **default_hooks** dictionary. - :rtype: dict - """ - - # Copy the default hooks settings. - default = config.settings.default_hooks - dispatching = dict([(k, v[:]) for k, v in default.items()]) - - # I abandoned the idea of a dictionary of sets because sets may not keep - # insertion order, while it may be important. Also, there is no real reason - # to force hooks to run once. - for hooks, values in supplied.items(): - hook_list = values if isinstance(values, Iterable) else [values] - dispatching[hooks].extends(hook_list) - - # If header is set, maybe response is encoded. Whatever hook you want to - # run on response, content decoding should be first. - if config.settings.base_headers.get('Accept-Encoding', ''): - dispatching['response'].insert(0, response.decode_encoding) - - if config.settings.decode_unicode: - try: - # Try unicode encoding just after content decoding... - index = dispatching['response'].index(response.decode_encoding) + 1 - except ValueError: - # ... Or as first hook - index = 0 - dispatching['response'].insert(index, response.decode_unicode) - - return dispatching - -def dispatch_hooks(hooks, data): - """Dispatches multiple hooks on a given piece of data. - - :param key: the hooks group to lookup - :type key: str - :param hooks: the hooks dictionary. The value of each key can be a callable - object, or a list of callable objects. - :type hooks: dict - :param data: the object on witch the hooks should be applied - :type data: object - """ - for hook in hooks: - try: - # hook must be a callable. - data = hook(data) - - except Exception, why: - - # Letting users to choose a policy may be an idea. It can be as - # simple as "be gracefull, or not": - # - # config.settings.gracefull_hooks = True | False - if not config.settings.gracefull_hooks: raise - - warnings.warn(str(why)) - - return data diff --git a/requests/hooks/args.py b/requests/hooks/args.py deleted file mode 100644 index 6ac6fb6c..00000000 --- a/requests/hooks/args.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -request.hooks.args -~~~~~~~~~~~~~~~~~~ - -This module provide a collection of args hooks. -""" - diff --git a/requests/hooks/post_request.py b/requests/hooks/post_request.py deleted file mode 100644 index 3a4ff543..00000000 --- a/requests/hooks/post_request.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -request.hooks.post_request -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This module provide a collection of post_request hooks. -""" - diff --git a/requests/hooks/pre_request.py b/requests/hooks/pre_request.py deleted file mode 100644 index ca1e715f..00000000 --- a/requests/hooks/pre_request.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -request.hooks.pre_request -~~~~~~~~~~~~~~~~~~~~~~~~~ - -This module provide a collection of pre_request hooks. -""" - diff --git a/requests/hooks/response.py b/requests/hooks/response.py deleted file mode 100644 index beec248b..00000000 --- a/requests/hooks/response.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -request.hooks.response -~~~~~~~~~~~~~~~~~~~~~~ - -This module provide a collection of response hooks. -""" -import zlib -import bz2 -from cgi import parse_header - -try: - import json -except ImportError: - try: - import simplejson as json - except ImportError: - json = False -try: - from lxml import etree -except ImportError: - try: - import xml.etree.cElementTree as etree - except ImportError: - try: - import xml.etree.ElementTree as etree - except ImportError: - try: - import cElementTree as etree - except ImportError: - try: - import elementtree.ElementTree as etree - except ImportError: - etree = False - -#: Dictionary of content decoders. -content_decoders = { - # No decoding applied. - 'identity': lambda content: content, - # Decode Response file object compressed with deflate. - 'deflate': lambda content: zlib.decompress(content), - # Decode Response file object compressed with gzip. - 'gzip': lambda content: zlib.decompress(content, 16+zlib.MAX_WBITS), - # Decode Response file object compressed with bz2. - # Not a standard Content-Encoding value, but.. - 'bzip2': lambda content: bz2.decompress(content), -} - -# Decode Response file object compressed with compress. -content_decoders['compress'] = content_decoders['deflate'] - -def decode_unicode(r): - """Encode a :py:class:`requests.models.Response` file object in unicode.""" - content_type, params = parse_header(r.headers['content-type']) - charset = params.get('charset', '').strip("'\"") - r._content = unicode(r.content, charset) if charset else unicode(r.content) - return r - -def decode_encoding(r): - """ - Decode a :py:class:`requests.models.Response` content using - Contetn-Encoding header. - """ - # Apply decoding only if the header is set. - encoding = r.headers['content-encoding'] - if encoding: - r._content = content_decoders[encoding](r.content) - return r - -if json: - def json_content(r): - """ - Turns :py:class:`requests.models.Response` content into a dumped - JSON structure. - """ - r._content = json.dumps(r.content) - return r - -if etree: - def etree_content(r): - """ - Turns :py:class:`requests.models.Response` content into an - ElementTree structure. - """ - r._content = etree.fromstring(r.content) - return r