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

Commit

Permalink
Merge pull request #14 from iotaledger/develop
Browse files Browse the repository at this point in the history
1.0.0b6
  • Loading branch information
todofixthis authored Jan 19, 2017
2 parents 332412d + 2d21f9a commit 2f2fc47
Show file tree
Hide file tree
Showing 42 changed files with 1,664 additions and 633 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
name = 'PyOTA',
description = 'IOTA API library for Python',
url = 'https://github.com/iotaledger/iota.lib.py',
version = '1.0.0b5',
version = '1.0.0b6',

packages = find_packages('src'),
include_package_data = True,
Expand Down
143 changes: 127 additions & 16 deletions src/iota/adapter.py → src/iota/adapter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,28 @@
from abc import ABCMeta, abstractmethod as abstract_method
from inspect import isabstract as is_abstract
from socket import getdefaulttimeout as get_default_timeout
from typing import Dict, Text, Tuple, Union
from typing import Dict, List, Text, Tuple, Union

import requests
from iota import DEFAULT_PORT
from iota.exceptions import with_context
from iota.json import JsonEncoder
from six import with_metaclass
from six import PY2, binary_type, with_metaclass

__all__ = [
'AdapterSpec',
'BadApiResponse',
'InvalidUri',
]

if PY2:
# Fix an error when importing this package using the ``imp`` library
# (note: ``imp`` is deprecated since Python 3.4 in favor of
# ``importlib``).
# https://docs.python.org/3/library/imp.html
# https://travis-ci.org/iotaledger/iota.lib.py/jobs/191974244
__all__ = map(binary_type, __all__)


# Custom types for type hints and docstrings.
AdapterSpec = Union[Text, 'BaseAdapter']
Expand Down Expand Up @@ -81,7 +89,7 @@ def resolve_adapter(uri):

class _AdapterMeta(ABCMeta):
"""
Automatically registers new adapter classes in `adapter_registry`.
Automatically registers new adapter classes in ``adapter_registry``.
"""
# noinspection PyShadowingBuiltins
def __init__(cls, what, bases=None, dict=None):
Expand All @@ -93,7 +101,9 @@ def __init__(cls, what, bases=None, dict=None):

def configure(cls, uri):
# type: (Text) -> BaseAdapter
"""Creates a new adapter from the specified URI."""
"""
Creates a new adapter from the specified URI.
"""
return cls(uri)


Expand All @@ -102,24 +112,31 @@ class BaseAdapter(with_metaclass(_AdapterMeta)):
Interface for IOTA API adapters.
Adapters make it easy to customize the way an StrictIota instance
communicates with a node.
communicates with a node.
"""
supported_protocols = () # type: Tuple[Text]
"""
Protocols that `resolve_adapter` can use to identify this adapter
type.
Protocols that ``resolve_adapter`` can use to identify this adapter
type.
"""
@abstract_method
def send_request(self, payload, **kwargs):
# type: (dict, dict) -> dict
"""
Sends an API request to the node.
:param payload: JSON payload.
:param kwargs: Additional keyword arguments for the adapter.
:param payload:
JSON payload.
:param kwargs:
Additional keyword arguments for the adapter.
:return: Decoded response from the node.
:raise: BadApiResponse if a non-success response was received.
:return:
Decoded response from the node.
:raise:
- :py:class:`BadApiResponse` if a non-success response was
received.
"""
raise NotImplementedError(
'Not implemented in {cls}.'.format(cls=type(self).__name__),
Expand All @@ -138,7 +155,8 @@ def configure(cls, uri):
"""
Creates a new instance using the specified URI.
:param uri: E.g., `udp://localhost:14265/`
:param uri:
E.g., `udp://localhost:14265/`
"""
try:
protocol, config = uri.split('://', 1)
Expand Down Expand Up @@ -197,7 +215,9 @@ def __init__(self, host, port=DEFAULT_PORT, path='/'):
@property
def node_url(self):
# type: () -> Text
"""Returns the node URL."""
"""
Returns the node URL.
"""
return 'http://{host}:{port}{path}'.format(
host = self.host,
port = self.port,
Expand Down Expand Up @@ -241,8 +261,8 @@ def send_request(self, payload, **kwargs):
try:
# Response always has 200 status, even for errors/exceptions, so the
# only way to check for success is to inspect the response body.
# :see:`https://github.com/iotaledger/iri/issues/9`
# :see:`https://github.com/iotaledger/iri/issues/12`
# https://github.com/iotaledger/iri/issues/9
# https://github.com/iotaledger/iri/issues/12
error = decoded.get('exception') or decoded.get('error')
except AttributeError:
raise with_context(
Expand All @@ -268,7 +288,98 @@ def _send_http_request(self, payload, **kwargs):
Sends the actual HTTP request.
Split into its own method so that it can be mocked during unit
tests.
tests.
"""
kwargs.setdefault('timeout', get_default_timeout())
return requests.post(self.node_url, data=payload, **kwargs)


class MockAdapter(BaseAdapter):
"""
An mock adapter used for simulating API responses.
To use this adapter, you must first "seed" the responses that the
adapter should return for each request. The adapter will then return
the appropriate seeded response each time it "sends" a request.
"""
supported_protocols = ('mock',)

# noinspection PyUnusedLocal
@classmethod
def configure(cls, uri):
return cls()

def __init__(self):
super(MockAdapter, self).__init__()

self.responses = {} # type: Dict[Text, List[dict]]
self.requests = [] # type: List[dict]

def seed_response(self, command, response):
# type: (Text, dict) -> MockAdapter
"""
Sets the response that the adapter will return for the specified
command.
You can seed multiple responses per command; the adapter will put
them into a FIFO queue. When a request comes in, the adapter will
pop the corresponding response off of the queue.
Example::
adapter.seed_response('sayHello', {'message': 'Hi!'})
adapter.seed_response('sayHello', {'message': 'Hello!'})
adapter.send_request({'command': 'sayHello'})
# {'message': 'Hi!'}
adapter.send_request({'command': 'sayHello'})
# {'message': 'Hello!'}
"""
if command not in self.responses:
self.responses[command] = []

self.responses[command].append(response)
return self

def send_request(self, payload, **kwargs):
# type: (dict, dict) -> dict
# Store a snapshot so that we can inspect the request later.
self.requests.append(dict(payload))

command = payload['command']

try:
response = self.responses[command].pop(0)
except KeyError:
raise with_context(
exc = BadApiResponse(
'No seeded response for {command!r} '
'(expected one of: {seeds!r}).'.format(
command = command,
seeds = list(sorted(self.responses.keys())),
),
),

context = {
'request': payload,
},
)
except IndexError:
raise with_context(
exc = BadApiResponse(
'{command} called too many times; no seeded responses left.'.format(
command = command,
),
),

context = {
'request': payload,
},
)

error = response.get('exception') or response.get('error')
if error:
raise with_context(BadApiResponse(error), context={'request': payload})

return response
70 changes: 70 additions & 0 deletions src/iota/adapter/wrappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# coding=utf-8
from __future__ import absolute_import, division, print_function, \
unicode_literals

from abc import ABCMeta, abstractmethod as abstract_method
from logging import INFO, Logger

from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter
from six import with_metaclass

__all__ = [
'LogWrapper',
]


class BaseWrapper(with_metaclass(ABCMeta)):
"""
Base functionality for "adapter wrappers", used to extend the
functionality of IOTA adapters.
"""
def __init__(self, adapter):
# type: (AdapterSpec) -> None
super(BaseWrapper, self).__init__()

if not isinstance(adapter, BaseAdapter):
adapter = resolve_adapter(adapter)

self.adapter = adapter # type: BaseAdapter

@abstract_method
def send_request(self, payload, **kwargs):
# type: (dict, dict) -> dict
raise NotImplementedError(
'Not implemented in {cls}.'.format(cls=type(self).__name__),
)


class LogWrapper(BaseWrapper):
"""
Wrapper that sends all adapter requests and responses to a logger.
To use it, "wrap" the real adapter instance/spec::
logger = getLogger('...')
api = Iota(LogWrapper('http://localhost:14265', logger))
"""
def __init__(self, adapter, logger, level=INFO):
# type: (AdapterSpec, Logger, int) -> None
super(LogWrapper, self).__init__(adapter)

self.logger = logger
self.level = level

def send_request(self, payload, **kwargs):
# type: (dict, dict) -> dict
command = payload.get('command') or 'command'

self.logger.log(self.level, 'Sending {command}: {request!r}'.format(
command = command,
request = payload,
))

response = self.adapter.send_request(payload, **kwargs)

self.logger.log(self.level, 'Receiving {command}: {response!r}'.format(
command = command,
response = response,
))

return response
12 changes: 6 additions & 6 deletions src/iota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,22 +489,22 @@ def get_new_addresses(self, index=0, count=1):
"""
return self.getNewAddresses(seed=self.seed, index=index, count=count)

def get_transfers(self, start=0, end=None, inclusion_states=False):
def get_transfers(self, start=0, stop=None, inclusion_states=False):
# type: (int, Optional[int], bool) -> dict
"""
Returns all transfers associated with the seed.
:param start:
Starting key index.
:param end:
:param stop:
Stop before this index.
Note that this parameter behaves like the ``stop`` attribute in a
:py:class:`slice` object; the end index is _not_ included in the
:py:class:`slice` object; the stop index is _not_ included in the
result.
If not specified, then this method will not stop until it finds
an unused address.
If ``None`` (default), then this method will check every address
until it finds one without any transfers.
:param inclusion_states:
Whether to also fetch the inclusion states of the transfers.
Expand All @@ -526,7 +526,7 @@ def get_transfers(self, start=0, end=None, inclusion_states=False):
return self.getTransfers(
seed = self.seed,
start = start,
end = end,
stop = stop,
inclusion_states = inclusion_states,
)

Expand Down
7 changes: 5 additions & 2 deletions src/iota/commands/core/attach_to_tangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
unicode_literals

import filters as f
from iota import TransactionHash
from iota import TransactionHash, TransactionTrytes
from iota.commands import FilterCommand, RequestFilter, ResponseFilter
from iota.filters import Trytes

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

'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes),
'trytes':
f.Required
| f.Array
| f.FilterRepeater(f.Required | Trytes(result_type=TransactionTrytes)),

# Loosely-validated; testnet nodes require a different value than
# mainnet.
Expand Down
7 changes: 5 additions & 2 deletions src/iota/commands/core/broadcast_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
unicode_literals

import filters as f

from iota import TransactionTrytes
from iota.commands import FilterCommand, RequestFilter, ResponseFilter
from iota.filters import Trytes

Expand All @@ -30,7 +30,10 @@ def get_response_filter(self):
class BroadcastTransactionsRequestFilter(RequestFilter):
def __init__(self):
super(BroadcastTransactionsRequestFilter, self).__init__({
'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes),
'trytes':
f.Required
| f.Array
| f.FilterRepeater(f.Required | Trytes(result_type=TransactionTrytes)),
})


Expand Down
Loading

0 comments on commit 2f2fc47

Please sign in to comment.