Skip to content

Commit

Permalink
Merge 3f754c8 into 966659e
Browse files Browse the repository at this point in the history
  • Loading branch information
Ira Miller committed Oct 19, 2015
2 parents 966659e + 3f754c8 commit 1ac82ff
Show file tree
Hide file tree
Showing 3 changed files with 368 additions and 6 deletions.
214 changes: 214 additions & 0 deletions bravado/bitjws_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
"""
The :class:`SwaggerClient` provides an interface for making API calls based on
a swagger spec, and returns responses of python objects which build from the
API response.
Structure Diagram::
+---------------------+
| |
| SwaggerClient |
| |
+------+--------------+
|
| has many
|
+------v--------------+
| |
| Resource +------------------+
| | |
+------+--------------+ has many |
| |
| has many |
| |
+------v--------------+ +------v--------------+
| | | |
| Operation | | SwaggerModel |
| | | |
+------+--------------+ +---------------------+
|
| uses
|
+------v--------------+
| |
| HttpClient |
| |
+---------------------+
To get a client
.. code-block:: python
client = bravado.client.SwaggerClient.from_url(swagger_spec_url)
"""
import functools
import logging
import sys

from bravado_core.docstring import create_operation_docstring
from bravado_core.exception import MatchingResponseNotFound
from bravado_core.exception import SwaggerMappingError
from bravado_core.formatter import SwaggerFormat # noqa
from bravado_core.param import marshal_param
from bravado_core.response import unmarshal_response
from bravado_core.spec import Spec
import six
from six import iteritems, itervalues

from bravado.docstring_property import docstring_property
from bravado.exception import HTTPError
from bravado.requests_client import RequestsClient
from bravado.swagger_model import Loader
from bravado.warning import warn_for_deprecated_op

from bravado.client import *

import bitjws

log = logging.getLogger(__name__)


class BitJWSResourceDecorator(object):
"""
Wraps :class:`bravado_core.resource.Resource` so that accesses to contained
operations can be instrumented.
"""

def __init__(self, resource):
"""
:type resource: :class:`bravado_core.resource.Resource`
"""
self.resource = resource

def __getattr__(self, name):
"""
:rtype: :class:`CallableOperation`
"""
return BitJWSCallableOperation(getattr(self.resource, name))

def __dir__(self):
"""
Exposes correct attrs on resource when tab completing in a REPL
"""
return self.resource.__dir__()


class BitJWSCallableOperation(object):
"""
Wraps an operation to make it callable and provide a docstring. Calling
the operation uses the configured http_client.
"""
def __init__(self, operation):
"""
:type operation: :class:`bravado_core.operation.Operation`
"""
self.operation = operation

@docstring_property(__doc__)
def __doc__(self):
return create_operation_docstring(self.operation)

def __getattr__(self, name):
"""
Forward requests for attrs not found on this decorator to the delegate.
"""
return getattr(self.operation, name)

def construct_request(self, **op_kwargs):
"""
:param op_kwargs: parameter name/value pairs to passed to the
invocation of the operation.
:return: request in dict form
"""
request_options = op_kwargs.pop('_request_options', {})
url = self.operation.swagger_spec.api_url.rstrip('/') + self.path_name
request = {
'method': self.operation.http_method.upper(),
'url': url,
'params': {}, # filled in downstream
'headers': request_options.get('headers', {}),
}

# Copy over optional request options
for request_option in ('connect_timeout', 'timeout'):
if request_option in request_options:
request[request_option] = request_options[request_option]

self.construct_params(request, op_kwargs)
return request

def construct_params(self, request, op_kwargs):
"""
Given the parameters passed to the operation invocation, validates and
marshals the parameters into the provided request dict.
:type request: dict
:param op_kwargs: the kwargs passed to the operation invocation
:raises: SwaggerMappingError on extra parameters or when a required
parameter is not supplied.
"""
current_params = self.operation.params.copy()
for param_name, param_value in iteritems(op_kwargs):
param = current_params.pop(param_name, None)
if param is None:
raise SwaggerMappingError(
"{0} does not have parameter {1}"
.format(self.operation.operation_id, param_name))
marshal_param(param, param_value, request)

# Check required params and non-required params with a 'default' value
for remaining_param in itervalues(current_params):
if remaining_param.required:
raise SwaggerMappingError(
'{0} is a required parameter'.format(remaining_param.name))
if not remaining_param.required and remaining_param.has_default():
marshal_param(remaining_param, None, request)

def __call__(self, **op_kwargs):
"""
Invoke the actual HTTP request and return a future that encapsulates
the HTTP response.
:rtype: :class:`bravado.http_future.HTTPFuture`
"""
log.debug(u"%s(%s)" % (self.operation.operation_id, op_kwargs))
warn_for_deprecated_op(self.operation)
request_params = self.construct_request(**op_kwargs)
callback = functools.partial(bitjws_response_callback, operation=self)
return self.operation.swagger_spec.http_client.request(request_params,
callback)


def bitjws_response_callback(incoming_response, operation):
"""
So the http_client is finished with its part of processing the response.
This hands the response over to bravado_core for validation and
unmarshalling.
:type incoming_response: :class:`bravado_core.response.IncomingResponse`
:type operation: :class:`bravado_core.operation.Operation`
:return: Response spec's return value.
:raises: HTTPError
- On 5XX status code, the HTTPError has minimal information.
- On non-2XX status code with no matching response, the HTTPError
contains a detailed error message.
- On non-2XX status code with a matching response, the HTTPError
contains the return value.
"""
raise_on_unexpected(incoming_response)

print incoming_response.text
print incoming_response._delegate.content.decode('utf8')
try:
swagger_return_value = unmarshal_response(incoming_response, operation)
except MatchingResponseNotFound as e:
six.reraise(
HTTPError,
HTTPError(response=incoming_response, message=str(e)),
sys.exc_info()[2])

raise_on_expected(incoming_response, swagger_return_value)
return swagger_return_value

140 changes: 140 additions & 0 deletions bravado/bitjws_requests_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
import logging
import json
import bitjws
import requests
import requests.auth
from bravado.requests_client import *

from bravado.http_client import HttpClient
from bravado.http_future import HttpFuture

log = logging.getLogger(__name__)


class BitJWSAuthenticator(Authenticator):
"""BitJWS authenticator uses JWS and the CUSTOM-BITCOIN-SIGN algorithm.
:param host: Host to authenticate for.
:param privkey: Private key as a WIF string
"""

def __init__(self, host, privkey):
super(BitJWSAuthenticator, self).__init__(host)
self.privkey = bitjws.PrivateKey(bitjws.wif_to_privkey(privkey))

def apply(self, request):
if len(request.data) > 0:
data = bitjws.sign_serialize(self.privkey, **json.loads(request.data))
else:
data = bitjws.sign_serialize(self.privkey, **request.params)
request.params = {}
request.data = data
return request


class BitJWSRequestsClient(HttpClient):
"""Synchronous HTTP client implementation.
"""

def __init__(self):
self.session = requests.Session()
self.authenticator = None

@staticmethod
def separate_params(request_params):
"""Splits the passed in dict of request_params into two buckets.
- sanitized_params are valid kwargs for constructing a
requests.Request(..)
- misc_options are things like timeouts which can't be communicated
to the Requests library via the requests.Request(...) constructor.
:param request_params: kitchen sink of request params. Treated as a
read-only dict.
:returns: tuple(sanitized_params, misc_options)
"""
sanitized_params = request_params.copy()
misc_options = {}

if 'connect_timeout' in sanitized_params:
misc_options['connect_timeout'] = \
sanitized_params.pop('connect_timeout')

if 'timeout' in sanitized_params:
misc_options['timeout'] = sanitized_params.pop('timeout')

return sanitized_params, misc_options

def request(self, request_params, response_callback=None):
"""
:param request_params: complete request data.
:type request_params: dict
:param response_callback: Function to be called on the response
:returns: HTTP Future object
:rtype: :class: `bravado_core.http_future.HttpFuture`
"""
sanitized_params, misc_options = self.separate_params(request_params)
requests_future = RequestsFutureAdapter(
self.session,
self.authenticated_request(sanitized_params),
misc_options)

return HttpFuture(
requests_future,
BitJWSRequestsResponseAdapter,
response_callback,
)

def set_basic_auth(self, host, username, password):
self.authenticator = BasicAuthenticator(
host=host, username=username, password=password)

def set_api_key(self, host, api_key, param_name=u'api_key'):
self.authenticator = ApiKeyAuthenticator(
host=host, api_key=api_key, param_name=param_name)

def set_bitjws_key(self, host, privkey):
self.authenticator = BitJWSAuthenticator(host=host, privkey=privkey)

def authenticated_request(self, request_params):
return self.apply_authentication(requests.Request(**request_params))

def apply_authentication(self, request):
if self.authenticator and self.authenticator.matches(request.url):
return self.authenticator.apply(request)
return request


class BitJWSRequestsResponseAdapter(IncomingResponse):
"""Wraps a requests.models.Response object to provide a uniform interface
to the response innards.
"""

def __init__(self, requests_lib_response):
"""
:type requests_lib_response: :class:`requests.models.Response`
"""
self._delegate = requests_lib_response

@property
def status_code(self):
return self._delegate.status_code

@property
def text(self):
return self._delegate.text

@property
def reason(self):
return self._delegate.reason

def json(self, **kwargs):
if 'content-type' in self._delegate.headers and \
'json' in self._delegate.headers['content-type']:
jso = self._delegate.json(**kwargs)
else:
rawtext = self.text.decode('utf8')
headers, jso = bitjws.validate_deserialize(rawtext)
return jso

0 comments on commit 1ac82ff

Please sign in to comment.