New demos for Python packages (#1734)

End-to-end demos of constructing and signing an order and submitting it to a Relayer.  Docs are generated from the code, and include usage examples that are verified through automated testing.
This commit is contained in:
Michael Huang
2019-03-26 18:07:04 -05:00
committed by F. Eugene Aumson
parent 28c4ca73ab
commit 3099ba71eb
33 changed files with 872 additions and 697 deletions

View File

@@ -0,0 +1,4 @@
[MESSAGES CONTROL]
disable=C0330,line-too-long,fixme,too-few-public-methods,too-many-ancestors
# C0330 is "bad hanging indent". we use indents per `black`.

View File

@@ -1,11 +1,22 @@
[
{
"timestamp": 1553491629,
"version": "1.0.1",
"changes": [
{
"note": "Fix regex validation on numeric values"
"note": "Fix regex validation on numeric values",
"pr": 1731
}
]
],
"timestamp": 1553491629
},
{
"version": "1.0.1",
"changes": [
{
"note": "Added new devdependencies, and linting commands to `setup.py`. Added sphinx docs to demonstrate how to use sra_client.",
"pr": 1734
}
],
"timestamp": 1553183790
}
]

View File

@@ -0,0 +1,55 @@
"""Configuration file for the Sphinx documentation builder."""
# Reference: http://www.sphinx-doc.org/en/master/config
from typing import List
import pkg_resources
# pylint: disable=invalid-name
# because these variables are not named in upper case, as globals should be.
project = "0x-sra-client"
# pylint: disable=redefined-builtin
copyright = "2018, ZeroEx, Intl."
author = "F. Eugene Aumson"
version = pkg_resources.get_distribution("0x-sra-client").version
release = "" # The full version, including alpha/beta/rc tags
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx_autodoc_typehints",
]
templates_path = ["doc_templates"]
source_suffix = ".rst"
# eg: source_suffix = [".rst", ".md"]
master_doc = "index" # The master toctree document.
language = None
exclude_patterns: List[str] = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
html_theme = "alabaster"
html_static_path = ["doc_static"]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# Output file base name for HTML help builder.
htmlhelp_basename = "sraclientpydoc"
# -- Extension configuration:
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None}

View File

@@ -0,0 +1,25 @@
.. source for the sphinx-generated build/docs/web/index.html
Python zero_ex.sra_client.api_client
====================================
.. toctree::
:maxdepth: 2
:caption: Contents:
.. automodule:: sra_client
----
API
---
.. automodule:: sra_client.api.default_api
:members:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -1,14 +1,16 @@
#!/usr/bin/env python
# coding: utf-8
"""setuptools module for sra_client package."""
import subprocess
import subprocess # nosec
import distutils.command.build_py
from setuptools import setup, find_packages # noqa: H301
from setuptools.command.test import test as TestCommand
NAME = "0x-sra-client"
VERSION = "1.0.0"
VERSION = "1.0.1"
# To install the library, run the following
#
# python setup.py install
@@ -21,6 +23,17 @@ with open("README.md", "r") as file_handle:
REQUIRES = ["urllib3 >= 1.15", "six >= 1.10", "certifi", "python-dateutil"]
class TestCommandExtension(TestCommand):
"""Run pytest tests."""
def run_tests(self):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
class TestPublishCommand(distutils.command.build_py.build_py):
"""Custom command to publish to test.pypi.org."""
@@ -38,6 +51,59 @@ class TestPublishCommand(distutils.command.build_py.build_py):
)
class GanacheCommand(distutils.command.build_py.build_py):
"""Custom command to publish to pypi.org."""
description = "Run ganache daemon to support tests."
def run(self):
"""Run ganache."""
cmd_line = (
"docker run -d -p 8545:8545 0xorg/ganache-cli:2.2.2"
).split()
subprocess.call(cmd_line) # nosec
class LaunchKitCommand(distutils.command.build_py.build_py):
"""Custom command to boot up a local 0x-launch-kit in docker."""
description = "Run launch-kit daemon to support sra_client demos."
def run(self):
"""Run 0x-launch-kit."""
cmd_line = ("docker run -d -p 3000:3000 0xorg/launch-kit-ci").split()
subprocess.call(cmd_line) # nosec
class LintCommand(distutils.command.build_py.build_py):
"""Custom setuptools command class for running linters."""
description = "Run linters"
def run(self):
"""Run linter shell commands."""
lint_commands = [
# formatter:
"black --line-length 79 --check --diff test sra_client/__init__.py setup.py".split(), # noqa: E501 (line too long)
# style guide checker (formerly pep8):
"pycodestyle test sra_client/__init__.py setup.py".split(),
# docstring style checker:
"pydocstyle src test sra_client/__init__.py setup.py".split(),
# static type checker:
"bandit -r test sra_client/__init__.py setup.py".split(),
# general linter:
"pylint test sra_client/__init__.py setup.py".split(),
# pylint takes relatively long to run, so it runs last, to enable
# fast failures.
]
for lint_command in lint_commands:
print(
"Running lint command `", " ".join(lint_command).strip(), "`"
)
subprocess.check_call(lint_command) # nosec
class PublishCommand(distutils.command.build_py.build_py):
"""Custom command to publish to pypi.org."""
@@ -48,13 +114,17 @@ class PublishCommand(distutils.command.build_py.build_py):
subprocess.check_call("twine upload dist/*".split()) # nosec
class LintCommand(distutils.command.build_py.build_py):
"""No-op lint command to support top-level lint script."""
class PublishDocsCommand(distutils.command.build_py.build_py):
"""Custom command to publish docs to S3."""
description = "No-op"
description = (
"Publish docs to "
+ "http://0x-sra-demos-py.s3-website-us-east-1.amazonaws.com/"
)
def run(self):
pass
"""Run npm package `discharge` to build & upload docs."""
subprocess.check_call("discharge deploy".split()) # nosec
setup(
@@ -72,6 +142,33 @@ setup(
cmdclass={
"test_publish": TestPublishCommand,
"publish": PublishCommand,
"launch_kit": LaunchKitCommand,
"lint": LintCommand,
"publish_docs": PublishDocsCommand,
"test": TestCommandExtension,
"ganache": GanacheCommand,
},
extras_require={
"dev": [
"0x-contract-addresses",
"0x-order-utils",
"0x-web3",
"bandit",
"black",
"coverage",
"coveralls",
"pycodestyle",
"pydocstyle",
"pylint",
"pytest",
"sphinx",
"sphinx-autodoc-typehints",
]
},
command_options={
"build_sphinx": {
"source_dir": ("setup.py", "."),
"build_dir": ("setup.py", "build/docs"),
}
},
)

View File

@@ -2,6 +2,167 @@
# flake8: noqa
"""Python api client to interact with SRA compatible 0x relayers.
0x Protocol is an open standard. Many relayers opt-in to implementing a set of
`standard relayer API endpoints <http://sra-spec.s3-website-us-east-1.amazonaws.com/>`_
to make it easier for anyone to source liquidity that conforms to the 0x order format.
Here, we will show you how you can use our `sra_client
<https://github.com/0xProject/0x-monorepo/tree/development/python-packages/sra_client#0x-sra-client>`_
module to interact with 0x relayers that implements the Standard Relayer API.
Setup
=====
Install the sra-client package with pip:
`pip install 0x-sra-client`:code:
To interact with a 0x Relayer, you need the HTTP endpoint of the Relayer you'd like to
connect to (i.e. https://api.radarrelay.com/0x/v2).
For local testing one can use the `0x-launch-kit
<https://github.com/0xProject/0x-launch-kit#table-of-contents/>`_
to host orders locally. For convenience, a docker container is provided
for just this purpose. To start it:
`docker run -d -p 3000:3000 0xorg/launch-kit-ci`:code:
and then connect to the http server running at http://localhost:3000.
----
Configure and create an API client instance
--------------------------------------------
>>> from sra_client import ApiClient, Configuration
>>> from sra_client.api import DefaultApi
>>> config = Configuration()
>>> config.host = "http://localhost:3000"
>>> relayer_api = DefaultApi(ApiClient(config))
Post Order
-----------
Post an order to an SRA-compliant Relayer.
>>> from web3 import HTTPProvider, Web3
>>> from zero_ex.contract_addresses import (
... NETWORK_TO_ADDRESSES, NetworkId)
>>> from zero_ex.order_utils import (
... asset_data_utils,
... generate_order_hash_hex,
... jsdict_order_to_struct,
... sign_hash)
>>> provider = HTTPProvider("http://localhost:8545")
>>> maker_address = "0x5409ed021d9299bf6814279a6a1411a7e866a631"
>>> exchange_address = NETWORK_TO_ADDRESSES[NetworkId.KOVAN].exchange
>>> weth_address = NETWORK_TO_ADDRESSES[NetworkId.KOVAN].ether_token
>>> zrx_address = NETWORK_TO_ADDRESSES[NetworkId.KOVAN].zrx_token
>>> weth_asset_data = asset_data_utils.encode_erc20_asset_data(weth_address)
>>> zrx_asset_data = asset_data_utils.encode_erc20_asset_data(zrx_address)
>>> example_order = {
... "makerAddress": maker_address,
... "takerAddress": "0x0000000000000000000000000000000000000000",
... "senderAddress": "0x0000000000000000000000000000000000000000",
... "exchangeAddress": exchange_address,
... "feeRecipientAddress":
... "0x0000000000000000000000000000000000000000",
... "makerAssetData": weth_asset_data,
... "takerAssetData": zrx_asset_data,
... "salt": "2362734632784682376287462",
... "makerFee": "0",
... "takerFee": "0",
... "makerAssetAmount": "1000000000000000000",
... "takerAssetAmount": "500000000000000000000",
... "expirationTimeSeconds": "999999999999999999999"}
>>> order_hash = generate_order_hash_hex(
... jsdict_order_to_struct(example_order), exchange_address)
>>> example_order["signature"] = sign_hash(
... provider, Web3.toChecksumAddress(maker_address), order_hash)
>>> relayer_api.post_order_with_http_info(
... network_id=42, signed_order_schema=example_order)[1]
200
Get Orders
-----------
Get orders from an SRA-compliant Relayer.
>>> relayer_api.get_orders()
{'records': [{'meta_data': {},
'order': {'exchange_address': '0x35dd2932454449b14cee11a94d3674a936d5d7b2',
'expiration_time_seconds': '1000000000000000000000',
'fee_recipient_address': '0x0000000000000000000000000000000000000000',
'maker_address': '0x5409ed021d9299bf6814279a6a1411a7e866a631',
'maker_asset_amount': '1000000000000000000',
'maker_asset_data': '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c',
'maker_fee': '0',
'salt': '2362734632784682376287462',
'sender_address': '0x0000000000000000000000000000000000000000',
'taker_address': '0x0000000000000000000000000000000000000000',
'taker_asset_amount': '500000000000000000000',
'taker_asset_data': '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa',
'taker_fee': '0'}}]}
Get Order
---------
Get an order by hash from an SRA-compliant Relayer.
>>> relayer_api.get_order(order_hash) # doctest: +SKIP
{'meta_data': {},
'order': {'exchange_address': '0x35dd2932454449b14cee11a94d3674a936d5d7b2',
'expiration_time_seconds': '1000000000000000000000',
'fee_recipient_address': '0x0000000000000000000000000000000000000000',
'maker_address': '0x5409ed021d9299bf6814279a6a1411a7e866a631',
'maker_asset_amount': '1000000000000000000',
'maker_asset_data': '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c',
'maker_fee': '0',
'salt': '2362734632784682376287462',
'sender_address': '0x0000000000000000000000000000000000000000',
'taker_address': '0x0000000000000000000000000000000000000000',
'taker_asset_amount': '500000000000000000000',
'taker_asset_data': '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa',
'taker_fee': '0'}},
Get Asset Pair
---------------
Get available asset pairs from an SRA-compliant Relayer.
>>> relayer_api.get_asset_pairs()
{'records': [{'assetDataA': {'assetData': '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c',
'maxAmount': '115792089237316195423570985008687907853269984665640564039457584007913129639936',
'minAmount': '0',
'precision': 18},
'assetDataB': {'assetData': '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa',
'maxAmount': '115792089237316195423570985008687907853269984665640564039457584007913129639936',
'minAmount': '0',
'precision': 18}}]}
Get Orderbook
-------------
Get the orderbook for the WETH/ZRX asset pair from an SRA-compliant Relayer.
>>> relayer_api.get_orderbook(
... base_asset_data=weth_asset_data,
... quote_asset_data=zrx_asset_data)
{'asks': {'records': [{'meta_data': {},
'order': {'exchange_address': '0x35dd2932454449b14cee11a94d3674a936d5d7b2',
'expiration_time_seconds': '1000000000000000000000',
'fee_recipient_address': '0x0000000000000000000000000000000000000000',
'maker_address': '0x5409ed021d9299bf6814279a6a1411a7e866a631',
'maker_asset_amount': '1000000000000000000',
'maker_asset_data': '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c',
'maker_fee': '0',
'salt': '2362734632784682376287462',
'sender_address': '0x0000000000000000000000000000000000000000',
'taker_address': '0x0000000000000000000000000000000000000000',
'taker_asset_amount': '500000000000000000000',
'taker_asset_data': '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa',
'taker_fee': '0'}}]},
'bids': {'records': []}}
""" # noqa: E501 (line too long)
# NOTE: Bug in get_order method.
# Sra_client not deserialzing order from server properly, need fix!
from __future__ import absolute_import
@@ -28,7 +189,7 @@ from sra_client.models.relayer_api_asset_data_trade_info_schema import (
from sra_client.models.relayer_api_error_response_schema import (
RelayerApiErrorResponseSchema,
)
from sra_client.models.relayer_api_error_response_schema_validation_errors import (
from sra_client.models.relayer_api_error_response_schema_validation_errors import ( # noqa: E501 (line too long)
RelayerApiErrorResponseSchemaValidationErrors,
)
from sra_client.models.relayer_api_fee_recipients_response_schema import (
@@ -44,7 +205,7 @@ from sra_client.models.relayer_api_order_schema import RelayerApiOrderSchema
from sra_client.models.relayer_api_orderbook_response_schema import (
RelayerApiOrderbookResponseSchema,
)
from sra_client.models.relayer_api_orders_channel_subscribe_payload_schema import (
from sra_client.models.relayer_api_orders_channel_subscribe_payload_schema import ( # noqa: E501 (line too long)
RelayerApiOrdersChannelSubscribePayloadSchema,
)
from sra_client.models.relayer_api_orders_channel_subscribe_schema import (

File diff suppressed because it is too large Load Diff

View File

@@ -207,8 +207,7 @@ class ApiClient(object):
If obj is None, return None.
If obj is str, int, long, float, bool, return directly.
If obj is datetime.datetime, datetime.date
convert to string in iso8601 format.
If obj is datetime.datetime, datetime.date convert to string in iso8601 format.
If obj is list, sanitize each element in the list.
If obj is dict, return the dict.
If obj is OpenAPI model, return the properties dict.

View File

@@ -0,0 +1 @@
"""Test for sra_client."""

View File

@@ -1,3 +1,4 @@
"""Test the default api client"""
# coding: utf-8
@@ -5,30 +6,92 @@ from __future__ import absolute_import
import unittest
import sra_client
from sra_client.api.default_api import DefaultApi # noqa: E501
from sra_client.models.relayer_api_asset_data_pairs_response_schema import (
RelayerApiAssetDataPairsResponseSchema
)
from sra_client.rest import ApiException
from sra_client import ApiClient, Configuration
from sra_client.api import DefaultApi
class TestDefaultApi(unittest.TestCase):
"""DefaultApi unit test stubs"""
def setUp(self):
self.api = sra_client.api.default_api.DefaultApi() # noqa: E501
config = Configuration()
config.host = "http://localhost:3000"
self.api = DefaultApi(ApiClient(config))
def tearDown(self):
pass
# pylint: disable=too-many-locals
def test_get_asset_pairs(self):
"""Test case for get_asset_pairs
"""
expected = RelayerApiAssetDataPairsResponseSchema([])
expected = {
"records": [
{
"assetDataA": {
"assetData": "0xf47261b0000000000000000000000000"
"d0a1e359811322d97991e03f863a0c30c2cf029c",
"maxAmount": "115792089237316195423570985008687907853"
"269984665640564039457584007913129639936",
"minAmount": "0",
"precision": 18,
},
"assetDataB": {
"assetData": "0xf47261b0000000000000000000000000"
"2002d3812f58e35f0ea1ffbf80a75a38c32175fa",
"maxAmount": "115792089237316195423570985008687907853"
"269984665640564039457584007913129639936",
"minAmount": "0",
"precision": 18,
},
}
]
}
actual = self.api.get_asset_pairs()
self.assertEqual(actual, expected)
acutal_asset_data_a = actual.records[0]["assetDataA"]["assetData"]
expected_asset_data_a = expected["records"][0]["assetDataA"][
"assetData"
]
self.assertEqual(acutal_asset_data_a, expected_asset_data_a)
acutal_max_amount_a = actual.records[0]["assetDataA"]["maxAmount"]
expected_max_amount_a = expected["records"][0]["assetDataA"][
"maxAmount"
]
self.assertEqual(acutal_max_amount_a, expected_max_amount_a)
acutal_min_amount_a = actual.records[0]["assetDataA"]["minAmount"]
expected_min_amount_a = expected["records"][0]["assetDataA"][
"minAmount"
]
self.assertEqual(acutal_min_amount_a, expected_min_amount_a)
acutal_precision_a = actual.records[0]["assetDataA"]["precision"]
expected_precision_a = expected["records"][0]["assetDataA"][
"precision"
]
self.assertEqual(acutal_precision_a, expected_precision_a)
acutal_asset_data_b = actual.records[0]["assetDataB"]["assetData"]
expected_asset_data_b = expected["records"][0]["assetDataB"][
"assetData"
]
self.assertEqual(acutal_asset_data_b, expected_asset_data_b)
acutal_max_amount_b = actual.records[0]["assetDataB"]["maxAmount"]
expected_max_amount_b = expected["records"][0]["assetDataB"][
"maxAmount"
]
self.assertEqual(acutal_max_amount_b, expected_max_amount_b)
acutal_min_amount_b = actual.records[0]["assetDataB"]["minAmount"]
expected_min_amount_b = expected["records"][0]["assetDataB"][
"minAmount"
]
self.assertEqual(acutal_min_amount_b, expected_min_amount_b)
acutal_precision_b = actual.records[0]["assetDataB"]["precision"]
expected_precision_b = expected["records"][0]["assetDataB"][
"precision"
]
self.assertEqual(acutal_precision_b, expected_precision_b)
if __name__ == "__main__":