Adapters and Wrappers

The Iota class defines the API methods that are available for interacting with the node, but it delegates the actual interaction to another set of classes: Adapters and Wrappers.

The API instance’s methods contain the logic and handle PyOTA-specific types, construct and translate objects, while the API instance’s adapter deals with the networking, communicating with a node.

You can choose and configure the available adapters to be used with the API:

  • HttpAdapter,

  • MockAdapter.

AdapterSpec

In a few places in the PyOTA codebase, you may see references to a meta-type called AdapterSpec.

iota.adapter.AdapterSpec = typing.Union[str, ForwardRef('BaseAdapter')]

Placeholder that means “URI or adapter instance”.

Will be resolved to a correctly-configured adapter instance upon API instance creation.

For example, when creating an Iota object, the first argument of Iota.__init__() is an AdapterSpec. This means that you can initialize an Iota object using either a node URI, or an adapter instance:

  • Node URI:

    api = Iota('http://localhost:14265')
    
  • Adapter instance:

    api = Iota(HttpAdapter('http://localhost:14265'))
    

Adapters

Adapters are responsible for sending requests to the node and returning the response.

PyOTA ships with a few adapters:

HttpAdapter

from iota import Iota, HttpAdapter

# Use HTTP:
api = Iota('http://localhost:14265')
api = Iota(HttpAdapter('http://localhost:14265'))

# Use HTTPS:
api = Iota('https://nodes.thetangle.org:443')
api = Iota(HttpAdapter('https://nodes.thetangle.org:443'))

# Use HTTPS with basic authentication and 60 seconds timeout:
api = Iota(
    HttpAdapter(
        'https://nodes.thetangle.org:443',
        authentication=('myusername', 'mypassword'),
        timeout=60))
class iota.HttpAdapter(uri: Union[str, urllib.parse.SplitResult], timeout: Optional[int] = None, authentication: Optional[Tuple[str, str]] = None)

Sends standard HTTP(S) requests to the node.

Parameters
  • uri (AdapterSpec) –

    URI or adapter instance.

    If uri is a str, it is parsed to extract scheme, hostname and port.

  • timeout (Optional[int]) – Connection timeout in seconds.

  • authentication (Optional[Tuple(str,str)]) – Credetentials for basic authentication with the node.

Returns

HttpAdapter object.

Raises

InvalidUri

  • if protocol is unsupported.

  • if hostname is empty.

  • if non-numeric port is supplied.

To configure an Iota instance to use HttpAdapter, specify an http:// or https:// URI, or provide an HttpAdapter instance.

The HttpAdapter raises a BadApiResponse exception if the server sends back an error response (due to invalid request parameters, for example).

Debugging HTTP Requests

To see all HTTP requests and responses as they happen, attach a logging.Logger instance to the adapter via its set_logger method.

Any time the HttpAdapter sends a request or receives a response, it will first generate a log message. Note: if the response is an error response (e.g., due to invalid request parameters), the HttpAdapter will log the request before raising BadApiResponse.

Note

HttpAdapter generates log messages with DEBUG level, so make sure that your logger’s level attribute is set low enough that it doesn’t filter these messages!

Logging to console with default format

from logging import getLogger, basicConfig, DEBUG
from iota import Iota

api = Iota("https://nodes.thetangle.org:443")

# Sets the logging level for the root logger (and for its handlers)
basicConfig(level=DEBUG)

# Get a new logger derived from the root logger
logger = getLogger(__name__)

# Attach the logger to the adapter
api.adapter.set_logger(logger)

# Execute a command that sends request to the node
api.get_node_info()

# Log messages should be printed to console

Logging to a file with custom format

from logging import getLogger, DEBUG, FileHandler, Formatter
from iota import Iota

# Create a custom logger
logger = getLogger(__name__)

# Set logging level to DEBUG
logger.setLevel(DEBUG)

# Create handler to write to a log file
f_handler = FileHandler(filename='pyota.log',mode='a')
f_handler.setLevel(DEBUG)

# Create formatter and add it to handler
f_format = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
f_handler.setFormatter(f_format)

# Add handler to the logger
logger.addHandler(f_handler)

# Create API instance
api = Iota("https://nodes.thetangle.org:443")

# Add logger to the adapter of the API instance
api.adapter.set_logger(logger)

# Sends a request to the node
api.get_node_info()

# Open 'pyota.log' file and observe the logs

Logging to console with custom format

from logging import getLogger, DEBUG, StreamHandler, Formatter
from iota import Iota

# Create a custom logger
logger = getLogger(__name__)

# Set logging level to DEBUG
logger.setLevel(DEBUG)

# Create handler to write to sys.stderr
s_handler = StreamHandler()
s_handler.setLevel(DEBUG)

# Create formatter and add it to handler
s_format = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
s_handler.setFormatter(s_format)

# Add handler to the logger
logger.addHandler(s_handler)

# Create API instance
api = Iota("https://nodes.thetangle.org:443")

# Add logger to the adapter of the API instance
api.adapter.set_logger(logger)

# Sends a request to the node
api.get_node_info()

# Observe log messages in console

MockAdapter

from iota import Iota, MockAdapter

# Inject a mock adapter.
api = Iota('mock://')
api = Iota(MockAdapter())

# Seed responses from the node.
api.adapter.seed_response('getNodeInfo', {'message': 'Hello, world!'})
api.adapter.seed_response('getNodeInfo', {'message': 'Hello, IOTA!'})

# Invoke API commands, using the adapter.
print(api.get_node_info()) # {'message': 'Hello, world!'}
print(api.get_node_info()) # {'message': 'Hello, IOTA!'}
print(api.get_node_info()) # raises BadApiResponse exception
class iota.MockAdapter

A mock adapter used for simulating API responses without actually sending any requests to the node.

This is particularly useful in unit and functional tests where you want to verify that your code works correctly in specific scenarios, without having to engineer your own subtangle.

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.

Parameters

None – To construct a MockAdapter, you don’t need to supply any arguments.

Returns

MockAdapter object.

To configure an Iota instance to use MockAdapter, specify mock:// as the node URI, or provide a MockAdapter instance.

Example usage:

from iota import Iota, MockAdapter

# Create API with a mock adapter.
api = Iota('mock://')
api = Iota(MockAdapter())

To use MockAdapter, you must first seed the responses that you want it to return by calling its MockAdapter.seed_response() method.

seed_response

MockAdapter.seed_response(command: str, response: dict) → iota.adapter.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.

Note that you have to call seed_response() once for each request you expect it to process. If MockAdapter does not have a seeded response for a particular command, it will raise a BadApiResponse exception (simulates a 404 response).

Parameters
  • command (str) – The name of the command. Note that this is the camelCase version of the command name (e.g., getNodeInfo, not get_node_info).

  • response (dict) – The response that the adapter will return.

Example usage:

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!'}

Wrappers

Wrappers act like decorators for adapters; they are used to enhance or otherwise modify the behavior of adapters.

RoutingWrapper

class iota.adapter.wrappers.RoutingWrapper(default_adapter: Union[str, BaseAdapter])

Routes commands (API requests) to different nodes depending on the command name.

This allows you to, for example, send POW requests to a local node, while routing all other requests to a remote one.

Once you’ve initialized the RoutingWrapper, invoke its add_route() method to specify a different adapter to use for a particular command.

Parameters

default_adapter (AdapterSpec) – RoutingWrapper must be initialized with a default URI/adapter. This is the adapter that will be used for any command that doesn’t have a route associated with it.

Returns

RoutingWrapper object.

Example usage:

from iota import Iota
from iota.adapter.wrappers import RoutingWrapper

# Route POW to localhost, everything else to 'https://nodes.thetangle.org:443'.
api = Iota(
  RoutingWrapper('https://nodes.thetangle.org:443.'')
    .add_route('attachToTangle', 'http://localhost:14265')
    .add_route('interruptAttachingToTangle', 'http://localhost:14265')
)

Note

A common use case for RoutingWrapper is to perform proof-of-work on a specific (local) node, but let all other requests go to another node. Take care when you use RoutingWrapper adapter and local_pow parameter together in an API instance (see iota.Iota), because the behavior might not be obvious.

local_pow tells the API to perform proof-of-work (iota.Iota.attach_to_tangle()) without relying on an actual node. It does this by calling an extension package PyOTA-PoW that does the job. In PyOTA, this means the request doesn’t reach the adapter, it is redirected before. As a consequence, local_pow has precedence over the route that is defined in RoutingWrapper.

add_route

RoutingWrapper.add_route(command: str, adapter: Union[str, BaseAdapter]) → iota.adapter.wrappers.RoutingWrapper

Adds a route to the wrapper.

Parameters
  • command (str) – The name of the command. Note that this is the camelCase version of the command name (e.g., attachToTangle, not attach_to_tangle).

  • adapter (AdapterSpec) – The adapter object or URI to route requests to.

Returns

The RoutingWrapper object it was called on. Useful for chaining the operation of adding routes in code.

See RoutingWrapper for example usage.