Advanced: PyOTA Commands

Note

This page contains information about how PyOTA works under the hood.

It is absolutely not necessary to be familiar with the content described below if you just want to use the library.

However, if you are a curious mind or happen to do development on the library, the following information might be useful.

PyOTA provides the API interface (Core API Methods and Extended API Methods) for users of the library. These handle constructing and sending HTTP requests to the specified node through adapters, furthermore creating, transforming and translating between PyOTA-specific types and (JSON-encoded) raw data. They also filter outgoing requests and incoming responses to ensure that only appropriate data is communicated with the node.

PyOTA implements the Command Design Pattern. High level API interface methods (Core API Methods and Extended API Methods) internally call PyOTA commands to get the job done.

Most PyOTA commands are sub-classed from FilterCommand class, which is in turn sub-classed from BaseCommand class. The reason for the 2-level inheritance is simple: separating functionality. As the name implies, FilterCommand adds filtering capabilities to BaseCommand, that contains the logic of constructing the request and using its adapter to send it and receive a response.

Command Flow

As mentioned earlier, API methods rely on PyOTA commands to carry out specific operations. It is important to understand what happens during command execution so you are able to implement new methods that extend the current capabilities of PyOTA.

Let’s investigate the process through an example of a core API method, for instance find_transactions(), that calls FindTransactionCommand PyOTA command internally.

Note

FindTransactionCommand is sub-classed from FilterCommand.

To illustrate what the happens inside the API method, take a look at the following figure

Inner workings of a PyOTA Command.

Inner workings of a PyOTA Command.

  • When you call find_transactions() core API method, it initializes a FindTransactionCommand object with the adapter of the API instance it belongs to.

  • Then calls this command with the keyword arguments it was provided with.

  • The command prepares the request by applying a RequestFilter on the payload. The command specific RequestFilter validates that the payload has correct types, in some cases it is even able to convert the payload to the required type and format.

  • Command execution injects the name of the API command (see IRI API Reference for command names) in the request and sends it to the adapter.

  • The adapter communicates with the node and returns its response.

  • The response is prepared by going through a command-specific ResponseFilter.

  • The response is returned to the high level API method as a dict, ready to be returned to the main application.

Note

A command object can only be called once without resetting it. When you use the high level API methods, you don’t need to worry about resetting commands as each call to an API method will initialize a new command object.

Filters

If you take a look at the actual implementation of FindTransactionsCommand, you notice that you have to define your own request and response filter classes.

Filters in PyOTA are based on the Filters library. Read more about how they work at the filters documentation site.

In short, you can create filter chains through which the filtered value passes, and generates errors if something failed validation. Filter chains are specified in the custom filter class’s __init__() function. If you also want to modify the filtered value before returning it, override the _apply() method of its base class. Read more about how to create custom filters.

PyOTA offers you some custom filters for PyOTA-specific types:

Trytes

class iota.filters.Trytes(result_type: type = <class 'iota.types.TryteString'>)

Validates a sequence as a sequence of trytes.

When a value doesn’t pass the filter, a ValueError is raised with lots of contextual info attached to it.

Parameters

result_type (TryteString) – Any subclass of TryteString that you want the filter to validate.

Raises
  • TypeError – if value is not of result_type.

  • ValueError – if result_type is not of TryteString type.

Returns

Trytes object.

StringifiedTrytesArray

filters.StringifiedTrytesArray(**runtime_kwargs) → filters.base.FilterChain

Validates that the incoming value is an array containing tryte strings corresponding to the specified type (e.g., TransactionHash).

When a value doesn’t pass the filter, a ValueError is raised with lots of contextual info attached to it.

Parameters

trytes_type (TryteString) – Any subclass of TryteString that you want the filter to validate.

Returns

filters.FilterChain object.

Important

This filter will return string values, suitable for inclusion in an API request. If you are expecting objects (e.g., Address), then this is not the filter to use!

Note

This filter will allow empty arrays and None. If this is not desirable, chain this filter with f.NotEmpty or f.Required, respectively.

AddressNoChecksum

class iota.filters.AddressNoChecksum

Validates a sequence as an Address, then chops off the checksum if present.

When a value doesn’t pass the filter, a ValueError is raised with lots of contextual info attached to it.

Returns

AddressNoChecksum object.

GeneratedAddress

class iota.filters.GeneratedAddress

Validates an incoming value as a generated Address (must have key_index and security_level set).

When a value doesn’t pass the filter, a ValueError is raised with lots of contextual info attached to it.

Returns

GeneratedAddress object.

NodeUri

class iota.filters.NodeUri

Validates a string as a node URI.

When a value doesn’t pass the filter, a ValueError is raised with lots of contextual info attached to it.

Returns

NodeUri object.

SCHEMES = {'tcp', 'udp'}

Allowed schemes for node URIs.

SecurityLevel

filters.SecurityLevel(**runtime_kwargs) → filters.base.FilterChain

Generates a filter chain for validating a security level.

Returns

filters.FilterChain object.

Important

The general rule in PyOTA is that all requests going to a node are validated, but only responses that contain transaction/bundle trytes or hashes are checked.

Also note, that for extended commands, ResponseFilter is usually implemented with just a “pass” statement. The reason being that these commands do not directly receive their result a node, but rather from core commands that do have their ResponseFilter implemented. More about this topic in the next section.

Extended Commands

Core commands, like find_transactions() in the example above, are for direct communication with the node for simple tasks such as finding a transaction on the Tangle or getting info about the node. Extended commands (that serve Extended API Methods) on the other hand carry out more complex operations such as combining core commands, building objects, etc…

As a consequence, extended commands override the default execution phase of their base class.

Observe for example FindTransactionObjectsCommand extended command that is called in find_transaction_objects() extended API method. It overrides the _execute() method of its base class.

Let’s take a closer look at the implementation:

...
def _execute(self, request):
    bundles = request\
        .get('bundles')  # type: Optional[Iterable[BundleHash]]
    addresses = request\
        .get('addresses')  # type: Optional[Iterable[Address]]
    tags = request\
        .get('tags')  # type: Optional[Iterable[Tag]]
    approvees = request\
        .get('approvees')  # type: Optional[Iterable[TransactionHash]]

    ft_response = FindTransactionsCommand(adapter=self.adapter)(
        bundles=bundles,
        addresses=addresses,
        tags=tags,
        approvees=approvees,
    )

    hashes = ft_response['hashes']
    transactions = []
    if hashes:
        gt_response = GetTrytesCommand(adapter=self.adapter)(hashes=hashes)

        transactions = list(map(
            Transaction.from_tryte_string,
            gt_response.get('trytes') or [],
        ))  # type: List[Transaction]

    return {
        'transactions': transactions,
    }
...

Instead of sending the request to the adapter, FindTransactionObjectsCommand._execute() calls FindTransactionsCommand core command, gathers the transaction hashes that it found, and collects the trytes of those transactions by calling GetTrytesCommand core command. Finally, using the obtained trytes, it constructs a list of transaction objects that are returned to find_transaction_objects().

Important

If you come up with a new functionality for the PyOTA API, please raise an issue in the PyOTA Bug Tracker to facilitate discussion.

Once the community agrees on your proposal, you may start implementing a new extended API method and the corresponding extended PyOTA command.

Contributions are always welcome! :)

Visit the Contributing to PyOTA page to find out how you can make a difference!