Python contract demo, with lots of refactoring (#1485)

* Refine Order for Web3 compat. & add conversions

Changed some of the fields in the Order class so that it can be passed
to our contracts via Web3.

Added conversion utilities so that an Order can be easily converted to
and from a JSON-compatible dict (specifically by encoding/decoding the
`bytes` fields), to facilitate validation against the JSON schema.

Also modified JSON order schema to accept integers in addition to
stringified integers.

* Fixes for json_schemas

Has-types indicator file, py.typed, was not being included in package.

Schemas were not being properly gathered into package installation.

* Add test/demo of Exchange.getOrderInfo()

* web3 bug workaround

* Fix problem packaging contract artifacts

* Move contract addresses to their own package

* Move contract artifacts to their own package

* Add scripts to install, test & lint all components

* prettierignore files in local python dev env

* Correct missing coverage analysis for sra_client

* CI cache lint: don't save, re-use from test-python

* tag hacks as hacks

* correct merge mistake

* remove local strip_0x() in favor of eth_utils

* remove json schemas from old order_utils location

* correct merge mistake

* doctest json schemas via command-line, not code
This commit is contained in:
F. Eugene Aumson
2019-01-09 09:58:29 -05:00
committed by GitHub
parent 5b7eff217e
commit aa5af04447
136 changed files with 8274 additions and 178 deletions

View File

@@ -0,0 +1,13 @@
{
"domain": "0x-contract-demo-py",
"build_command": "python setup.py build_sphinx",
"upload_directory": "build/docs/html",
"index_key": "index.html",
"error_key": "index.html",
"trailing_slashes": true,
"cache": 3600,
"aws_profile": "default",
"aws_region": "us-east-1",
"cdn": false,
"dns_configured": true
}

View File

@@ -0,0 +1,39 @@
## 0x-contract-demo
A demonstration of calling 0x smart contracts from Python.
Read the [documentation](http://0x-contract-demo-py.s3-website-us-east-1.amazonaws.com/)
## Contributing
We welcome improvements and fixes from the wider community! To report bugs within this package, please create an issue in this repository.
Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
### Install Code and Dependencies
Ensure that you have installed Python >=3.6 and Docker. Then:
```bash
pip install -e .[dev]
```
### Test
Tests depend on a running ganache instance with the 0x contracts deployed in it. For convenience, a docker container is provided that has ganache-cli and a snapshot containing the necessary contracts. A shortcut is provided to run that docker container: `./setup.py ganache`. With that running, the tests can be run with `./setup.py test`.
### Clean
`./setup.py clean --all`
### Lint
`./setup.py lint`
### Build Documentation
`./setup.py build_sphinx`
### More
See `./setup.py --help-commands` for more info.

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python
"""setuptools module for 0x-contract-demo package."""
import distutils.command.build_py
from distutils.command.clean import clean
import subprocess # nosec
from shutil import rmtree
from os import environ, path
from sys import argv
from setuptools import setup
from setuptools.command.test import test as TestCommand
class TestCommandExtension(TestCommand):
"""Run pytest tests."""
def run_tests(self):
"""Invoke pytest."""
import pytest
exit(pytest.main())
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 setup.py".split(),
# style guide checker (formerly pep8):
"pycodestyle test setup.py".split(),
# docstring style checker:
"pydocstyle test setup.py".split(),
# static type checker:
"mypy test setup.py".split(),
# security issue checker:
"bandit -r ./setup.py".split(),
# general linter:
"pylint test setup.py".split(),
# pylint takes relatively long to run, so it runs last, to enable
# fast failures.
]
# tell mypy where to find interface stubs for 3rd party libs
environ["MYPYPATH"] = path.join(
path.dirname(path.realpath(argv[0])), "stubs"
)
for lint_command in lint_commands:
print(
"Running lint command `", " ".join(lint_command).strip(), "`"
)
subprocess.check_call(lint_command) # nosec
class CleanCommandExtension(clean):
"""Custom command to do custom cleanup."""
def run(self):
"""Run the regular clean, followed by our custom commands."""
super().run()
rmtree(".mypy_cache", ignore_errors=True)
rmtree(".tox", ignore_errors=True)
rmtree(".pytest_cache", ignore_errors=True)
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 PublishDocsCommand(distutils.command.build_py.build_py):
"""Custom command to publish docs to S3."""
description = (
"Publish docs to "
+ "http://0x-contract-addresses-py.s3-website-us-east-1.amazonaws.com/"
)
def run(self):
"""Run npm package `discharge` to build & upload docs."""
subprocess.check_call("discharge deploy".split()) # nosec
setup(
name="0x-contract-demo",
version="1.0.0",
description="Demonstration of calling 0x contracts",
url=(
"https://github.com/0xProject/0x-monorepo/tree/development"
+ "/python-packages/contract_demo"
),
author="F. Eugene Aumson",
author_email="feuGeneA@users.noreply.github.com",
cmdclass={
"clean": CleanCommandExtension,
"lint": LintCommand,
"test": TestCommandExtension,
"ganache": GanacheCommand,
"publish_docs": PublishDocsCommand,
},
install_requires=[
"0x-contract-addresses",
"0x-contract-artifacts",
"0x-order-utils",
"0x-web3", # TEMPORARY! pending resolution of our web3.py PR#1147
"mypy_extensions",
],
extras_require={
"dev": [
"bandit",
"black",
"coverage",
"coveralls",
"mypy",
"mypy_extensions",
"pycodestyle",
"pydocstyle",
"pylint",
"pytest",
"sphinx",
"tox",
]
},
python_requires=">=3.6, <4",
license="Apache 2.0",
zip_safe=False, # required per mypy
command_options={
"build_sphinx": {
"source_dir": ("setup.py", "test"),
"build_dir": ("setup.py", "build/docs"),
}
},
)

View File

@@ -0,0 +1,7 @@
from distutils.core import Command
class clean(Command):
def initialize_options(self: clean) -> None: ...
def finalize_options(self: clean) -> None: ...
def run(self: clean) -> None: ...
...

View File

@@ -0,0 +1,7 @@
from distutils.core import Command
class clean(Command):
def initialize_options(self: clean) -> None: ...
def finalize_options(self: clean) -> None: ...
def run(self: clean) -> None: ...
...

View File

@@ -0,0 +1,4 @@
from typing import Union
def to_checksum_address(value: Union[str, bytes]) -> str:
...

View File

@@ -0,0 +1,8 @@
from distutils.dist import Distribution
from typing import Any, List
def setup(**attrs: Any) -> Distribution: ...
class Command: ...
def find_packages(where: str) -> List[str]: ...

View File

@@ -0,0 +1,3 @@
from setuptools import Command
class test(Command): ...

View File

@@ -0,0 +1,2 @@
class Web3:
...

View File

@@ -0,0 +1,3 @@
class Contract:
def call(self): ...
...

View File

@@ -0,0 +1 @@
"""Demonstrations of calling 0x smart contracts."""

View File

@@ -0,0 +1,54 @@
"""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-contract-demo"
# pylint: disable=redefined-builtin
copyright = "2018, ZeroEx, Intl."
author = "F. Eugene Aumson"
version = pkg_resources.get_distribution("0x-contract-demo").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",
]
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 = "contract_demopydoc"
# -- Extension configuration:
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None}

View File

@@ -0,0 +1,18 @@
.. source for the sphinx-generated build/docs/web/index.html
Python demo of 0x Smart Contracts
=================================
.. toctree::
:maxdepth: 2
:caption: Contents:
.. automodule:: test.test_exchange
:members:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -0,0 +1,65 @@
"""Test calling methods on the Exchange contract."""
from eth_utils import to_checksum_address
from web3 import Web3
from web3.utils import datatypes
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
import zero_ex.contract_artifacts
from zero_ex.json_schemas import assert_valid
from zero_ex.order_utils import (
Order,
OrderInfo,
order_to_jsdict,
generate_order_hash_hex,
)
def test_get_order_info():
"""Demonstrate Exchange.getOrderInfo()."""
order: Order = {
"makerAddress": "0x0000000000000000000000000000000000000000",
"takerAddress": "0x0000000000000000000000000000000000000000",
"feeRecipientAddress": "0x0000000000000000000000000000000000000000",
"senderAddress": "0x0000000000000000000000000000000000000000",
"makerAssetAmount": 1000000000000000000,
"takerAssetAmount": 1000000000000000000,
"makerFee": 0,
"takerFee": 0,
"expirationTimeSeconds": 12345,
"salt": 12345,
"makerAssetData": (b"\x00") * 20,
"takerAssetData": (b"\x00") * 20,
}
web3_instance = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))
# false positive from pylint: disable=no-member
contract_address = NETWORK_TO_ADDRESSES[
NetworkId(int(web3_instance.net.version))
].exchange
assert_valid(
order_to_jsdict(order, exchange_address=contract_address),
"/orderSchema",
)
# false positive from pylint: disable=no-member
exchange: datatypes.Contract = web3_instance.eth.contract(
address=to_checksum_address(contract_address),
abi=zero_ex.contract_artifacts.abi_by_name("Exchange"),
)
order_info = OrderInfo(*exchange.call().getOrderInfo(order))
assert isinstance(order_info.order_status, int)
assert order_info.order_status == 4
assert isinstance(order_info.order_hash, bytes)
assert order_info.order_hash.hex() == generate_order_hash_hex(
order,
exchange_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange,
)
assert isinstance(order_info.order_taker_asset_filled_amount, int)
assert order_info.order_taker_asset_filled_amount == 0