-
Notifications
You must be signed in to change notification settings - Fork 125
Asynchronous API, Networking and Asyncio Support #301
Conversation
- httpx supports both sync and async
- Introduce 'AsyncStrictIota' and 'AsyncIota' for async API - Refactor original API classes to rely on their async counterpart - Make 'BaseCommand' async - Make 'AttachToTangleCommand' async
- update tests for async
- Refactor `BroadCastBundleCommand` and update its tests - Refactor `GetBundleCommand` and its update tests - Refactor `TraverseBundleCommand` and update its tests `get_bundles` fetches bundles concurrently, which speeds it up if it is called with more than one argument.
- Update tests for async
- Updated tests for async - `iter_used_addresses` becomes an async generator - `get_bundles_from_transaction_hashes` ready for async - Update `utils_test.py`
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
`Helpers` added `is_promotable` method (wrapper for checkConsistency) to the api class, which is now implemented as a standalone API command, see `IsPromotableCommand`.
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- New class `AsyncMultisigIota` - Original `MultisigIota` rebased on `AsyncMultisigIota` - Updated multisig commands to work with async - Updated test for async
The new async functionalities are incompatible with anything older, than Python 3.6. Future work: - Clean up legacy PY2 -> PY3 code snippets. (future, six, etc..) - Transform codebase to use PY3 style code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good with minor questions/suggestions.
iota/api.py
Outdated
# Execute original coroutine inside an event loop to make this method | ||
# synchronous | ||
return asyncio.get_event_loop().run_until_complete( | ||
super(StrictIota, self).add_neighbors(uris) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super(StrictIota, self).add_neighbors(uris) | |
super().add_neighbors(uris) |
💭 Since we're no longer supporting Python 2, we can use py3-style super()
calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, @todofixthis could you explain me why super()
is called in a base class?
For example here:
@add_metaclass(AdapterMeta)
class BaseAdapter(object):
"""
Interface for IOTA API adapters.
Adapters make it easy to customize the way an API instance
communicates with a node.
"""
supported_protocols = () # type: Tuple[Text]
"""
Protocols that ``resolve_adapter`` can use to identify this adapter
type.
"""
def __init__(self):
super(BaseAdapter, self).__init__()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 Good question. I think the reason I did this is in case a derived class uses multiple inheritance, e.g., CustomAdapter(BaseAdapter, OtherBase):
(or possibly the other way around; multiple inheritance is so confusing 😅) — without the super()
call, it's possible that the constructor for one or more bases won't get called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, thanks for the answer!
Let's say we switch to py3-style super()
calls everywhere and don't need to explicitly tell type
and object
to super()
. If I do it with BaseAdapter
:
@add_metaclass(AdapterMeta)
class BaseAdapter:
"""
Interface for IOTA API adapters.
Adapters make it easy to customize the way an API instance
communicates with a node.
"""
supported_protocols = () # type: Tuple[Text]
"""
Protocols that ``resolve_adapter`` can use to identify this adapter
type.
"""
def __init__(self):
super().__init__()
I get a TypeError
in the super()
call:
Traceback (most recent call last):
File "/pyota/test/commands/extended/utils_test.py", line 16, in setUp
self.adapter = MockAdapter()
File "/pyota/iota/adapter/__init__.py", line 547, in __init__
super(MockAdapter, self).__init__()
File "/pyota/iota/adapter/__init__.py", line 172, in __init__
super().__init__()
TypeError: super(type, obj): obj must be an instance or subtype of type
, but the MRO seems okay:
>>> from iota import BaseAdapter
>>> BaseAdapter.__mro__
(<class 'iota.adapter.BaseAdapter'>, <class 'object'>)
What am I missing here? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for checking that out! Hmmm.. maybe it's not needed in Python 3 then? I don't recall the exact situation where I thought it was necessary; maybe we just remove it and see if anyone complains 😅
- docstrings update - info in comment for future devs about api classes
- Better reflect the diff between them, - For easier code maintenance , - Add explanatory comment.
- AsyncClient instance attribute in HttpAdapter class - Client will keep and reuse connections to the same host
Solves #224
Description
Motivation
So far, PyOTA has been relying on synchronous communication with nodes. This meant, that API requests could only be sent sequentially, one after the other. While this is simple and easy for developers to comprehend, it doesn't scale well and wastes a lot of precious time waiting for the results of network IO bound tasks.
Solution
With this PR, PyOTA is capable of doing asynchronous networking. Requests can be sent concurrently with the help of
asyncio
andhttpx
packages. When using the newAsyncIota
API class, one can schedule API calls to execute concurrently. The API calls in the new class are actually python coroutines, but both their argument lists and return values are preserved from the previous version of PyOTA.Regular, synchronous API calls are still supported with the "old"
Iota
API class. There should be no difference from a user perspective to issue a synchronous API call, however, the methods have been reworked to execute the aforementioned coroutines inside an event loop under the hood. This speeds up some extended API calls too.Changes in source code
Use
AsyncClient
fromhttpx
for networking instead ofrequests
.httpx
supports Python 3.6+.Mimic the behavior of
AsyncClient
inMockAdapter
, return anasyncio.Future
response object for a request.Refactor
RoutingWrapper
to work with async.Create new API classes:
AsyncStrictIota
andAsyncIota
.These API classes define the API calls as coroutines rather than methods. Hence, the API calls can be awaited by the application developer.
Refactor
StrictIota
andIota
classes to build on top of the two new classes. They provide the synchronous method interface by executing the parents' coroutines in an event loop and returning their result.Refactor commands to work asynchronously. Modify
BaseCommand.__call__()
andBaseCommand._execute()
to work as coroutines.All core commands are async.
Update extended commands to work as async.
BroadcastAndStoreCommand
is (nearly) twice as fast as before, which speeds up for exampleSendTrytesCommand
andSendTransferCommand
as well.GetBundlesCommand
is faster when called with multiple args.GetNewAddressesCommand
is faster as internal address checks are done concurrently.Remove
Helpers
class asIsPromotableCommand
is properly implemented as an extended API call.Update
iota.multisig
module to work async, create newAsyncMultisigIota
API class and rebase the original multisig api class on top of it, using the same logic as above.Remove Support for Pyhton 2 and Python 3.5!
Async modules do not work well with
3.6>
.Changes in test code
aiounittest
module for testing async coroutines.@async_test
decorator runs a test method inside an event loop.Future work in another PR(s):