| 
7
 | 
     1 """
 | 
| 
 | 
     2 abstract ReSTful HTTP client
 | 
| 
 | 
     3 """
 | 
| 
 | 
     4 
 | 
| 
 | 
     5 # imports
 | 
| 
 | 
     6 import requests
 | 
| 
 | 
     7 import sys
 | 
| 
 | 
     8 import time
 | 
| 
 | 
     9 from urlparse import urlparse
 | 
| 
 | 
    10 from .cli import ConfigurationParser
 | 
| 
 | 
    11 
 | 
| 
 | 
    12 
 | 
| 
 | 
    13 def isurl(string):
 | 
| 
 | 
    14     """is `string` a URL?"""
 | 
| 
 | 
    15 
 | 
| 
 | 
    16     return bool(urlparse(string).scheme)
 | 
| 
 | 
    17 
 | 
| 
 | 
    18 
 | 
| 
 | 
    19 def serialize_headers(headers):
 | 
| 
 | 
    20     return '\n'.join(["{key}: {value}".format(key=key, value=value)
 | 
| 
 | 
    21                       for key, value in sorted(headers.items(),
 | 
| 
 | 
    22                                                key=lambda x: x[0])
 | 
| 
 | 
    23     ])
 | 
| 
 | 
    24 
 | 
| 
 | 
    25 
 | 
| 
 | 
    26 def serialize_request(request):
 | 
| 
 | 
    27     """serialize a request object to a string"""
 | 
| 
 | 
    28 
 | 
| 
 | 
    29     template = u"""{method} {url}
 | 
| 
 | 
    30 {headers}
 | 
| 
 | 
    31 
 | 
| 
 | 
    32 {body}
 | 
| 
 | 
    33 """
 | 
| 
 | 
    34     headers = '\n'.join(['{key}: {value}'.format(key=key, value=value)
 | 
| 
 | 
    35                          for key, value in request.headers.items()])
 | 
| 
 | 
    36     retval = template.format(method=request.method,
 | 
| 
 | 
    37                              url=request.url,
 | 
| 
 | 
    38                              headers=headers,
 | 
| 
 | 
    39                              body=request.body or '')
 | 
| 
 | 
    40     return retval
 | 
| 
 | 
    41 
 | 
| 
 | 
    42 
 | 
| 
 | 
    43 def serialize_response(response):
 | 
| 
 | 
    44     """serialize a response object to a string"""
 | 
| 
 | 
    45 
 | 
| 
 | 
    46     template = u"""{url}
 | 
| 
 | 
    47 {status_code}
 | 
| 
 | 
    48 {headers}
 | 
| 
 | 
    49 
 | 
| 
 | 
    50 {text}
 | 
| 
 | 
    51 """
 | 
| 
 | 
    52 
 | 
| 
 | 
    53     retval = template.format(url=response.url,
 | 
| 
 | 
    54                              status_code=response.status_code,
 | 
| 
 | 
    55                              headers=serialize_headers(response.headers),
 | 
| 
 | 
    56                              text=response.text.strip())
 | 
| 
 | 
    57     return retval
 | 
| 
 | 
    58 
 | 
| 
 | 
    59 
 | 
| 
 | 
    60 class Endpoint(object):
 | 
| 
 | 
    61     """abstract base class for a RESTful API client"""
 | 
| 
 | 
    62 
 | 
| 
 | 
    63     path = ''
 | 
| 
 | 
    64     endpoints = []
 | 
| 
 | 
    65 
 | 
| 
 | 
    66     def __init__(self, base_url, session=None, timeout=60.):
 | 
| 
 | 
    67         base_url = base_url.rstrip('/')
 | 
| 
 | 
    68         self.url = '{}/{}'.format(base_url, self.path)
 | 
| 
 | 
    69         self.timeout = timeout
 | 
| 
 | 
    70         if session is None:
 | 
| 
 | 
    71             session = requests.Session()
 | 
| 
 | 
    72         self.session = session
 | 
| 
 | 
    73         for endpoint in self.endpoints:
 | 
| 
 | 
    74             path = endpoint.path
 | 
| 
 | 
    75             setattr(self,
 | 
| 
 | 
    76                     path,
 | 
| 
 | 
    77                     endpoint(self.url, session=self.session, timeout=timeout))
 | 
| 
 | 
    78 
 | 
| 
 | 
    79     def __call__(self, method, url, **kwargs):
 | 
| 
 | 
    80         """make an HTTP request"""
 | 
| 
 | 
    81 
 | 
| 
 | 
    82         kwargs.setdefault('timeout', self.timeout)
 | 
| 
 | 
    83         start = time.time()
 | 
| 
 | 
    84         response = self.session.request(method, url, **kwargs)
 | 
| 
 | 
    85         end = time.time()
 | 
| 
 | 
    86         response.duration = end - start
 | 
| 
 | 
    87         try:
 | 
| 
 | 
    88             response.raise_for_status()
 | 
| 
 | 
    89         except requests.HTTPError as e:
 | 
| 
 | 
    90             sys.stderr.write(serialize_response(response) + '\n')
 | 
| 
 | 
    91             sys.stderr.write("=>\n")
 | 
| 
 | 
    92             sys.stderr.write(serialize_request(response.request) + '\n')
 | 
| 
 | 
    93             raise
 | 
| 
 | 
    94         return response
 | 
| 
 | 
    95 
 | 
| 
 | 
    96     def POST(self, data, **kwargs):
 | 
| 
 | 
    97         return self('POST', self.url, data=data, **kwargs)
 | 
| 
 | 
    98 
 | 
| 
 | 
    99 
 | 
| 
 | 
   100 class ClientParser(ConfigurationParser):
 | 
| 
 | 
   101     """abstract argument parser for HTTP client"""
 | 
| 
 | 
   102 
 | 
| 
 | 
   103     client_class = Endpoint
 | 
| 
 | 
   104 
 | 
| 
 | 
   105     def add_arguments(self):
 | 
| 
 | 
   106         self.add_argument('base_url', help="base URL")
 | 
| 
 | 
   107         self.add_argument('--timeout', dest='timeout',
 | 
| 
 | 
   108                           type=float, default=60.,
 | 
| 
 | 
   109                           help='per request timeout in seconds [DEFAULT: %(default)s]')
 | 
| 
 | 
   110 
 | 
| 
 | 
   111     def base_url(self):
 | 
| 
 | 
   112         return self.options.base_url
 | 
| 
 | 
   113 
 | 
| 
 | 
   114     def client(self):
 | 
| 
 | 
   115         """return argument specified requests HTTP client"""
 | 
| 
 | 
   116 
 | 
| 
 | 
   117         if self.options is None:
 | 
| 
 | 
   118             raise Exception("options not yet parsed!")
 | 
| 
 | 
   119 
 | 
| 
 | 
   120         return self.client_class(self.base_url(),
 | 
| 
 | 
   121                                  timeout=self.options.timeout)
 |