Commits

Lynn Rees committed 07e4c78

- begin refactor

  • Participants
  • Parent commits 186dd72
  • Branches next

Comments (0)

Files changed (30)

     from distutils.core import setup
 
 install_requires = ['httplib2', 'stuf']
-if sys.version_info <= (2, 7): install_requires.append('importlib')
+if sys.version_info <= (2, 7):
+    install_requires.append('importlib')
 
 setup(
     name='wire',
     author_email='lcrees@gmail.com',
     url='https://bitbucket.org/lcrees/wire',
     license='BSD',
-    packages = ['wire', 'wire.talk', 'wire.http'],
+    packages=['wire', 'wire.talk', 'wire.http'],
     test_suite='wire.test',
-    zip_safe = False,
+    zip_safe=False,
     keywords='http client https Web REST',
     install_requires=install_requires,
     classifiers=[
         'Topic :: Internet :: WWW/HTTP',
         'Topic :: Software Development :: Libraries :: Application Frameworks',
     ],
-)
+)

File wire/http/__init__.py

-from wire.http.core import (
-    Client, RESTClient, get, mget, fast_get, post, mpost, fast_post, put, mput,
-    fast_put, delete, mdelete, fast_delete, head, mhead, fast_head, options,
-    moptions, fast_options,
-)

File wire/http/chains.py

+# -*- coding: utf-8 -*-
+'''wire http call chain'''
+
+from callchain.root.chain import callchain
+from wire.util import missing
+
+
+class http(callchain):
+
+    '''HTTP client call chain'''
+
+    def __init__(self, pattern=None, required=None, defaults=None, **kw):
+        '''
+        init
+
+        @param pattern: pattern configuration or appspace label (default: None)
+        @param required: required settings (default: None)
+        @param defaults: default settings (default: None)
+        '''
+        super(http, self).__init__(pattern, required, defaults, **kw)
+        # set data to something
+        self._data = ''
+        # wire protocol
+        self._format = ''
+        # files something
+        self._files = kw.pop('files', {})
+        # custom headers
+        self._headers = kw.pop('headers', {})
+        # custom cookie
+        self._cookie = kw.pop('cookie', {})
+        # default timeout
+        self._timeout = kw.pop('timeout', 100)
+        # use random user agent
+        self._randomua = kw.pop('randomua', False)
+        # preserve any original data
+        self._original = kw.pop('data', '')
+        # url, if any
+        self._url = kw.pop('url', '')
+        # files demand multipart as default otherwise go with urlencode
+        self._originalfmt = kw.pop(
+            'format', 'multipart' if self._files else 'url'
+        )
+        # authentication
+        self._auth = kw.pop('auth', {})
+        if self._auth:
+            # verify
+            if missing(self._auth, ('username', 'password')):
+                raise TypeError(
+                    u'authentication missing both "username" and "password"',
+                )
+        # proxy
+        self._proxy = kw.pop('proxy', {})
+        if self._proxy:
+            if missing(self._proxy, ('server', 'host')):
+                raise TypeError(u'proxy missing both "server" and "host"')
+        # misc. options, if any
+        self._options = kw
+        self._changed = False
+
+    def __call__(self, *urls):
+        '''
+        load urls
+
+        @param urls: Uniform Resource Locations (URLs)
+        '''
+        self._urls = urls
+        return self

File wire/http/client/app.py

     httplib2='hl2_client.REST',
     pycurl='pycurl_client.REST',
 )
-REST = Loader(RESTLIST, package='wire.http.client')
+REST = Loader(RESTLIST, package='wire.http.client')

File wire/http/core.py

-'''wire http core'''
-
-from collections import defaultdict
-
-from wire.util import lazy
-from wire.http.client.app import HTTP, REST
-
-
-class RESTClient(object):
-
-    '''REST client dispatcher'''
-
-    def __new__(cls, **kw):
-        return REST(kw.pop('client', 'httplib2'))(**kw)
-
-
-class Client(object):
-
-    '''HTTP client dispatcher'''
-
-    def __new__(cls, **kw):
-        return HTTP(kw.pop('client', 'httplib2'))(**kw)
-
-
-class Dispatcher(object):
-
-    '''HTTP client dispatcher for real'''
-
-    @lazy
-    def _clients(self):
-        '''client cache'''
-        return defaultdict(lambda: Client())
-
-    def __call__(self, **kw):
-        try:
-            client = kw.pop('client')
-            return self._clients.setdefault(client, Client(client=client))
-        except KeyError:
-            return self._clients['default']
-
-
-# instantiate dispatcher
-_dispatcher = Dispatcher()
-
-
-def delete(url, **kw):
-    '''
-    use "DELETE" HTTP method
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
-
-    @param url: URL to delete
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).delete(url=url, **kw)
-
-def fast_delete(url, **kw):
-    '''
-    only returns DELETE HTTP status
-
-    @param url: URL to DELETE quickly
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).fast_delete(url=url, **kw)
-
-def fast_get(url, **kw):
-    '''
-    only returns GET data
-
-    @param url: URL to GET quickly
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).fast_get(url=url, **kw)
-
-def fast_head(url, **kw):
-    '''
-    only returns head headers
-
-    @param url: URL to HEAD quickly
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).fast_head(url=url, **kw)
-
-def fast_options(url, **kw):
-    '''
-    only returns options data
-
-    @param url: URL to OPTIONS quickly
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).fast_options(url=url, **kw)
-
-def fast_post(url, **kw):
-    '''
-    only returns post data
-
-    @param url: URL to POST quickly
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).fast_post(url=url, **kw)
-
-def fast_put(url, **kw):
-    '''
-    only returns put HTTP status
-
-    @param url: URL to PUT quickly
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).fast_put(url=url, **kw)
-
-def get(url, **kw):
-    '''
-    use "GET" HTTP method
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
-
-    @param url: URL to GET
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).get(url=url, **kw)
-
-def head(url, **kw):
-    '''
-    use "HEAD" HTTP method
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
-
-    @param url: URL to HEAD
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).head(url=url, **kw)
-
-def mdelete(urls, **kw):
-    '''
-    use "DELETE" HTTP method for multiple URLs
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
-
-    @param urls: URLs to DELETE
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).mdelete(urls, **kw)
-
-def mget(urls, **kw):
-    '''
-    use "GET" HTTP method for multiple URLs
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
-
-    @param urls: URLs to GET
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(**kw).mget(urls, **kw)
-
-def mhead(urls, **kw):
-    '''
-    use "HEAD" HTTP method for multiple URLs
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
-
-    @param urls: URLs to HEAD
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).mhead(urls, **kw)
-
-def moptions(urls, **kw):
-    '''
-    use "OPTIONS" HTTP method for multiple URLs
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
-
-    @param urls: URLs to OPTIONS
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).mpost(urls, **kw)
-
-def mpost(urls, **kw):
-    '''
-    use "POST" HTTP method for multiple URLs
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
-
-    @param urls: URLs to POST
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).mpost(urls, **kw)
-
-def mput(urls, **kw):
-    '''
-    use "PUT" HTTP method for multiple URLs
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
-
-    @param urls: URLs to PUT
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).mput(urls, **kw)
-
-def options(url, **kw):
-    '''
-    use "OPTIONS" HTTP method
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
-
-    @param url: URL to OPTIONS
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(
-        client=kw.pop('client', 'httplib2')
-    ).options(url=url, **kw)
-
-def post(url, **kw):
-    '''
-    use "POST" HTTP method
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
-
-    @param url: URL to POST
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).post(url=url, **kw)
-
-def put(url, **kw):
-    '''
-    use "PUT" HTTP method
-
-    http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
-
-    @param url: URL to PUT
-    '''
-    kw.setdefault('data', '')
-    kw.setdefault('format', '')
-    kw.setdefault('headers', {})
-    return _dispatcher(client=kw.pop('client', 'httplib2')).put(url=url, **kw)

File wire/http/header.py

 from functools import partial
 from Cookie import SimpleCookie, Morsel
 
-from wire.util import lazy, lazier
+from wire.util import lazier
 
 
 class HTTPHeaderException(Exception):
             tuple(i.split(': ')) for i in str(self).splitlines())
         )
 
-    @lazy
     def __str__(self):
         return self.output
 
         else:
             for k, v in data.iteritems(): self.__setitem__(k, v)
 
-    @lazy
     def output(self):
         return partial(super(CoreCookie, self).output, header='cookie:')
 
 
     '''HTTP header manager'''
 
-    def __getitem__(self, key):
-        return self.get(key)
-
-    def __contains__(self, key):
-        return self.get(key) is not None
-
-    def __len__(self):
-        return len(list(self.__iter__()))
-
-    def __str__(self):
-        return '\r\n'.join(': '.join([k, v]) for k, v in self)
-
     @staticmethod
     def _checkname(name):
         '''
         return list(i[1] for i in self)
 
     @lazier
-    def COMMA_SEP(self):
-        '''May have values that are split on the comma'''
+    def _COMMA_SEP(self):
+        '''May have values that split on the comma'''
         return frozenset([
             'accept', 'accept-charset', 'accept-encoding', 'accept-language',
             'allow', 'cache-control', 'content-encoding', 'content-language',
         ])
 
     @lazier
-    def DATES(self):
+    def _DATES(self):
         '''Coverted to datetime fields'''
         return frozenset([
             'date', 'expires', 'if-modified-since',  'if-unmodified-since',
         ])
 
     @lazier
-    def ENCODINGS(self):
+    def _ENCODINGS(self):
         '''"content-encoding" compatible values'''
         return frozenset(['gzip', 'compress', 'deflate'])
 
     @lazier
-    def ENTITY(self):
+    def _ENTITY(self):
         '''HTTP entity headers'''
         return frozenset([
             'allow', 'content-encoding', 'content-language', 'content-length',
         ])
 
     @lazier
-    def GENERAL(self):
+    def _GENERAL(self):
         '''General HTTP headers'''
         return frozenset([
             'cache-control', 'connection', 'date', 'pragma', 'trailer', 'via',
         ])
 
     @lazier
-    def HOP_BY_HOP(self):
+    def _HOP_BY_HOP(self):
         '''hop by hop headers'''
         return frozenset([
             'connection', 'keep-alive', 'proxy-authenticate', 'upgrade',
         ])
 
     @lazier
-    def HTTPIF(self):
+    def _HTTPIF(self):
         '''HTTP if tests'''
         return frozenset([
             'if-match', 'if-none-match',  'if-modified-since', 'if-rangeset',
         ])
 
     @lazier
-    def METHODS(self):
+    def _METHODS(self):
         '''HTTP methods'''
         return frozenset([
             'OPTIONS', 'GET', 'HEAD', 'POST', 'POST', 'DELETE', 'CONNECT',
         ])
 
     @lazier
-    def PATTERNS(self):
+    def _PATTERNS(self):
         '''relevant HTTP validators'''
         _PATTERNS = dict(
         # Integer
         return dict((k, re.compile(v, re.U)) for k, v in _PATTERNS.iteritems())
 
     @lazier
-    def REQUEST(self):
+    def _REQUEST(self):
         '''HTTP request headers'''
         return frozenset([
             'accept', 'accept-charset', 'accept-encoding', 'accept-language',
         ])
 
     @lazier
-    def REQ_HEADERS(self):
+    def _REQ_HEADERS(self):
         '''Combined HTTP request compatible headers'''
         return self.GENERAL | self.REQUEST | self.ENTITY
 
     @lazier
-    def RESPONSE(self):
+    def _RESPONSE(self):
         '''HTTP response headers'''
         return frozenset([
             'accept-ranges', 'age', 'etag', 'location', 'proxy-authenticate',
         ])
 
     @lazier
-    def RES_HEADERS(self):
+    def _RES_HEADERS(self):
         '''Combined HTTP response compatible headers'''
         return self.GENERAL | self.RESPONSE | self.ENTITY
 
     @lazier
-    def SC_SEP(self):
+    def _SC_SEP(self):
         '''May have values that are split on semi-colon'''
         return frozenset([
             'accept', 'accept-charset', 'accept-encoding', 'accept-language',
         ])
 
     @classmethod
-    def iscontentrange(cls, data):
+    def _iscontentrange(cls, data):
         '''Validates a HTTP content range header'''
         return cls._regeval('content_type', data)
 
     @classmethod
-    def ishttpname(cls, data):
+    def _ishttpname(cls, data):
         '''Validates data is a correct HTTP header name'''
         return cls._regeval('http_header_name', data)
 
     @classmethod
-    def isinteger(cls, data):
+    def _isinteger(cls, data):
         '''Validates data is an integer'''
         return cls._regeval('integer', data)
 
     @classmethod
-    def isrfc1123(cls, data):
+    def _isrfc1123(cls, data):
         '''RFC1123 compliance checker e.g. "Wed, 03 Oct 2007 23:59:24 GMT"'''
         return cls._regeval('rfc1123', data)
 
     @classmethod
-    def isurl(cls, data):
+    def _isurl(cls, data):
         '''Validates data is a valid URL'''
         return cls._regeval('url', data)
+    
+    def final(self):
+        return '\r\n'.join(': '.join([k, v]) for k, v in self)

File wire/http/keys/__init__.py

+# -*- coding: utf-8 -*-
+'''http client keys'''

File wire/http/keys/request.py

+# -*- coding: utf-8 -*-
+#@PydevCodeAnalysisIgnore
+#pylint: disable-msg=e0211,e0213
+'''http client keys'''
+
+from appspace.keys import AppspaceKey
+
+
+class KRequest(AppspaceKey):
+
+    '''HTTP client key'''
+
+    def __call__(*urls):
+        '''
+        load urls
+    
+        @param urls: Uniform Resource Locations (URLs)
+        '''
+
+    def get():
+        '''
+        *GET* HTTP method
+    
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
+        '''
+
+    def post():
+        '''
+        *POST* HTTP method
+    
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
+        '''
+
+    def delete():
+        '''
+        *DELETE* HTTP method
+    
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
+        '''
+
+    def put():
+        '''
+        *PUT* HTTP method
+    
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
+        '''
+
+    def head():
+        '''
+        *HEAD* HTTP method
+    
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
+        '''
+
+    def options():
+        '''
+        *OPTIONS* HTTP method
+    
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+        '''
+        
+    def authenticate(username, password):
+        '''
+        set authentication settings
+        
+        @param username: username credential
+        @param password: password credential
+        '''
+
+    def proxy(server, host):
+        '''
+        proxy settings
+        
+        @param server: proxy server
+        @param host: proxy host
+        '''
+        
+
+class KAPI(AppspaceKey):
+    
+    '''HTTP API key'''
+        
+    def api():
+        '''switch to HTTP API chain'''
+        
+
+class KRequestData(AppspaceKey):
+    
+    '''HTTP request data'''
+    
+    def file(io):
+        '''load file'''
+        
+    def params(**kw):
+        '''request parameters'''
+        
+        
+class KRequestHeaders(AppspaceKey):
+    
+    '''HTTP request key'''
+    
+    def header(key, value=None):
+        '''switch to HTTP request headers chain'''
+        
+    def headers(*args, **kw):
+        '''HTTP request header setter'''
+    
+    def cache_control(value=None):
+        '''
+        set *CACHE-CONTROL* HTTP header
+        
+        @param value HTTP header value
+        '''
+    
+    def connection(value=None):
+        '''
+        set *CONNECTION* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def date(value=None):
+        '''
+        *DATE* HTTP header
+        
+        @param value HTTP header value
+        '''
+    
+    def pragma(value=None):
+        '''
+        *PRAGMA* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def trailer(value=None):
+        '''
+        *TRAILER* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def via(value=None):
+        '''
+        *VIA* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def transfer_enconding(value=None):
+        '''
+        *TRANSFER-ENCODING* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def upgrade(value=None):
+        '''
+        *UPGRADE* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def warning(value=None):
+        '''
+        *WARNING* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def allow(value=None):
+        '''
+        *ALLOW* HTTP header
+        
+        @param value HTTP header value
+        '''
+    
+    def content_encoding(value=None):
+        '''
+        *CONTENT-ENCODING* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def content_language(value=None):
+        '''
+        *CONTENT-LANGUAGE* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def content_length(value=None):
+        '''
+        *CONTENT-LENGTH* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def content_location(value=None):
+        '''
+        *CONTENT-LOCATION* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def content_md5(value=None):
+        '''
+        take MD5 hash of data and set *CONTENT-MD5* HTTP header.
+
+        @param value: HTTP response data
+        '''
+        
+    def content_range(first, last, length='*'):
+        '''
+        set rangeset in HTTP header *CONTENT-RANGE*
+
+        @param first First byte position
+        @param last Last byte position
+        @param length Instance length
+        '''
+
+    def content_type(mime, charset=None):
+        '''
+        set the HTTP *CONTENT-TYPE* header.
+
+        @param mime Media type
+        @param charset Optional charset (default: None)
+        '''
+    
+    def expires(value=None):
+        '''
+        *EXPIRES* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def last_modified(value=None):
+        '''
+        *LAST-MODIFIED* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def cookie(morsel):
+        '''
+        *COOKIE* HTTP header
+        
+        @param value HTTP header value
+        '''
+    
+    def accept(value=None):
+        '''
+        *ACCEPT* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def accept_charset(value=None):
+        '''
+        *ACCEPT-CHARSET* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def accept_encoding(value=None):
+        '''
+        *ACCEPT-ENCODING* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def accept_language(value=None):
+        '''
+        *ACCEPT-LANGUAGE* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def range(value=None):
+        '''
+        *RANGE* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def authorization(value=None):
+        '''
+        *AUTHORIZATION* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def expect(value=None):
+        '''
+        *EXPECT* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def from_header(value=None):
+        '''
+        *FROM* HTTP header
+        
+        @param value HTTP header value
+        '''
+    
+    def host(value=None):
+        '''
+        *HOST* HTTP header
+        
+        @param value HTTP header value
+        '''
+    
+    def if_match(value=None):
+        '''
+        *IF-MATCH* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def if_modified_since(value=None):
+        '''
+        *IF-MODIFIED-SINCE* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def if_none_match(value=None):
+        '''
+        *IF-NONE-MATCH* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def if_range(value=None):
+        '''if-range HTTP header'''
+        
+    def if_unmodified_since(value=None):
+        '''
+        *IF-UNMODIFIED-SINCE* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def max_forwards(value=None):
+        '''*MAX-FORWARDS* HTTP header'''
+        
+    def te(value=None):
+        '''
+        *TE* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def proxy_authorization(value=None):
+        '''
+        *PROXY-AUTHORIZATION* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def referer(value=None):
+        '''
+        *REFERER* HTTP header
+        
+        @param value HTTP header value
+        '''
+
+    def user_agent(value=None):
+        '''
+        *USER-AGENT* HTTP header
+        
+        @param value HTTP header value
+        '''
+        
+    def rfc2822(key, value, charset=None):
+        '''
+        set header value with specific charset encoded following RFC 2822
+
+        @param key HTTP header key
+        @param value HTTP header value
+        @param charset HTTP charset (default: None)
+        '''

File wire/http/keys/response.py

+# -*- coding: utf-8 -*-
+#@PydevCodeAnalysisIgnore
+#pylint: disable-msg=e0211,e0213
+'''http client response keys'''
+
+from appspace.keys import AppspaceKey
+
+
+class KResponse(AppspaceKey):
+    
+    '''HTTP client response key'''
+
+    def code():
+        '''
+        HTTP response code
+
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+        '''
+
+    def good():
+        '''True if HTTP was successful enough'''
+
+    def fail():
+        '''True if HTTP request failed'''
+
+    def noop():
+        '''True if HTTP request was more or less harmless'''
+
+    def ok():
+        '''True if HTTP request was successful'''
+
+    def status():
+        '''status of HTTP response'''
+
+
+class KResponseCookie(AppspaceKey):
+    
+    '''HTTP response cookie key'''
+    
+    def cookie(*keys):
+        '''
+        HTTP cookie
+        
+        @param *keys: cookie keys
+        '''
+        
+    def morsel(*keys):
+        '''
+        switch to HTTP response cookies chain
+        
+        @param *keys: cookie keys
+        '''
+        
+        
+class KResponseData(AppspaceKey):
+    
+    '''HTTP response data'''
+    
+    def touched():
+        '''Verify MD5 digest of response data against "Content-MD5" digest'''
+        
+    def raw():
+        '''read original response content'''   
+    
+    def data():
+        '''response data'''
+
+
+class KResponseHeader(AppspaceKey):
+    
+    '''response headers key'''
+    
+    def header(*keys):
+        '''
+        fetch HTTP response headers
+        
+        @param key: header keys key
+        '''
+    
+    def cache_control():
+        '''*CACHE-CONTROL* HTTP header'''
+    
+    def connection():
+        '''*CONNECTION* HTTP header'''
+        
+    def date():
+        '''*DATE* HTTP header'''
+    
+    def pragma():
+        '''*PRAGMA* HTTP header'''
+        
+    def trailer():
+        '''*TRAILER* HTTP header'''
+        
+    def via():
+        '''*VIA* HTTP header'''
+        
+    def transfer_enconding():
+        '''*TRANSFER-ENCODING* HTTP header'''
+        
+    def upgrade():
+        '''*UPGRADE* HTTP header'''
+        
+    def warning():
+        '''*WARNING* HTTP header'''
+        
+    def allow():
+        '''*ALLOW* HTTP header'''
+    
+    def content_encoding():
+        '''*CONTENT-ENCODING* HTTP header'''
+        
+    def content_language():
+        '''*CONTENT-LANGUAGE* HTTP header'''
+        
+    def content_length():
+        '''*CONTENT-LENGTH* HTTP header'''
+        
+    def content_location():
+        '''*CONTENT-LOCATION* HTTP header'''
+        
+    def content_md5():
+        '''*CONTENT-MD5* HTTP header'''
+        
+    def content_range():
+        '''HTTP header *CONTENT-RANGE*'''
+
+    def content_type():
+        '''
+        HTTP *CONTENT-TYPE* header
+        
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
+        '''
+    
+    def expires():
+        '''*EXPIRES* HTTP header'''
+        
+    def last_modified():
+        '''*LAST-MODIFIED* HTTP header'''
+    
+    def accept_ranges():
+        '''*ACCEPT-RANGES* HTTP header'''
+        
+    def age():
+        '''*AGE* HTTP header'''
+        
+    def etag():
+        '''*ETAG* HTTP header''' 
+        
+    def location():
+        '''*LOCATION* HTTP header'''
+        
+    def proxy_authenticate():
+        '''*PROXY-AUTHENTICATE* HTTP header'''
+
+    def retry_after():
+        '''*RETRY-AFTER* HTTP header'''
+        
+    def server():
+        '''*SERVER* HTTP header'''
+        
+    def vary():
+        '''*VARY* HTTP header'''
+
+    def www_authenticate():
+        '''*WWW-AUTHETICATE* HTTP header'''

File wire/http/request.py

 from functools import partial
 from email.header import Header
 
+from wire.http.util import httpdate
+from wire.util import OrderedDict, subdictremove, hiddenhand, lazier
+from wire.http.header import CoreHeaders, HTTPHeaderException
+from callchain.root.linked import chainlink
+from appspace.keys import appifies
+from wire.http.keys.request import KRequest, KRequestData, KRequestHeaders
 
-from wire.http.util import httpdate
-from wire.util import (
-    OrderedDict, lazy, missing, subdictremove, hiddenhand, lazier,
-)
-from wire.http.header import CoreHeaders, CoreCookie, HTTPHeaderException
 
-
-class RequestHeaders(CoreHeaders):
+@appifies(KRequestHeaders)
+class Headers(CoreHeaders):
 
     '''HTTP request header manager'''
 
     def __init__(self, headers=()):
-        super(RequestHeaders, self).__init__()
+        super(Headers, self).__init__()
         # "best practice" sends HTTP headers in order: general, request, entity
         hiddenhand(self, '_general', OrderedDict())
         hiddenhand(self, '_entity', OrderedDict())
                 prev.append(value)
             except AttributeError:
                 # join with any existing value if header allows multiple values
-                if key in self.COMMA_SEP:
+                if key in self._COMMA_SEP:
                     headers[key] = ', '.join([headers[key], value])
                 # replace value on headers allowing only single values
-                if key in self.SC_SEP:
+                if key in self._SC_SEP:
                     headers[key] = '; '.join([headers[key], value])
                 else:
                     headers[key] = value
             list(self.cookies),
         ) if h is not None)
 
+    def _headerfy(self, headers, **kw):
+        if headers:
+            new_headers = Headers(i for i in headers.iteritems())
+        else:
+            new_headers = Headers()
+        if kw.get('randomua', self._randomua):
+            new_headers.user_agent = new_headers._randomua()
+        return new_headers
+
     @lazier
     def _badheaders(self):
         '''Bad header detecting regex from wsgiref'''
         @param value: Value to format
         '''
         # Keep nulls out of the pipeline
-        if value is None or not value: return value
+        if value is None or not value:
+            return value
         # Join with any existing value if header allows multiple values
-        if key in self.DATES and isinstance(value, datetime):
+        if key in self._DATES and isinstance(value, datetime):
             value = httpdate(value)
         return value
 
 
         @param: key Header key
         '''
-        if key in self.GENERAL:
+        if key in self._GENERAL:
             return self._general
-        elif key in self.REQUEST:
+        elif key in self._REQUEST:
             return self._request
-        elif key in self.ENTITY:
+        elif key in self._ENTITY:
             return self._entity
         return self._custom
 
         return {
             # "Allow" values must be valid HTTP methods
             'allow': (
-                lambda v: v.upper() not in self.METHODS,
+                lambda v: v.upper() not in self._METHODS,
                 '"Allow" value must be valid HTTP method'
             ),
             # "Content-Encoding" value must be zip, compress, or deflate
             'content-encoding': (
-                lambda v: v.lower() not in self.ENCODINGS,
+                lambda v: v.lower() not in self._ENCODINGS,
                 '"Content-Encoding" must be gzip, compress, deflate',
             ),
             # "Content-Language" values must be valid language codes
             ),
             # "Content-Range" value must be in the proper format
             'content-range': (
-                lambda v: not self.iscontentrange(v),
+                lambda v: not self._iscontentrange(v),
                 'Invalid format for "Content-Range"'
             ),
         }
         @param value Putative header value
         '''
         err_msg = None
-        if not isinstance(value, basestring): return True
+        if not isinstance(value, basestring):
+            return True
         # Don't allow request headers
-        if key in self.RESPONSE:
+        if key in self._RESPONSE:
             err_msg = '"%s" is HTTP response-only header' % key
         # Don't allow unicode header names
         if not isinstance(key, str):
         elif '\n' in key or ':' in key:
             err_msg = 'HTTP header names may not contain ":" or "\\n"'
         # Do not allow invalid characters in header name
-        elif not self.ishttpname(key):
+        elif not self._ishttpname(key):
             err_msg = 'Bad HTTP header name: %r' % key
         # Do not allow dash or underscore at end of header name
         elif key.endswith('-') or key.endswith('_'):
                 self._badheaders.search(value).group(0), value
             )
         # Validate RFC 1123 compliance
-        elif key in self.DATES and not self.isrfc1123(value):
+        elif key in self._DATES and not self._isrfc1123(value):
             err_msg = 'Date is not RFC 1123 compliant'
         # Validate individual header
         else:
             rule = self._rules.get(key)
-            if rule is not None and rule[0](value): err_msg = rule[1]
-        if err_msg is not None: raise HTTPHeaderException(err_msg)
+            if rule is not None and rule[0](value):
+                err_msg = rule[1]
+        if err_msg is not None:
+            raise HTTPHeaderException(err_msg)
 
-    @lazy
-    def cookies(self):
-        '''HTTP cookie manager'''
-        return CoreCookie()
-
-    def get(self, key, default=None):
-        '''
-        Get the value for "key" or return "default".
-
-        @param key: HTTP header key
-        @param default: HTTP header value (default None)
-        '''
-        key = key.lower()
-        headers = self._getheaders(key)
-        return headers.get(key, default)
-
-    @lazy
-    def items(self):
-        '''All the header fields and values'''
-        return self._items
-
-    @lazy
-    def keys(self):
-        '''list of header values'''
-        return self._keys
-
-    def md5set(self, data):
+    def md5(self, data):
         '''
         Take a MD5 hash of data and set the "Content-MD5" header.
 
         import md5
         from base64 import b64encode
         self['Content-MD5'] = b64encode(md5.new(data).digest())
+        return self
 
-    def rangeset(self, first, last, length='*'):
+    def range(self, first, last, length='*'):
         '''
         Set the rangeset in header "Content-Range"
 
             self['Content-Range'] = 'bytes %d-%d/*' % (first, last)
         else:
             self['Content-Range'] = 'bytes %d-%d/%d' % (first, last, length)
+        return self
 
-    def rfc2822set(self, key, value, charset=None):
+    def rfc2822(self, key, value, charset=None):
         '''
         Set a header value in with a specific charset encoded as RFC 2822.
 
                 pass
         else:
             # Override existing value if not Header instance or add new value
+            self[key] = Header(value, charset)
+        return self
 
-            self[key] = Header(value, charset)
-
-    def setdefault(self, key, value):
-        '''
-        Return header value for "key" if it has been set. Otherwise, set
-        a new header under key "key" and value "value".
-
-        @param key HTTP header key
-        @param value Provisional HTTP header value
-        '''
-        result = self.get(key)
-        if result is None:
-            self[key] = value
-            return value
-        return result
-
-    def typeset(self, mime, charset=None):
+    def content_type(self, mime, charset=None):
         '''
         Utility method to set the "Content-Type" header.
 
             self['Content-Type'] = mime
         else:
             self['Content-Type'] = '{0}; charset={1}'.format(mime, charset)
+        return self
 
     def update(self, headers=(), **kw):
         '''update header values'''
         setitem = self.__setitem__
         if kw:
-            for k, v in kw.iteritems(): setitem(k, v)
+            for k, v in kw.iteritems():
+                setitem(k, v)
         if headers:
-            for h in headers: setitem(h[0], h[1])
+            for h in headers:
+                setitem(h[0], h[1])
+        return self
 
-    @lazy
-    def values(self):
-        '''A list of all header values'''
-        return self._values
+#    def headers(self):
+#        '''
+#        HTTP header manager
+#        '''
+#        return self._headerfy(self._headers)
 
 
-class CoreRequest(object):
+@appifies(KRequestData)
+class Data(chainlink):
+
+    def __init__(self, root):
+        super(Data, self).__init__(root)
+        self._data = ''
+
+    @property
+    def _thedata(self):
+        return self._data or self._original
+
+    def data(self):
+        if not self._data:
+            data = self._thedata
+            if data:
+                self._data = self._datafy(
+                    self._thedata, self.headers, self.format, **self._options
+                )
+        return self._data
+
+    def format(self):
+        return self._format or self._originalfmt
+
+    # private methods
+
+    def _datafy(self, data, headers, fmt, **kw):
+        if fmt in self._dumpers.apps:
+            files = kw.pop('files', self._files)
+            if files: data = dict(form=data, files=files)
+            type_, data = self.dumps(data, fmt, **self._kwfy(kw)[-1])
+            headers.content_type = type_
+            headers.accept = type_
+        return data
+    
+
+@appifies(KRequest)
+class Request(object):
 
     '''core HTTP request'''
 
-    def __init__(self, **kw):
-        '''defaults'''
-        # set data to something
-        self._data = ''
-        # wire protocol
-        self._format = ''
-        # files something
-        self._files = kw.pop('files', {})
-        # custom headers
-        self._headers = kw.pop('headers', {})
-        # custom cookie
-        self._cookie = kw.pop('cookie', {})
-        # default timeout
-        self._timeout = kw.pop('timeout', 100)
-        # use random user agent
-        self._randomua = kw.pop('randomua', False)
-        # preserve any original data
-        self._original = kw.pop('data', '')
-        # url, if any
-        self._url = kw.pop('url', '')
-        # files demand multipart as default otherwise go with urlencode
-        self._originalfmt = kw.pop(
-            'format', 'multipart' if self._files else 'url'
-        )
-        # authentication
-        self._auth = kw.pop('auth', {})
-        if self._auth:
-            # verify
-            if missing(self._auth, ('username', 'password')):
-                raise TypeError(
-                    u'authentication missing both "username" and "password"',
-                )
-        # proxy
-        self._proxy = kw.pop('proxy', {})
-        if self._proxy:
-            if missing(self._proxy, ('server', 'host')):
-                raise TypeError(u'proxy missing both "server" and "host"')
-        # misc. options, if any
-        self._options = kw
-        self._changed = False
+
 
     def __getattr__(self, k):
         try:
             object.__setattr__(self, k, v)
             return v
 
-#    @classgelder
-
     def __setattr__(self, k, v):
         if k in self._dumpers.apps:
             v = self._datafy(v, self.headers, k, **self._options)
             self._changed = True
         object.__setattr__(self, k, v)
 
-    def __str__(self):
-        return self.data
-
-    # properties
-
-    @property
-    def _thedata(self):
-        return self._data or self._original
-
-    @property
-    def data(self):
-        if not self._data:
-            data = self._thedata
-            if data:
-                self._data = self._datafy(
-                    self._thedata, self.headers, self.format, **self._options
-                )
-        return self._data
-
-    @data.setter
-    def data(self, data):
-        if data is None:
-            self._data = ''
-            self._original = ''
-        else:
-            self._data = self._datafy(
-                data, self.headers, self.format, **self._options
-            )
-        self._changed = True
-
-    @data.deleter
-    def data(self):
-        self._data = ''
-        self._original = ''
-        self._changed = True
-
-    @property
-    def format(self):
-        return self._format or self._originalfmt
-
-    # private methods
-
-    def _datafy(self, data, headers, fmt, **kw):
-        if fmt in self._dumpers.apps:
-            files = kw.pop('files', self._files)
-            if files: data = dict(form=data, files=files)
-            type_, data = self.dumps(data, fmt, **self._kwfy(kw)[-1])
-            headers.content_type = type_
-            headers.accept = type_
-        return data
-
-    @lazy
-    def _defmultiple(self):
-        request = self.request
-        def func(urls, **kw):
-            method = kw.pop('method', 'GET')
-            return list(request(url=url, method=method, **kw) for url in urls)
-        return func
-
-    @lazier
-    def _dumpers(self):
-        from wire.talk.app import TALKERS
-        return TALKERS
-
-    @lazy
-    def _eventletmultiple(self):
-        import eventlet
-        request = self.request
-        def func(urls, **kw):
-            def fetch(url):
-                return request(url=url, **kw)
-            pool = eventlet.GreenPool()
-            return list(data for data in pool.imap(fetch, urls))
-        return func
-
-    def _headerfy(self, headers, **kw):
-        if headers:
-            new_headers = RequestHeaders(i for i in headers.iteritems())
-        else:
-            new_headers = RequestHeaders()
-        if kw.get('randomua', self._randomua):
-            new_headers.user_agent = new_headers._randomua()
-        return new_headers
-
-    # public methods
-
-    def _kwfy(self, kw):
-        # remove consumed data
-        newkw = subdictremove(
-            kw, ('auth', 'timeout', 'url', 'data', 'headers')
-        )
-        # preserve HTTP method
-        respkw = dict((k, v) for k, v in kw.iteritems() if k not in ('method',))
-        # use default misc if necessary
-        if not respkw: respkw = self._options
-        return kw, newkw, respkw
-
-    @lazy
-    def cookies(self):
-        '''
-        shortcut to header cookies
-        '''
-        return self.headers.cookies
-
-    @lazy
-    def delete(self):
-        '''
-        use "DELETE" HTTP method
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
-        '''
-        return partial(self.request, method='DELETE')
-
-    def dumps(self, data, fmt='url', **kw):
-        '''
-        serializes data in specific format
-
-        @param data: data serialized
-        @param format: serialization format (default: url (for URL encoding))
-        '''
-        try:
-            dumper = self._dumpers(fmt)
-            return dumper.type, dumper.dumps(data, **kw)
-        except ImportError:
-            raise TypeError(u'Unsupported format "{0}"'.format(fmt))
-
-    def fast_delete(self, **kw):
-        '''only returns delete data'''
-        return self.delete(**kw).code
-
-    def fast_get(self, **kw):
-        '''only returns get data'''
-        resp = self.get(**kw)
-        return resp.data if resp.data else resp.code
-
-    def fast_head(self, **kw):
-        '''only returns head headers'''
-        resp = self.head(**kw)
-        return dict(resp.headers) if not resp.fail else resp.code
-
-    def fast_options(self, **kw):
-        '''only returns options data'''
-        resp = self.options(**kw)
-        return resp.headers['allow'] if 'allow' in resp.headers else resp.code
-
-    def fast_post(self, **kw):
-        '''only returns post data'''
-        resp = self.post(**kw)
-        return resp.data if resp.data else resp.code
-
-    def fast_put(self, **kw):
-        '''only returns put HTTP status'''
-        resp = self.put(**kw)
-        return resp.data if resp.data else resp.code
-
-    @lazy
-    def get(self):
-        '''
-        use "GET" HTTP method
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
-        '''
-        return partial(self.request, method='GET')
-
-    @lazy
-    def head(self):
-        '''
-        use "HEAD" HTTP method
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
-        '''
-        return partial(self.request, method='HEAD')
-
-    @lazy
-    def headers(self):
-        '''
-        HTTP header manager
-        '''
-        return self._headerfy(self._headers)
-
-    @lazy
-    def mdelete(self):
-        '''
-        use "DELETE" HTTP method for multiple URLs
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
-        '''
-        return partial(self.multiple, method='DELETE')
-
-    @lazy
-    def mget(self):
-        '''
-        use "GET" HTTP method for multiple URLs
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
-        '''
-        return partial(self.multiple, method='GET')
-
-    @lazy
-    def mhead(self):
-        '''
-        use "HEAD" HTTP method for multiple URLs
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
-        '''
-        return partial(self.multiple, method='HEAD')
-
-    @lazy
-    def moptions(self):
-        '''
-        use "OPTIONS" HTTP method for multiple URLs
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
-        '''
-        return partial(self.multiple, method='OPTIONS')
-
-    @lazy
-    def mpost(self):
-        '''
-        use "POST" HTTP method for multiple URLs
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
-        '''
-        return partial(self.multiple, method='POST')
-
-    @lazy
-    def mput(self):
-        '''
-        use "PUT" HTTP method for multiple URLs
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
-        '''
-        return partial(self.multiple, method='PUT')
-
-    @lazy
-    def multiple(self):
-        try:
-            return self._eventletmultiple
-        except ImportError:
-            return self._defmultiple
-
-    @lazy
-    def options(self):
-        '''
-        use "OPTIONS" HTTP method
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
-        '''
-        return partial(self.request, method='OPTIONS')
-
-    @lazy
-    def post(self):
-        '''
-        use "POST" HTTP method
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
-        '''
-        return partial(self.request, method='POST')
-
-    @lazy
-    def put(self):
-        '''
-        use "PUT" HTTP method
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
-        '''
-        return partial(self.request, method='PUT')
-
-    def request(self, **kw):
+    def _request(self, **kw):
         '''make HTTP request'''
         kw.setdefault('url', self._url)
         headers = self._headerfy(kw.pop('headers', self._headers), **kw)
             **kw
         )
         kw['headers'] = headers
-        return self._request(**kw)
+        self._raw = self._request(**kw)
+        return self
 
+    # properties
 
-class CoreREST(CoreRequest):
+    def _kwfy(self, kw):
+        # remove consumed data
+        newkw = subdictremove(
+            kw, ('auth', 'timeout', 'url', 'data', 'headers')
+        )
+        # preserve HTTP method
+        respkw = dict((k, v) for k, v in kw.iteritems() if k not in ('method',))
+        # use default misc if necessary
+        if not respkw:
+            respkw = self._options
+        return kw, newkw, respkw
 
-    '''core support for REST clients'''
+    def get(self):
+        '''
+        *GET* HTTP method
 
-    def __init__(self, username, password, server, **kw):
-        # pass in username, password as normal "auth" for requests
-        kw['auth'] = dict(username=username, password=password)
-        super(CoreREST, self).__init__(**kw)
-        # base URL for REST service URLs
-        self.server = server
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
+        '''
+        return partial(self._request, method='GET')
+    
+    def post(self):
+        '''
+        *POST* HTTP method
 
-    @lazy
-    def _defmultiple(self):
-        request = self.request
-        server = self.server
-        def func(urls, **kw):
-            return list(request(url=server+url, **kw) for url in urls)
-        return func
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
+        '''
+        return partial(self._request, method='POST')
 
-    @lazy
-    def _eventletmultiple(self):
-        import eventlet
-        request = self.request
-        server = self.server
-        def func(urls, **kw):
-            def fetch(url):
-                return request(url=server+url, **kw)
-            pool = eventlet.GreenPool()
-            return list(data for data in pool.imap(fetch, urls))
-        return func
+    def delete(self):
+        '''
+        *DELETE* HTTP method
 
-    @lazy
-    def request(self):
-        '''make HTTP request'''
-        cr_request = self._cr_request
-        server = self.server
-        def func(**kw):
-            url = kw.get('url', '')
-            return cr_request(url=server+url, **kw)
-        return func
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
+        '''
+        return partial(self._request, method='DELETE')
+    
+    def put(self):
+        '''
+        *PUT* HTTP method
+
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
+        '''
+        return partial(self._request, method='PUT')
+
+    def head(self):
+        '''
+        *HEAD* HTTP method
+
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
+        '''
+        return partial(self._request, method='HEAD')
+
+    def options(self):
+        '''
+        *OPTIONS* HTTP method
+
+        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+        '''
+        return partial(self._request, method='OPTIONS')

File wire/http/response.py

 from collections import defaultdict
 
 from wire.http.util import statuscode, datehttp
+from wire.util import Immutable, lru_cache, hiddenhand
 from wire.http.header import CoreHeaders, CoreCookie, CoreMorsel
-from wire.util import Immutable, lru_cache, lazy, lazier, hiddenhand
+from stuf.utils import lazy_class
+from callchain.root.linked import chainlink
 
 
-class ResponseMorsel(Immutable):
+class Morsel(Immutable):
 
     '''HTTP response specific morsel'''
 
     def __init__(self):
-        super(ResponseMorsel, self).__init__()
+        super(Morsel, self).__init__()
         self._hset('_morsel', CoreMorsel())
 
     def __getattr__(self, key):
     def __getitem__(self, key):
         return self._morsel.__getitem__(key)
 
-    @lazy
     def _set(self):
         return self._morsel.set
 
-    @lazy
     def output(self):
         return self._morsel.output
 
-    @lazy
     def value(self):
         return self._morsel.value
 
 
-class ResponseCookie(object):
+class Cookie(object):
 
     '''HTTP response specific cookie'''
 
         class _ResponseCookie(CoreCookie):
             def _set(self, k, real_value, coded_value):
                 '''invisible setters'''
-                m = self.get(k, ResponseMorsel())
+                m = self.get(k, Morsel())
                 m._set(k, real_value, coded_value)
                 dict.__setitem__(self, k, m)
         return _ResponseCookie
 
 
-class ResponseHeaders(Immutable, CoreHeaders):
+class Headers(Immutable, CoreHeaders):
 
     '''HTTP response headers'''
 
     def __init__(self, headers):
-        super(ResponseHeaders, self).__init__()
+        super(Headers, self).__init__()
         newheaders = self._hset('_headers', dict())
         cookies = list()
         cappend = cookies.append
                 value = datehttp(value)
         return value
 
-    @lazier
+    @lazy_class
     def _headattr(self):
         '''Python name -> header name mapping'''
         return dict((self._checkname(h), h) for h in self.RES_HEADERS)
 
-    @lazy
     def cookies(self):
         '''HTTP cookies'''
-        if self._cookies: return ResponseCookie(self._cookies)
+        if self._cookies: return Cookie(self._cookies)
         return ''
 
     def get(self, k, default=None):
         self._headers[k] = v = self._set(k, v)
         return self._hset(tk, v)
 
-    @lazy
     def items(self):
         '''all the header fields and values'''
         return lru_cache()(self._items)
 
-    @lazy
     def keys(self):
         '''list of all header keys'''
         return lru_cache()(self._keys)
 
-    @lazy
     def values(self):
         '''a list of all header values'''
         return lru_cache()(self._values)
-
-
-class CoreResponse(Immutable):
-
-    '''Core HTTP response manager'''
-
-    def __init__(self, **kw):
-        super(CoreResponse, self).__init__()
-        hiddenhand(self, '_options', kw)
-
-    def __getattr__(self, k):
-        try:
-            return object.__getattribute__(self, k)
-        except AttributeError:
-            if k in self._loaders.apps:
-                return self._hset(
-                    k, self._loads(self._original, k, **self._options)
-                )
-
-    def __len__(self):
-        return int(self.headers.content_length)
-
-    def __str__(self):
-        return self._original
-
-    @lazier
-    def _loaders(self):
-        '''deserializers'''
-        from wire.talk.app import TALKERS
-        return TALKERS
-
-    @lazier
+    
+    
+class Data(chainlink):
+    
+    def data(self):
+        '''data, serialized if supported, original if not'''
+        return self._hset(
+            self.format,
+            self._loads(self._original, self.format, **self._options),
+        )
+        
+    @lazy_class
     def _mime(self):
         '''built-in serialization mimetypes'''
         MIME = defaultdict(lambda: 'application/octet-stream')
             'application/x-yaml': 'yaml',
         })
         return MIME
+        
+    def format(self):
+        return self._options.pop('format', self._mime[self.content_type])
+    
+    def raw_data(self):
+        return self._original
+    
+    def touched(self):
+        '''Verify MD5 digest of response data against "Content-MD5" digest'''
+        digest = self.headers.content_md5
+        if digest is not None and digest:
+            import md5
+            from base64 import b64decode
+            digest = b64decode(digest)
+            body_digest = md5.new(self._original).digest()
+            if digest == body_digest: 
+                return False
+        return True
+    
+    def read(self):
+        '''read original response content'''
+        return StringIO(self._original).read()
 
-    @lazy
+
+class CoreResponse(Immutable):
+
+    '''Core HTTP response manager'''
+
+    def __init__(self, **kw):
+        super(CoreResponse, self).__init__()
+        hiddenhand(self, '_options', kw)
+
+    def __getattr__(self, k):
+        try:
+            return object.__getattribute__(self, k)
+        except AttributeError:
+            if k in self._loaders.apps:
+                return self._hset(
+                    k, self._loads(self._original, k, **self._options)
+                )
+
+    def __len__(self):
+        return int(self.headers.content_length)
+
+    def __str__(self):
+        return self._original
+
     def code(self):
         '''
         HTTP response code
         '''
         return int(self._code)
 
-    @lazy
-    def content_type(self):
-        '''
-        HTTP content type
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
-        '''
-        return self.headers.content_type
-
-    @lazy
-    def cookies(self):
-        '''shortcut to headers cookies'''
-        return self.headers.cookies
-
-    @lazy
-    def data(self):
-        '''data, serialized if supported, original if not'''
-        return self._hset(
-            self.format,
-            self._loads(self._original, self.format, **self._options),
-        )
-
-    @lazy
     def good(self):
         '''True if HTTP was successful enough'''
         return self._code.startswith('20')
 
-    @lazy