Files
pillar-python-sdk/pillarsdk/api.py

226 lines
7.4 KiB
Python

import copy
import base64
import requests
import json
import logging
import platform
import sys
from . import utils
from . import exceptions
from .config import __version__
log = logging.getLogger(__name__)
class Api(object):
# User-Agent for HTTP request
library_details = "requests {0}; python {1}".format(
requests.__version__, platform.python_version())
user_agent = "Pillar-Python-SDK/{0} ({1})".format(
__version__, library_details)
_api_singleton = None
# Global session object to do HTTP requests.
requests_session = requests.session()
# HTTP headers that should always be sent with every request.
global_headers = {}
def __init__(self, options=None, **kwargs):
"""Create API object
Usage::
>>> from pillarsdk import Api
>>> Api.Default(
endpoint="http://localhost:5000",
username='USERNAME',
password='PASSWORD'
)
"""
kwargs = utils.merge_dict(options, kwargs)
self.endpoint = kwargs["endpoint"]
self.username = kwargs["username"]
self.password = kwargs["password"]
self.token = kwargs["token"] if kwargs.get("token") else None
self.options = kwargs
@staticmethod
def Default(**kwargs):
"""Initialize the API in a singleton style
"""
if Api._api_singleton is None or kwargs:
Api._api_singleton = Api(
endpoint=kwargs.get("endpoint"),
username=kwargs.get("username"),
password=kwargs.get("password"),
token=kwargs.get("token"))
return Api._api_singleton
def basic_auth(self, token=None):
"""Returns base64 encoded token. Used to encode credentials
for retrieving the token.
"""
if token:
credentials = "%s:%s" % (token, self.password or '')
else:
credentials = "%s:%s" % (self.username, self.password)
return base64.b64encode(credentials.encode('utf-8')).decode('utf-8').replace("\n", "")
def get_token(self):
"""Generate new token by making a POST request
"""
return self.token
"""
path = "/tokens"
payload = {'username': self.username}
if self.token:
return self.token
else:
# If token is not set we do initial request with username and password
self.token = self.http_call(
utils.join_url(self.endpoint, path), "POST",
data=payload,
headers={
"Authorization": ("Basic {0}".format(self.basic_auth())),
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"User-Agent": self.user_agent
})
return self.token
"""
def request(self, url, method, body=None, headers=None, files=None):
"""Make HTTP call, formats response and does error handling.
Uses http_call method in API class.
:param files: Dictionary of files to be uploaded via POST
"""
http_headers = utils.merge_dict(self.headers(), headers)
if http_headers.get('Pillar-Request-Id'):
log.info("Pillar-Request-Id: %s", http_headers['Pillar-Request-Id'])
try:
# Support for Multipart-Encoded file upload
if files and method in ['POST', 'PUT', 'PATCH']:
return self.http_call(
url, method,
data=body,
files=files,
headers=http_headers)
else:
http_headers['Content-Type'] = "application/json"
return self.http_call(url, method,
data=json.dumps(body),
headers=http_headers)
# Handle unauthorized token
except exceptions.UnauthorizedAccess as error:
raise error
def http_call(self, url, method, **kwargs):
"""Makes a http call. Logs response information.
"""
try:
response = self.requests_session.request(method, url, **kwargs)
except Exception as ex:
log.warning('Error performing HTTP %s request to %s: %s', method, url, str(ex))
raise
try:
content = self.handle_response(response, response.text)
except:
log.info("Response[%s]: %s", response.status_code, response.reason)
raise
return content
def handle_response(self, response, content):
"""Check HTTP response codes
"""
status = response.status_code
if 200 <= status <= 299:
return json.loads(content) if content else {}
exception = exceptions.exception_for_status(status)
if exception:
raise exception(response, content)
raise exceptions.ConnectionError(response, content, "Unknown response code: %s" % status)
def headers(self):
"""Default HTTP headers
"""
token = self.get_token()
headers = copy.deepcopy(self.global_headers)
headers.update({
# "Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": self.user_agent
})
if token:
headers['Authorization'] = (
"Basic {0}".format(self.basic_auth(token=token)))
return headers
def get(self, action, headers=None):
"""Make GET request
"""
return self.request(utils.join_url(self.endpoint, action), 'GET',
headers=headers)
def post(self, action, params=None, headers=None, files=None):
"""Make POST request
"""
return self.request(utils.join_url(self.endpoint, action), 'POST',
body=params, headers=headers, files=files)
def put(self, action, params=None, headers=None):
"""Make PUT request
"""
return self.request(utils.join_url(self.endpoint, action), 'PUT',
body=params, headers=headers)
def patch(self, action, params=None, headers=None, files=None):
"""Make PATCH request
"""
return self.request(utils.join_url(self.endpoint, action), 'PATCH',
body=params, headers=headers, files=files)
def delete(self, action, headers=None):
"""Make DELETE request
"""
return self.request(utils.join_url(self.endpoint, action), 'DELETE',
headers=headers)
def OPTIONS(self, action, headers=None):
"""Make OPTIONS request.
Contrary to other requests, this method returns the raw requests.Response object.
:rtype: requests.Response
"""
http_headers = utils.merge_dict(self.headers(), headers)
url = utils.join_url(self.endpoint, action)
response = self.requests_session.request('OPTIONS', url, headers=http_headers)
if 200 <= response.status_code <= 299:
return response
exception = exceptions.exception_for_status(response.status_code)
if exception:
raise exception(response, response.text)
raise exceptions.ConnectionError(response, response.text,
"Unknown response code: %s" % response.status_code)