-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
368 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
Oops, something went wrong.