Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.

Commit 2f2fc47

Browse files
authored
Merge pull request #14 from iotaledger/develop
1.0.0b6
2 parents 332412d + 2d21f9a commit 2f2fc47

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1664
-633
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
name = 'PyOTA',
2929
description = 'IOTA API library for Python',
3030
url = 'https://github.com/iotaledger/iota.lib.py',
31-
version = '1.0.0b5',
31+
version = '1.0.0b6',
3232

3333
packages = find_packages('src'),
3434
include_package_data = True,

src/iota/adapter.py renamed to src/iota/adapter/__init__.py

Lines changed: 127 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,28 @@
66
from abc import ABCMeta, abstractmethod as abstract_method
77
from inspect import isabstract as is_abstract
88
from 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

1111
import requests
1212
from iota import DEFAULT_PORT
1313
from iota.exceptions import with_context
1414
from 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.
2533
AdapterSpec = Union[Text, 'BaseAdapter']
@@ -81,7 +89,7 @@ def resolve_adapter(uri):
8189

8290
class _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

src/iota/adapter/wrappers.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# coding=utf-8
2+
from __future__ import absolute_import, division, print_function, \
3+
unicode_literals
4+
5+
from abc import ABCMeta, abstractmethod as abstract_method
6+
from logging import INFO, Logger
7+
8+
from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter
9+
from six import with_metaclass
10+
11+
__all__ = [
12+
'LogWrapper',
13+
]
14+
15+
16+
class BaseWrapper(with_metaclass(ABCMeta)):
17+
"""
18+
Base functionality for "adapter wrappers", used to extend the
19+
functionality of IOTA adapters.
20+
"""
21+
def __init__(self, adapter):
22+
# type: (AdapterSpec) -> None
23+
super(BaseWrapper, self).__init__()
24+
25+
if not isinstance(adapter, BaseAdapter):
26+
adapter = resolve_adapter(adapter)
27+
28+
self.adapter = adapter # type: BaseAdapter
29+
30+
@abstract_method
31+
def send_request(self, payload, **kwargs):
32+
# type: (dict, dict) -> dict
33+
raise NotImplementedError(
34+
'Not implemented in {cls}.'.format(cls=type(self).__name__),
35+
)
36+
37+
38+
class LogWrapper(BaseWrapper):
39+
"""
40+
Wrapper that sends all adapter requests and responses to a logger.
41+
42+
To use it, "wrap" the real adapter instance/spec::
43+
44+
logger = getLogger('...')
45+
api = Iota(LogWrapper('http://localhost:14265', logger))
46+
"""
47+
def __init__(self, adapter, logger, level=INFO):
48+
# type: (AdapterSpec, Logger, int) -> None
49+
super(LogWrapper, self).__init__(adapter)
50+
51+
self.logger = logger
52+
self.level = level
53+
54+
def send_request(self, payload, **kwargs):
55+
# type: (dict, dict) -> dict
56+
command = payload.get('command') or 'command'
57+
58+
self.logger.log(self.level, 'Sending {command}: {request!r}'.format(
59+
command = command,
60+
request = payload,
61+
))
62+
63+
response = self.adapter.send_request(payload, **kwargs)
64+
65+
self.logger.log(self.level, 'Receiving {command}: {response!r}'.format(
66+
command = command,
67+
response = response,
68+
))
69+
70+
return response

src/iota/api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -489,22 +489,22 @@ def get_new_addresses(self, index=0, count=1):
489489
"""
490490
return self.getNewAddresses(seed=self.seed, index=index, count=count)
491491

492-
def get_transfers(self, start=0, end=None, inclusion_states=False):
492+
def get_transfers(self, start=0, stop=None, inclusion_states=False):
493493
# type: (int, Optional[int], bool) -> dict
494494
"""
495495
Returns all transfers associated with the seed.
496496
497497
:param start:
498498
Starting key index.
499499
500-
:param end:
500+
:param stop:
501501
Stop before this index.
502502
Note that this parameter behaves like the ``stop`` attribute in a
503-
:py:class:`slice` object; the end index is _not_ included in the
503+
:py:class:`slice` object; the stop index is _not_ included in the
504504
result.
505505
506-
If not specified, then this method will not stop until it finds
507-
an unused address.
506+
If ``None`` (default), then this method will check every address
507+
until it finds one without any transfers.
508508
509509
:param inclusion_states:
510510
Whether to also fetch the inclusion states of the transfers.
@@ -526,7 +526,7 @@ def get_transfers(self, start=0, end=None, inclusion_states=False):
526526
return self.getTransfers(
527527
seed = self.seed,
528528
start = start,
529-
end = end,
529+
stop = stop,
530530
inclusion_states = inclusion_states,
531531
)
532532

src/iota/commands/core/attach_to_tangle.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
unicode_literals
44

55
import filters as f
6-
from iota import TransactionHash
6+
from iota import TransactionHash, TransactionTrytes
77
from iota.commands import FilterCommand, RequestFilter, ResponseFilter
88
from iota.filters import Trytes
99

@@ -33,7 +33,10 @@ def __init__(self):
3333
'branch_transaction': f.Required | Trytes(result_type=TransactionHash),
3434
'trunk_transaction': f.Required | Trytes(result_type=TransactionHash),
3535

36-
'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes),
36+
'trytes':
37+
f.Required
38+
| f.Array
39+
| f.FilterRepeater(f.Required | Trytes(result_type=TransactionTrytes)),
3740

3841
# Loosely-validated; testnet nodes require a different value than
3942
# mainnet.

src/iota/commands/core/broadcast_transactions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
unicode_literals
44

55
import filters as f
6-
6+
from iota import TransactionTrytes
77
from iota.commands import FilterCommand, RequestFilter, ResponseFilter
88
from iota.filters import Trytes
99

@@ -30,7 +30,10 @@ def get_response_filter(self):
3030
class BroadcastTransactionsRequestFilter(RequestFilter):
3131
def __init__(self):
3232
super(BroadcastTransactionsRequestFilter, self).__init__({
33-
'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes),
33+
'trytes':
34+
f.Required
35+
| f.Array
36+
| f.FilterRepeater(f.Required | Trytes(result_type=TransactionTrytes)),
3437
})
3538

3639

0 commit comments

Comments
 (0)