Skip to content

Breakout Requests Functionality to New Adapter Module #223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 19, 2018
Merged
Prev Previous commit
Next Next commit
Add adapters and utils modules
  • Loading branch information
Jeffrey Hogan committed Jul 16, 2018
commit 8bc0c971417a07e39ab323f07f9fd13cbc4ce4c6
16 changes: 16 additions & 0 deletions docs/source/hvac.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ hvac.v1.Client
:undoc-members:
:show-inheritance:

hvac.utils
----------------------

.. automodule:: hvac.utils
:members:
:undoc-members:
:show-inheritance:

hvac.aws\_utils
----------------------

Expand All @@ -17,6 +25,14 @@ hvac.aws\_utils
:undoc-members:
:show-inheritance:

hvac.adapters
----------------------

.. automodule:: hvac.adapters
:members:
:undoc-members:
:show-inheritance:

hvac.exceptions
----------------------

Expand Down
203 changes: 203 additions & 0 deletions hvac/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# coding=utf-8
"""
HTTP Client Library Adapters

"""
import logging

import requests
import requests.exceptions

from hvac import utils
from abc import ABCMeta, abstractmethod

logger = logging.getLogger(__name__)


class Adapter(object):
"""Abstract base class used when constructing adapters for use with the Client class."""
__metaclass__ = ABCMeta

@staticmethod
def urljoin(*args):
"""Joins given arguments into a url. Trailing and leading slashes are stripped for each argument.

:param args: Multiple parts of a URL to be combined into one string.
:type args: str
:return: Full URL combining all provided arguments
:rtype: str
"""

return '/'.join(map(lambda x: str(x).strip('/'), args))

@abstractmethod
def close(self):
raise NotImplemented

@abstractmethod
def get(self, url, **kwargs):
raise NotImplemented

@abstractmethod
def post(self, url, **kwargs):
raise NotImplemented

@abstractmethod
def put(self, url, **kwargs):
raise NotImplemented

@abstractmethod
def delete(self, url, **kwargs):
raise NotImplemented

@abstractmethod
def request(self, method, url, headers=None, **kwargs):
raise NotImplemented


class Request(Adapter):
"""The Request adapter class"""

def __init__(self, base_uri='http://localhost:8200', token=None, cert=None, verify=True, timeout=30, proxies=None,
allow_redirects=True, session=None):
"""Create a new request adapter instance.

:param base_uri: Base URL for the Vault instance being addressed.
:type base_uri: str
:param token: Authentication token to include in requests sent to Vault.
:type token: str
:param cert: Certificates for use in requests sent to the Vault instance. This should be a tuple with the
certificate and then key.
:type cert: tuple
:param verify: Flag to indicate whether TLS verification should be performed when sending requests to Vault.
:type verify: bool
:param timeout: The timeout value for requests sent to Vault.
:type timeout: int
:param proxies: Proxies to use when preforming requests.
See: http://docs.python-requests.org/en/master/user/advanced/#proxies
:type proxies: dict
:param allow_redirects: Whether to follow redirects when sending requests to Vault.
:type allow_redirects: bool
:param session: Optional session object to use when performing request.
:type session: request.Session
"""
if not session:
session = requests.Session()

self.base_uri = base_uri
self.token = token
self.session = session
self.allow_redirects = allow_redirects

self._kwargs = {
'cert': cert,
'verify': verify,
'timeout': timeout,
'proxies': proxies,
}

def close(self):
"""Close the underlying Requests session.
"""
self.session.close()

def get(self, url, **kwargs):
"""Performs a GET request.

:param url: Partial URL path to send the request to. This will be joined to the end of the instance's base_uri
attribute.
:type url: str
:param kwargs: Additional keyword arguments to include in the requests call.
:type kwargs: dict
:return: The response of the request.
:rtype: requests.Response
"""
return self.request('get', url, **kwargs)

def post(self, url, **kwargs):
"""Performs a POST request.

:param url: Partial URL path to send the request to. This will be joined to the end of the instance's base_uri
attribute.
:type url: str
:param kwargs: Additional keyword arguments to include in the requests call.
:type kwargs: dict
:return: The response of the request.
:rtype: requests.Response
"""
return self.request('post', url, **kwargs)

def put(self, url, **kwargs):
"""Performs a PUT request.

:param url: Partial URL path to send the request to. This will be joined to the end of the instance's base_uri
attribute.
:type url: str
:param kwargs: Additional keyword arguments to include in the requests call.
:type kwargs: dict
:return: The response of the request.
:rtype: requests.Response
"""
return self.request('put', url, **kwargs)

def delete(self, url, **kwargs):
"""Performs a DELETE request.

:param url: Partial URL path to send the request to. This will be joined to the end of the instance's base_uri
attribute.
:type url: str
:param kwargs: Additional keyword arguments to include in the requests call.
:type kwargs: dict
:return: The response of the request.
:rtype: requests.Response
"""
return self.request('delete', url, **kwargs)

def request(self, method, url, headers=None, **kwargs):
"""

:param method: HTTP method to use with the request. E.g., GET, POST, etc.
:type method: str
:param url: Partial URL path to send the request to. This will be joined to the end of the instance's base_uri
attribute.
:type url: str
:param headers: Additional headers to include with the request.
:type headers: dict
:param kwargs: Additional keyword arguments to include in the requests call.
:type kwargs: dict
:return: The response of the request.
:rtype: requests.Response
"""
url = self.urljoin(self.base_uri, url)

if not headers:
headers = {}

if self.token:
headers['X-Vault-Token'] = self.token

wrap_ttl = kwargs.pop('wrap_ttl', None)
if wrap_ttl:
headers['X-Vault-Wrap-TTL'] = str(wrap_ttl)

_kwargs = self._kwargs.copy()
_kwargs.update(kwargs)

response = self.session.request(method, url, headers=headers,
allow_redirects=False, **_kwargs)

# NOTE(ianunruh): workaround for https://github.com/ianunruh/hvac/issues/51
while response.is_redirect and self.allow_redirects:
url = self.urljoin(self.base_uri, response.headers['Location'])
response = self.session.request(method, url, headers=headers,
allow_redirects=False, **_kwargs)

if response.status_code >= 400 and response.status_code < 600:
text = errors = None
if response.headers.get('Content-Type') == 'application/json':
errors = response.json().get('errors')
if errors is None:
text = response.text
utils.raise_for_error(response.status_code, text, errors=errors)

return response
32 changes: 32 additions & 0 deletions hvac/tests/test_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from unittest import TestCase

import requests_mock
from parameterized import parameterized

from hvac import adapters


class TestRequest(TestCase):
"""Unit tests providing coverage for requests-related methods in the hvac Client class."""

@parameterized.expand([
("standard Vault address", 'https://localhost:8200'),
("Vault address with route", 'https://example.com/vault'),
])
@requests_mock.Mocker()
def test___request(self, test_label, test_url, requests_mocker):
test_path = 'v1/sys/health'
expected_status_code = 200
mock_url = '{0}/{1}'.format(test_url, test_path)
requests_mocker.register_uri(
method='GET',
url=mock_url,
)
adapter = adapters.Request(base_uri=test_url)
response = adapter.get(
url='v1/sys/health',
)
self.assertEquals(
first=expected_status_code,
second=response.status_code,
)
92 changes: 92 additions & 0 deletions hvac/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Misc utility functions and constants
"""

import functools
import warnings
from textwrap import dedent

from hvac import exceptions


def raise_for_error(status_code, message=None, errors=None):
"""

:param status_code:
:type status_code:
:param message:
:type message:
:param errors:
:type errors:
:return:
:rtype:
"""
if status_code == 400:
raise exceptions.InvalidRequest(message, errors=errors)
elif status_code == 401:
raise exceptions.Unauthorized(message, errors=errors)
elif status_code == 403:
raise exceptions.Forbidden(message, errors=errors)
elif status_code == 404:
raise exceptions.InvalidPath(message, errors=errors)
elif status_code == 429:
raise exceptions.RateLimitExceeded(message, errors=errors)
elif status_code == 500:
raise exceptions.InternalServerError(message, errors=errors)
elif status_code == 501:
raise exceptions.VaultNotInitialized(message, errors=errors)
elif status_code == 503:
raise exceptions.VaultDown(message, errors=errors)
else:
raise exceptions.UnexpectedError(message)


def deprecated_method(to_be_removed_in_version, new_call_path=None, new_method=None):
"""This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.

:param to_be_removed_in_version: Version of this module the decorated method will be removed in.
:type to_be_removed_in_version: str
:param new_call_path: Example call to replace deprecated usage.
:type new_call_path: str
:param new_method: Method intended to replace the decorated method. This method's docstrings are included in the
decorated method's docstring.
:type new_method: function
:return: Wrapped function that includes a deprecation warning and update docstrings from the replacement method.
:rtype: types.FunctionType
"""
def decorator(method):
message = "Call to deprecated function '{old_func}'. This method will be removed in version '{version}'".format(
old_func=method.__name__,
version=to_be_removed_in_version,
)
if new_call_path:
message += " Please use `{}` moving forward.".format(new_call_path)

@functools.wraps(method)
def new_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter

warnings.warn(
message=message,
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return method(*args, **kwargs)
if new_method:
new_func.__doc__ = dedent(
"""\
{message}
Docstring content from this method's replacement copied below:
{new_docstring}
""".format(
message=message,
new_docstring=new_method.__doc__,
)
)
else:
new_func.__doc__ = message
return new_func
return decorator