Migrate Python libraries to v3 (#2284)

* .gitignore migrations/0x_ganache_snapshot

* .gitignore new-ish Python contract wrappers

These should have been added back when we started generating these
wrappers.

* rm superfluous contract artifact in Python package

All of the contract artifacts were removed from the Python package
recently, because now they're copied from the monorepo/packages area as
an automated build step.  Somehow this one artifact slipped through the
cracks.

* Eliminate circular dependency

This was preventing the Exchange wrapper from ever importing its
validator!

* Improve output of monorepo-level parallel script

- Capture stderr (and have it included in stdout) so that it doesn't
leak onto the console for commands that didn't actually fail.

- Include all error output in the Exception object (eliminate print
statement).

* Silence new versions of linters

Newer versions care about this stuff.  Old versions didn't, and we don't
either.

* Support Rich Reverts via Web3.py middleware

* Fix bug in generated wrappers' bytes handling

`bytes.fromhex(bytes.decode('utf-8')` is just plain wrong.  It would
work for some cases, but is not working when trying to fill orders with
the latest Exchange contract.

* Migrate to Exchange v3

* Fix typo in DevUtils documentation

* Include new contracts in docs

* Re-enable Python checks in CI

* Accept strings for bytes

* Fix CircleCI build artifacts for gen'd python

I swear the previous way was working before, but it wasn't working now,
so this fixes it.

* Accept a provider OR a Web3 object

In various places.  This allows the caller to install middleware (which
in web3.py is installed on a Web3 object, not on a provider) before
executing any RPC calls, which is important for the case where one wants
to produce signatures locally before submitting to a remote node.

* wrapper base: don't assume there are accounts

* Eliminate some inline linter directives

* make CHANGELOGs be REVERSE chronological

* Update CHANGELOG entries and bump version numbers

* @0x/contract-addresses: Put addr's in JSON, not TS

This allows easier consumption by other languages.  (Specifically, it
eliminates the overhead of keeping the Python addresses package in sync
with the TypeScript one.)

* sra_client.py: incl. docker in `./setup.py clean`

* sra_client.py: Migrate to protocol v3

Removed script that existed only to exclude runs of sra_client builds
(parallel_without_sra_client).  Now `parallel` is used by CI,
re-including sra_client in CI checks.

* abi-gen/templates/Py: clarify if/else logic

In response to
https://github.com/0xProject/0x-monorepo/pull/2284#discussion_r342200906

* sra_client.py: Update CHANGELOG and bump version

* contract_addresses/setup.py: rm unnecessary rm

* json_schemas.py: corrections to dev dependencies

* In tests against deployment, also run doctests

* contract_wrappers example: rm xtra Order attribute

Thanks to @steveklebanoff for catching this.
https://github.com/0xProject/0x-monorepo/pull/2284#pullrequestreview-312065368
This commit is contained in:
F. Eugene Aumson
2019-11-05 23:04:29 -05:00
committed by GitHub
parent cbe4c4fbf9
commit e61f23d001
69 changed files with 1996 additions and 941 deletions

View File

@@ -1,5 +1,9 @@
# Changelog
## 4.0.0 - TBD
- Migrated from v2 to v3 of the 0x protocol.
## 3.0.0 - 2019-08-08
- Migrated from v4 to v5 of Web3.py.

View File

@@ -3,10 +3,15 @@
"""setuptools module for sra_client package."""
# pylint: disable=import-outside-toplevel
# we import things outside of top-level because 3rd party libs may not yet be
# installed when you invoke this script
import subprocess # nosec
import distutils.command.build_py
from distutils.command.clean import clean
from shutil import rmtree
from sys import exit # pylint: disable=redefined-builtin
from urllib.request import urlopen
from urllib.error import URLError
@@ -14,7 +19,7 @@ from setuptools import setup, find_packages # noqa: H301
from setuptools.command.test import test as TestCommand
NAME = "0x-sra-client"
VERSION = "3.0.0"
VERSION = "4.0.0"
# To install the library, run the following
#
# python setup.py install
@@ -41,6 +46,12 @@ class CleanCommandExtension(clean):
rmtree("0x_sra_client.egg-info", ignore_errors=True)
rmtree("build", ignore_errors=True)
rmtree("dist", ignore_errors=True)
subprocess.check_call( # nosec
("docker-compose -f test/relayer/docker-compose.yml down").split()
)
subprocess.check_call( # nosec
("docker-compose -f test/relayer/docker-compose.yml rm").split()
)
class TestCommandExtension(TestCommand):
@@ -85,12 +96,15 @@ class StartTestRelayerCommand(distutils.command.build_py.build_py):
("docker-compose -f test/relayer/docker-compose.yml up -d").split()
)
launch_kit_ready = False
print("Waiting for relayer to start accepting connections...", end="")
print(
"Waiting for Launch Kit Backend to start accepting connections...",
flush=True,
)
while not launch_kit_ready:
try:
launch_kit_ready = (
urlopen( # nosec
"http://localhost:3000/v2/asset_pairs"
"http://localhost:3000/v3/asset_pairs"
).getcode()
== 200
)

View File

@@ -1,2 +1,2 @@
"""0x Python API."""
__import__("pkg_resources").declare_namespace(__name__)
__import__("pkg_resources").declare_namespace(__name__) # type: ignore

View File

@@ -19,7 +19,7 @@ Install the package with pip::
pip install 0x-sra-client
To interact with a 0x Relayer, you need the HTTP endpoint of the Relayer you'd
like to connect to (eg https://api.radarrelay.com/0x/v2).
like to connect to (eg https://api.radarrelay.com/0x/v3).
For testing one can use the `0x-launch-kit-backend
<https://github.com/0xProject/0x-launch-kit-backend#table-of-contents/>`_ to host
@@ -83,8 +83,8 @@ for this account, so the example orders below have the maker trading away ZRX.
Before such an order can be valid, though, the maker must give the 0x contracts
permission to trade their ZRX tokens:
>>> from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES
>>> contract_addresses = NETWORK_TO_ADDRESSES[network_id]
>>> from zero_ex.contract_addresses import network_to_addresses
>>> contract_addresses = network_to_addresses(network_id)
>>>
>>> from zero_ex.contract_artifacts import abi_by_name
>>> zrx_token_contract = Web3(eth_node).eth.contract(
@@ -105,7 +105,8 @@ Post Order
Post an order for our Maker to trade ZRX for WETH:
>>> from zero_ex.contract_wrappers.exchange.types import Order, order_to_jsdict
>>> from zero_ex.contract_wrappers.exchange.types import Order
>>> from zero_ex.contract_wrappers.order_conversions import order_to_jsdict
>>> from zero_ex.order_utils import (
... asset_data_utils,
... sign_hash)
@@ -120,9 +121,11 @@ Post an order for our Maker to trade ZRX for WETH:
... makerAssetData=asset_data_utils.encode_erc20(
... contract_addresses.zrx_token
... ),
... makerFeeAssetData=asset_data_utils.encode_erc20('0x'+'00'*20),
... takerAssetData=asset_data_utils.encode_erc20(
... contract_addresses.ether_token
... ),
... takerFeeAssetData=asset_data_utils.encode_erc20('0x'+'00'*20),
... salt=random.randint(1, 100000000000000000),
... makerFee=0,
... takerFee=0,
@@ -135,7 +138,7 @@ Post an order for our Maker to trade ZRX for WETH:
>>> from zero_ex.order_utils import generate_order_hash_hex
>>> order_hash_hex = generate_order_hash_hex(
... order, contract_addresses.exchange
... order, contract_addresses.exchange, Web3(eth_node).eth.chainId
... )
>>> relayer.post_order_with_http_info(
... network_id=network_id.value,
@@ -144,7 +147,8 @@ Post an order for our Maker to trade ZRX for WETH:
... exchange_address=contract_addresses.exchange,
... signature=sign_hash(
... eth_node, Web3.toChecksumAddress(maker_address), order_hash_hex
... )
... ),
... chain_id=Web3(eth_node).eth.chainId,
... )
... )[1]
200
@@ -152,24 +156,35 @@ Post an order for our Maker to trade ZRX for WETH:
Get Order
---------
(But first sleep for a moment, to give the test relayer a chance to start up.
>>> from time import sleep
>>> sleep(0.2)
This is necessary for automated verification of these examples.)
Retrieve the order we just posted:
>>> relayer.get_order("0x" + order_hash_hex)
{'meta_data': {},
'order': {'exchangeAddress': '0x...',
{'meta_data': {'orderHash': '0x...',
'remainingFillableTakerAssetAmount': '2'},
'order': {'chainId': 50,
'exchangeAddress': '0x...',
'expirationTimeSeconds': '...',
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',
'makerAssetAmount': '2',
'makerAssetData': '0xf47261b0000000000000000000000000...',
'makerFee': '0',
'makerFeeAssetData': '0xf47261b0000000000000000000000000...',
'salt': '...',
'senderAddress': '0x0000000000000000000000000000000000000000',
'signature': '0x...',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': '2',
'takerAssetData': '0xf47261b0000000000000000000000000...',
'takerFee': '0'}}
'takerFee': '0',
'takerFeeAssetData': '0xf47261b0000000000000000000000000...'}}
Get Orders
-----------
@@ -178,21 +193,25 @@ Retrieve all of the Relayer's orders, a set which at this point consists solely
of the one we just posted:
>>> relayer.get_orders()
{'records': [{'meta_data': {},
'order': {'exchangeAddress': '0x...',
{'records': [{'meta_data': {'orderHash': '0x...',
'remainingFillableTakerAssetAmount': '2'},
'order': {'chainId': 50,
'exchangeAddress': '0x...',
'expirationTimeSeconds': '...',
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',
'makerAssetAmount': '2',
'makerAssetData': '0xf47261b000000000000000000000000...',
'makerFee': '0',
'makerFeeAssetData': '0xf47261b000000000000000000000000...',
'salt': '...',
'senderAddress': '0x0000000000000000000000000000000000000000',
'signature': '0x...',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': '2',
'takerAssetData': '0xf47261b0000000000000000000000000...',
'takerFee': '0'}}]}
'takerFee': '0',
'takerFeeAssetData': '0xf47261b0000000000000000000000000...'}}...]}
Get Asset Pairs
---------------
@@ -233,43 +252,50 @@ consists just of our order):
... ).hex(),
... )
>>> orderbook
{'asks': {'records': []},
'bids': {'records': [{'meta_data': {},
'order': {'exchangeAddress': '0x...',
{'asks': {'records': [...]},
'bids': {'records': [{'meta_data': {'orderHash': '0x...',
'remainingFillableTakerAssetAmount': '2'},
'order': {'chainId': 50,
'exchangeAddress': '0x...',
'expirationTimeSeconds': '...',
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',
'makerAssetAmount': '2',
'makerAssetData': '0xf47261b0000000000000000000000000...',
'makerFee': '0',
'makerFeeAssetData': '0xf47261b0000000000000000000000000...',
'salt': '...',
'senderAddress': '0x0000000000000000000000000000000000000000',
'signature': '0x...',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': '2',
'takerAssetData': '0xf47261b0000000000000000000000000...',
'takerFee': '0'}}]}}
'takerFee': '0',
'takerFeeAssetData': '0xf47261b0000000000000000000000000...'}}...]}}
Select an order from the orderbook
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>>> from zero_ex.contract_wrappers.exchange.types import jsdict_to_order
>>> from zero_ex.contract_wrappers.order_conversions import jsdict_to_order
>>> order = jsdict_to_order(orderbook.bids.records[0].order)
>>> from pprint import pprint
>>> pprint(order)
{'expirationTimeSeconds': ...,
{'chainId': 50,
'expirationTimeSeconds': ...,
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',
'makerAssetAmount': 2,
'makerAssetData': b...
'makerFee': 0,
'makerFeeAssetData': b...
'salt': ...,
'senderAddress': '0x0000000000000000000000000000000000000000',
'signature': '0x...',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': 2,
'takerAssetData': b...
'takerFee': 0}
'takerAssetData': b...,
'takerFee': 0,
'takerFeeAssetData': b...}
Filling or Cancelling an Order
------------------------------
@@ -319,8 +345,8 @@ book. Now let's have the taker fill it:
>>> from zero_ex.contract_wrappers.exchange import Exchange
>>> from zero_ex.order_utils import Order
>>> exchange = Exchange(
... provider=eth_node,
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange
... web3_or_provider=eth_node,
... contract_address=network_to_addresses(NetworkId.GANACHE).exchange
... )
(Due to `an Issue with the Launch Kit Backend
@@ -331,7 +357,7 @@ checksum the address in the order before filling it.)
>>> exchange.fill_order.send_transaction(
... order=order,
... taker_asset_fill_amount=order['makerAssetAmount']/2, # note the half fill
... signature=order['signature'].replace('0x', '').encode('utf-8'),
... signature=bytes.fromhex(order['signature'].replace('0x', '')),
... tx_params=TxParams(from_=taker_address)
... )
HexBytes('0x...')

View File

@@ -139,7 +139,7 @@ class DefaultApi(object):
auth_settings = []
return self.api_client.call_api(
"/v2/asset_pairs",
"/v3/asset_pairs",
"GET",
path_params,
query_params,
@@ -250,7 +250,7 @@ class DefaultApi(object):
auth_settings = []
return self.api_client.call_api(
"/v2/fee_recipients",
"/v3/fee_recipients",
"GET",
path_params,
query_params,
@@ -363,7 +363,7 @@ class DefaultApi(object):
auth_settings = []
return self.api_client.call_api(
"/v2/order/{orderHash}",
"/v3/order/{orderHash}",
"GET",
path_params,
query_params,
@@ -497,7 +497,7 @@ class DefaultApi(object):
auth_settings = []
return self.api_client.call_api(
"/v2/order_config",
"/v3/order_config",
"POST",
path_params,
query_params,
@@ -680,7 +680,7 @@ class DefaultApi(object):
auth_settings = []
return self.api_client.call_api(
"/v2/orderbook",
"/v3/orderbook",
"GET",
path_params,
query_params,
@@ -718,50 +718,48 @@ class DefaultApi(object):
:param bool async_req: Whether request should be asynchronous.
:param str maker_asset_proxy_id: The maker
`asset proxy id
<https://0x.org/docs/tools/0x.js#types-AssetProxyId>`__
<https://0x.org/docs/tools/0x.js#enumeration-assetproxyid>`__
(example: "0xf47261b0" for ERC20, "0x02571792" for ERC721).
:param str taker_asset_proxy_id: The taker asset
`asset proxy id
<https://0x.org/docs/tools/0x.js#types-AssetProxyId>`__
<https://0x.org/docs/tools/0x.js#enumeration-assetproxyid>`__
(example: "0xf47261b0" for ERC20, "0x02571792" for ERC721).
:param str maker_asset_address: The contract address for the maker asset.
:param str taker_asset_address: The contract address for the taker asset.
:param str exchange_address: Same as exchangeAddress in the
`0x Protocol v2 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
:param str exchange_address: Contract address for the exchange
contract.
:param str sender_address: Same as senderAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str maker_asset_data: Same as makerAssetData in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str taker_asset_data: Same as takerAssetData in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str trader_asset_data: Same as traderAssetData in the [0x
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str maker_address: Same as makerAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str taker_address: Same as takerAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str trader_address: Same as traderAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str fee_recipient_address: Same as feeRecipientAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param int network_id: The id of the Ethereum network
:param int page: The number of the page to request in the collection.
:param int per_page: The number of records to return per page.
@@ -795,50 +793,50 @@ class DefaultApi(object):
:param bool async_req: Whether request should be asynchronous.
:param str maker_asset_proxy_id: The maker
`asset proxy id
<https://0x.org/docs/tools/0x.js#types-AssetProxyId>`__
<https://0x.org/docs/tools/0x.js#enumeration-assetproxyid>`__
(example: "0xf47261b0" for ERC20, "0x02571792" for ERC721).
:param str taker_asset_proxy_id: The taker asset
`asset proxy id
<https://0x.org/docs/tools/0x.js#types-AssetProxyId>`__
<https://0x.org/docs/tools/0x.js#enumeration-assetproxyid>`__
(example: "0xf47261b0" for ERC20, "0x02571792" for ERC721).
:param str maker_asset_address: The contract address for the maker asset.
:param str taker_asset_address: The contract address for the taker asset.
:param str exchange_address: Same as exchangeAddress in the [0x
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str sender_address: Same as senderAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str maker_asset_data: Same as makerAssetData in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str taker_asset_data: Same as takerAssetData in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str trader_asset_data: Same as traderAssetData in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str maker_address: Same as makerAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str taker_address: Same as takerAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str trader_address: Same as traderAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param str fee_recipient_address: Same as feeRecipientAddress in the
`0x Protocol v2 Specification
`0x Protocol v3 Specification
<https://github.com/0xProject/0x-protocol-specification/blob/
master/v2/v2-specification.md#order-message-format>`__
master/v3/v3-specification.md#order-message-format>`__
:param int network_id: The id of the Ethereum network
:param int page: The number of the page to request in the collection.
:param int per_page: The number of records to return per page.
@@ -965,7 +963,7 @@ class DefaultApi(object):
auth_settings = []
return self.api_client.call_api(
"/v2/orders",
"/v3/orders",
"GET",
path_params,
query_params,
@@ -1077,7 +1075,7 @@ class DefaultApi(object):
auth_settings = []
return self.api_client.call_api(
"/v2/order",
"/v3/order",
"POST",
path_params,
query_params,

View File

@@ -1,14 +1,36 @@
# Run Launch Kit with Ganache as the backing node
# Run Launch Kit Backend with Ganache and Mesh instances backing it.
version: '3'
services:
ganache:
image: "0xorg/ganache-cli:2.2.2"
image: "0xorg/ganache-cli:4.4.0-beta.1"
ports:
- "8545:8545"
launchkit:
image: "0xorg/launch-kit-backend:74bcc39"
environment:
- VERSION=4.4.0-beta.1
- SNAPSHOT_NAME=0x_ganache_snapshot-v3-beta
mesh:
image: 0xorg/mesh:6.0.0-beta-0xv3
depends_on:
- ganache
environment:
ETHEREUM_RPC_URL: 'http://localhost:8545'
ETHEREUM_NETWORK_ID: '50'
ETHEREUM_CHAIN_ID: '1337'
USE_BOOTSTRAP_LIST: 'true'
VERBOSITY: 3
PRIVATE_KEY_PATH: ''
BLOCK_POLLING_INTERVAL: '5s'
P2P_LISTEN_PORT: '60557'
ports:
- '60557:60557'
network_mode: "host" # to connect to ganache
command: |
sh -c "waitForGanache () { until printf 'POST /\r\nContent-Length: 26\r\n\r\n{\"method\":\"net_listening\"}' | nc localhost 8545 | grep true; do continue; done }; waitForGanache && ./mesh"
launch-kit-backend:
image: "0xorg/launch-kit-backend:v3"
depends_on:
- ganache
- mesh
ports:
- "3000:3000"
network_mode: "host" # to connect to ganache
@@ -16,5 +38,9 @@ services:
- NETWORK_ID=50
- RPC_URL=http://localhost:8545
- WHITELIST_ALL_TOKENS=True
- FEE_RECIPIENT=0x0000000000000000000000000000000000000001
- MAKER_FEE_UNIT_AMOUNT=0
- TAKER_FEE_UNIT_AMOUNT=0
- MESH_ENDPOINT=ws://localhost:60557
command: |
sh -c "until printf 'POST /\r\nContent-Length: 26\r\n\r\n{\"method\":\"net_listening\"}' | nc localhost 8545 | grep true; do continue; done; node_modules/.bin/forever ts/lib/index.js"
sh -c "waitForMesh () { sleep 3; }; waitForMesh && sleep 5 && node_modules/.bin/forever ts/lib/index.js"

View File

@@ -18,7 +18,7 @@ commands =
pytest test
[testenv:run_tests_against_deployment]
deps=pytest
setenv = PY_IGNORE_IMPORTMISMATCH = 1
commands =
pip install 0x-sra-client
pytest test
pip install 0x-sra-client[dev]
pytest --doctest-modules src test