""" Client class and exception for performing basic ReST API interactions. """ from __future__ import print_function import base64 import os import sys import copy import requests class RestHttpError(Exception): """ Exception object that returns error from remote call. """ def __init__(self, http_status, http_reason, msg=None, code=None): self.http_status = int(http_status) self.http_error = '%s %s' % (http_status, http_reason) self.msg = msg self.code = code def __int__(self): """Get HTTP status code.""" return self.http_status def __str__(self): """Get error message.""" if self.msg: return '%s: %s' % (self.http_error, self.msg) return self.http_error def status(self): """Get HTTP status.""" return self.http_status class ConnectionError(Exception): def __init__(self, message, code, detail=None): if detail: self.msg = '%s: %s' % (message.rstrip('.'), detail) else: self.msg = message.rstrip('.') self.code = int(code) def __int__(self): """Get errno value.""" return self.code def __str__(self): """Get error message.""" return self.msg def __repr__(self): """Get object representation string.""" if self.msg.find(': ') != -1: m, d = self.msg.split(': ', 1) return 'ConnectionError(message=%s, code=%d, detail=%s)' % ( m, self.code, d) return 'ConnectionError(message=%s, code=%d)' % (self.msg, self.code) class RestHttp(object): """ ReST API HTTP client wrapper object base class. Derive application-specific ReST API class from this base class. """ def __init__(self, base_url, user=None, password=None, ssl_verify=True, debug_print=False, timeout=None): """Initialize the ReST API HTTP wrapper object. Arguments: base_url -- Base URL for requests. Ex: http://example.com/stuff/ user -- Optional user name for basic auth. password -- Optional password for basic auth. ssl_verify -- Set to False to disable SSL verification (not secure). debug_print -- Enable debug print statements. timeout -- Number of seconds to wait for a response. """ self._base_url = base_url.strip('/') self._base_headers = {'Accept': 'application/json'} self._user = user self._password = password self._verify = ssl_verify self._dbg_print = debug_print self._timeout = None if timeout: self._timeout = timeout # autheticated API if user and password: b64string = base64.encodestring('%s:%s' % (user, password))[:-1] self._base_headers["Authorization"] = "Basic %s" % b64string @staticmethod def url(proto, server, port=None, uri=None): """Construct a URL from the given components.""" url_parts = [proto, '://', server] if port: port = int(port) if port < 1 or port > 65535: raise ValueError('invalid port value') if not ((proto == 'http' and port == 80) or (proto == 'https' and port == 443)): url_parts.append(':') url_parts.append(str(port)) if uri: url_parts.append('/') url_parts.append(requests.utils.quote(uri.strip('/'))) url_parts.append('/') return ''.join(url_parts) def debug_print(self): """Return True if debug printing enabled.""" return self._dbg_print def enable_debug_print(self): """Turn debug printing on.""" self._dbg_print = True def disable_debug_print(self): """Turn debug printing off.""" self._dbg_print = False def timeout(self): """Return the current timeout value.""" return self._timeout def set_timeout(self, timeout): """Seconds to wait for a response. Any zero-value means no timeout.""" if not timeout: self._timeout = None return self._timeout = timeout def add_header(self, header, value): """Include additional header with each request.""" self._base_headers[header] = value def del_header(self, header): """Removed a header from those included with each request.""" self._base_headers.pop(header, None) def base_url(self): """Return the base URL used for each request.""" return self._base_url def make_url(self, container=None, resource=None, query_items=None): """Create a URL from the specified parts.""" pth = [self._base_url] if container: pth.append(container.strip('/')) if resource: pth.append(resource) else: pth.append('') url = '/'.join(pth) if isinstance(query_items, (list, tuple, set)): url += RestHttp._list_query_str(query_items) query_items = None p = requests.PreparedRequest() p.prepare_url(url, query_items) return p.url def head_request(self, container, resource=None): """Send a HEAD request.""" url = self.make_url(container, resource) headers = self._make_headers(None) try: rsp = requests.head(url, headers=self._base_headers, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('HEAD', rsp.url, headers, None) return rsp.status_code def get_request(self, container, resource=None, query_items=None, accept=None, to_lower=False): """Send a GET request.""" url = self.make_url(container, resource) headers = self._make_headers(accept) if query_items and isinstance(query_items, (list, tuple, set)): url += RestHttp._list_query_str(query_items) query_items = None try: rsp = requests.get(url, query_items, headers=headers, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('GET', rsp.url, headers, None) return self._handle_response(rsp, to_lower) def post_request(self, container, resource=None, params=None, accept=None): """Send a POST request.""" url = self.make_url(container, resource) headers = self._make_headers(accept) try: rsp = requests.post(url, data=params, headers=headers, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('POST', rsp.url, headers, params) return self._handle_response(rsp) def put_request(self, container, resource=None, params=None, accept=None): """Send a PUT request.""" url = self.make_url(container, resource) headers = self._make_headers(accept) try: rsp = requests.put(url, params, headers=headers, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('PUT', rsp.url, headers, params) return self._handle_response(rsp) def delete_request(self, container, resource=None, query_items=None, accept=None): """Send a DELETE request.""" url = self.make_url(container, resource) headers = self._make_headers(accept) if query_items and isinstance(query_items, (list, tuple, set)): url += RestHttp._list_query_str(query_items) query_items = None try: rsp = requests.delete(url, params=query_items, headers=headers, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('DELETE', rsp.url, headers, None) return self._handle_response(rsp) def download_file(self, container, resource, save_path=None, accept=None, query_items=None): """Download a file. If a timeout defined, it is not a time limit on the entire download; rather, an exception is raised if the server has not issued a response for timeout seconds (more precisely, if no bytes have been received on the underlying socket for timeout seconds). If no timeout is specified explicitly, requests do not time out. """ url = self.make_url(container, resource) if not save_path: save_path = resource.split('/')[-1] headers = self._make_headers(accept) if query_items and isinstance(query_items, (list, tuple, set)): url += RestHttp._list_query_str(query_items) query_items = None try: rsp = requests.get(url, query_items, headers=headers, stream=True, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('GET', rsp.url, headers, None) if rsp.status_code >= 300: raise RestHttpError(rsp.status_code, rsp.reason, rsp.text) file_size_dl = 0 try: with open(save_path, 'wb') as f: for buff in rsp.iter_content(chunk_size=16384): f.write(buff) except Exception as e: raise RuntimeError('could not download file: ' + str(e)) finally: rsp.close() if self._dbg_print: print('===> downloaded %d bytes to %s' % (file_size_dl, save_path)) return rsp.status_code, save_path, os.path.getsize(save_path) def upload_file(self, container, src_file_path, dst_name=None, put=True, content_type=None): """Upload a single file.""" if not os.path.exists(src_file_path): raise RuntimeError('file not found: ' + src_file_path) if not dst_name: dst_name = os.path.basename(src_file_path) if not content_type: content_type = "application/octet.stream" headers = dict(self._base_headers) if content_type: headers["content-length"] = content_type else: headers["content-length"] = "application/octet.stream" headers["content-length"] = str(os.path.getsize(src_file_path)) headers['content-disposition'] = 'attachment; filename=' + dst_name if put: method = 'PUT' url = self.make_url(container, dst_name, None) else: method = 'POST' url = self.make_url(container, None, None) with open(src_file_path, 'rb') as up_file: try: rsp = requests.request(method, url, headers=headers, data=up_file, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) return self._handle_response(rsp) def upload_file_mp(self, container, src_file_path, dst_name=None, content_type=None): """Upload a file using multi-part encoding.""" if not os.path.exists(src_file_path): raise RuntimeError('file not found: ' + src_file_path) if not dst_name: dst_name = os.path.basename(src_file_path) if not content_type: content_type = "application/octet.stream" url = self.make_url(container, None, None) headers = self._base_headers with open(src_file_path, 'rb') as up_file: files = {'file': (dst_name, up_file, content_type)} try: rsp = requests.post(url, headers=headers, files=files, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) return self._handle_response(rsp) def upload_files(self, container, src_dst_map, content_type=None): """Upload multiple files.""" if not content_type: content_type = "application/octet.stream" url = self.make_url(container, None, None) headers = self._base_headers multi_files = [] try: for src_path in src_dst_map: dst_name = src_dst_map[src_path] if not dst_name: dst_name = os.path.basename(src_path) multi_files.append( ('files', (dst_name, open(src_path, 'rb'), content_type))) rsp = requests.post(url, headers=headers, files=multi_files, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) finally: for n, info in multi_files: dst, f, ctype = info f.close() return self._handle_response(rsp) ########################################################################### # private methods # def _make_headers(self, accept): if accept: headers = dict(self._base_headers) headers['Accept'] = accept return headers return self._base_headers def _handle_response(self, rsp, to_lower=False): if self._dbg_print: print('===> response status:', rsp.status_code, rsp.reason) app_json = 'application/json' data = None if rsp.status_code != 204: if rsp.headers.get('content-type', app_json).startswith(app_json): try: data = rsp.json() except Exception: data = None if data is None: data = rsp.content if sys.hexversion < 0x03000000: data = self._conv_to_str2(data, to_lower) else: data = self._conv_to_str3(data, to_lower) if self._dbg_print: print('===> response content-type:', rsp.headers.get('content-type')) print('===> DATA:', data) if rsp.status_code >= 300: code = None detail = None if (data and rsp.headers.get('content-type', '').startswith(app_json)): if isinstance(data, dict): if 'detail' in data: detail = data['detail'] elif 'message' in data: detail = data['message'] elif data: detail = 'unknown error: ' + str(data) code = data.get('code') else: detail = 'unknown error: ' + str(data) raise RestHttpError(rsp.status_code, rsp.reason, detail, code) return rsp.status_code, data @staticmethod def _list_query_str(items): return '?' + '&'.join(items) def _conv_to_str3(self, data, lc): if isinstance(data, dict): return {self._conv_to_str3(k, lc): self._conv_to_str3(v, lc) for k, v in data.items()} if isinstance(data, list): return [self._conv_to_str3(i, lc) for i in data] if isinstance(data, bytes): if lc: return data.decode().lower() return data.decode() if isinstance(data, str) and lc: return data.lower() return data def _conv_to_str2(self, data, lc): if isinstance(data, dict): return {self._conv_to_str2(k, lc): self._conv_to_str2(v, lc) for k, v in data.iteritems()} if isinstance(data, list): return [self._conv_to_str2(i, lc) for i in data] if isinstance(data, unicode): if lc: return str(data).lower() return str(data) if isinstance(data, str) and lc: return data.lower() return data @staticmethod def _raise_conn_error(e): if isinstance(e, requests.exceptions.SSLError): raise ConnectionError(str(e), -1) try: msg, err = e.message num, detail = err except: msg = str(e) num = -1 detail = None raise ConnectionError(msg, num, detail) def __print_req(self, method, url, headers, params): print('===> %s %s' % (method, url)) print(' --- Headers ---') for k, v in headers.items(): print(' %s: %s' % (k, v)) if params: print(' --- Params ---') print(' ', params) def bulk_get_request(self, container, resource=None, query_items=None, depth=1, accept=None, to_lower=False): """Send a GET request.""" url = self.make_url(container, resource) headers = self._make_headers(accept) myheaders = copy.deepcopy(headers) myheaders["X-STC-API-Children-Depth"] = str(depth) if query_items and isinstance(query_items, (list, tuple, set)): url += RestHttp._list_query_str(query_items) query_items = None try: rsp = requests.get(url, query_items, headers=myheaders, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('GET', rsp.url, headers, None) return self._handle_response(rsp, to_lower) def bulk_put_request(self, container, resource=None, params=None, accept=None): """Send a PUT request.""" url = self.make_url(container, resource) headers = self._make_headers(accept) myheaders = copy.deepcopy(headers) myheaders["content-length"] = str(len(params)) myheaders["content-type"] = "application/json" try: rsp = requests.put(url, params, headers=myheaders, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('PUT', rsp.url, headers, params) return self._handle_response(rsp) def bulk_post_request(self, container, resource=None, params=None, accept=None): """Send a POST request.""" url = self.make_url(container, resource) headers = self._make_headers(accept) myheaders = copy.deepcopy(headers) myheaders["content-length"] = str(len(params)) myheaders["content-type"] = "application/json" try: rsp = requests.post(url, data=params, headers=myheaders, verify=self._verify, timeout=self._timeout) except requests.exceptions.ConnectionError as e: RestHttp._raise_conn_error(e) if self._dbg_print: self.__print_req('POST', rsp.url, headers, params) return self._handle_response(rsp)