66from abc import ABCMeta , abstractmethod as abstract_method
77from inspect import isabstract as is_abstract
88from socket import getdefaulttimeout as get_default_timeout
9- from typing import Dict , Text , Tuple , Union
9+ from typing import Dict , List , Text , Tuple , Union
1010
1111import requests
1212from iota import DEFAULT_PORT
1313from iota .exceptions import with_context
1414from iota .json import JsonEncoder
15- from six import with_metaclass
15+ from six import PY2 , binary_type , with_metaclass
1616
1717__all__ = [
1818 'AdapterSpec' ,
1919 'BadApiResponse' ,
2020 'InvalidUri' ,
2121]
2222
23+ if PY2 :
24+ # Fix an error when importing this package using the ``imp`` library
25+ # (note: ``imp`` is deprecated since Python 3.4 in favor of
26+ # ``importlib``).
27+ # https://docs.python.org/3/library/imp.html
28+ # https://travis-ci.org/iotaledger/iota.lib.py/jobs/191974244
29+ __all__ = map (binary_type , __all__ )
30+
2331
2432# Custom types for type hints and docstrings.
2533AdapterSpec = Union [Text , 'BaseAdapter' ]
@@ -81,7 +89,7 @@ def resolve_adapter(uri):
8189
8290class _AdapterMeta (ABCMeta ):
8391 """
84- Automatically registers new adapter classes in `adapter_registry`.
92+ Automatically registers new adapter classes in `` adapter_registry` `.
8593 """
8694 # noinspection PyShadowingBuiltins
8795 def __init__ (cls , what , bases = None , dict = None ):
@@ -93,7 +101,9 @@ def __init__(cls, what, bases=None, dict=None):
93101
94102 def configure (cls , uri ):
95103 # type: (Text) -> BaseAdapter
96- """Creates a new adapter from the specified URI."""
104+ """
105+ Creates a new adapter from the specified URI.
106+ """
97107 return cls (uri )
98108
99109
@@ -102,24 +112,31 @@ class BaseAdapter(with_metaclass(_AdapterMeta)):
102112 Interface for IOTA API adapters.
103113
104114 Adapters make it easy to customize the way an StrictIota instance
105- communicates with a node.
115+ communicates with a node.
106116 """
107117 supported_protocols = () # type: Tuple[Text]
108118 """
109- Protocols that `resolve_adapter` can use to identify this adapter
110- type.
119+ Protocols that `` resolve_adapter` ` can use to identify this adapter
120+ type.
111121 """
112122 @abstract_method
113123 def send_request (self , payload , ** kwargs ):
114124 # type: (dict, dict) -> dict
115125 """
116126 Sends an API request to the node.
117127
118- :param payload: JSON payload.
119- :param kwargs: Additional keyword arguments for the adapter.
128+ :param payload:
129+ JSON payload.
130+
131+ :param kwargs:
132+ Additional keyword arguments for the adapter.
120133
121- :return: Decoded response from the node.
122- :raise: BadApiResponse if a non-success response was received.
134+ :return:
135+ Decoded response from the node.
136+
137+ :raise:
138+ - :py:class:`BadApiResponse` if a non-success response was
139+ received.
123140 """
124141 raise NotImplementedError (
125142 'Not implemented in {cls}.' .format (cls = type (self ).__name__ ),
@@ -138,7 +155,8 @@ def configure(cls, uri):
138155 """
139156 Creates a new instance using the specified URI.
140157
141- :param uri: E.g., `udp://localhost:14265/`
158+ :param uri:
159+ E.g., `udp://localhost:14265/`
142160 """
143161 try :
144162 protocol , config = uri .split ('://' , 1 )
@@ -197,7 +215,9 @@ def __init__(self, host, port=DEFAULT_PORT, path='/'):
197215 @property
198216 def node_url (self ):
199217 # type: () -> Text
200- """Returns the node URL."""
218+ """
219+ Returns the node URL.
220+ """
201221 return 'http://{host}:{port}{path}' .format (
202222 host = self .host ,
203223 port = self .port ,
@@ -241,8 +261,8 @@ def send_request(self, payload, **kwargs):
241261 try :
242262 # Response always has 200 status, even for errors/exceptions, so the
243263 # only way to check for success is to inspect the response body.
244- # :see:` https://github.com/iotaledger/iri/issues/9`
245- # :see:` https://github.com/iotaledger/iri/issues/12`
264+ # https://github.com/iotaledger/iri/issues/9
265+ # https://github.com/iotaledger/iri/issues/12
246266 error = decoded .get ('exception' ) or decoded .get ('error' )
247267 except AttributeError :
248268 raise with_context (
@@ -268,7 +288,98 @@ def _send_http_request(self, payload, **kwargs):
268288 Sends the actual HTTP request.
269289
270290 Split into its own method so that it can be mocked during unit
271- tests.
291+ tests.
272292 """
273293 kwargs .setdefault ('timeout' , get_default_timeout ())
274294 return requests .post (self .node_url , data = payload , ** kwargs )
295+
296+
297+ class MockAdapter (BaseAdapter ):
298+ """
299+ An mock adapter used for simulating API responses.
300+
301+ To use this adapter, you must first "seed" the responses that the
302+ adapter should return for each request. The adapter will then return
303+ the appropriate seeded response each time it "sends" a request.
304+ """
305+ supported_protocols = ('mock' ,)
306+
307+ # noinspection PyUnusedLocal
308+ @classmethod
309+ def configure (cls , uri ):
310+ return cls ()
311+
312+ def __init__ (self ):
313+ super (MockAdapter , self ).__init__ ()
314+
315+ self .responses = {} # type: Dict[Text, List[dict]]
316+ self .requests = [] # type: List[dict]
317+
318+ def seed_response (self , command , response ):
319+ # type: (Text, dict) -> MockAdapter
320+ """
321+ Sets the response that the adapter will return for the specified
322+ command.
323+
324+ You can seed multiple responses per command; the adapter will put
325+ them into a FIFO queue. When a request comes in, the adapter will
326+ pop the corresponding response off of the queue.
327+
328+ Example::
329+
330+ adapter.seed_response('sayHello', {'message': 'Hi!'})
331+ adapter.seed_response('sayHello', {'message': 'Hello!'})
332+
333+ adapter.send_request({'command': 'sayHello'})
334+ # {'message': 'Hi!'}
335+
336+ adapter.send_request({'command': 'sayHello'})
337+ # {'message': 'Hello!'}
338+ """
339+ if command not in self .responses :
340+ self .responses [command ] = []
341+
342+ self .responses [command ].append (response )
343+ return self
344+
345+ def send_request (self , payload , ** kwargs ):
346+ # type: (dict, dict) -> dict
347+ # Store a snapshot so that we can inspect the request later.
348+ self .requests .append (dict (payload ))
349+
350+ command = payload ['command' ]
351+
352+ try :
353+ response = self .responses [command ].pop (0 )
354+ except KeyError :
355+ raise with_context (
356+ exc = BadApiResponse (
357+ 'No seeded response for {command!r} '
358+ '(expected one of: {seeds!r}).' .format (
359+ command = command ,
360+ seeds = list (sorted (self .responses .keys ())),
361+ ),
362+ ),
363+
364+ context = {
365+ 'request' : payload ,
366+ },
367+ )
368+ except IndexError :
369+ raise with_context (
370+ exc = BadApiResponse (
371+ '{command} called too many times; no seeded responses left.' .format (
372+ command = command ,
373+ ),
374+ ),
375+
376+ context = {
377+ 'request' : payload ,
378+ },
379+ )
380+
381+ error = response .get ('exception' ) or response .get ('error' )
382+ if error :
383+ raise with_context (BadApiResponse (error ), context = {'request' : payload })
384+
385+ return response
0 commit comments