aboutsummaryrefslogtreecommitdiffstats
path: root/python-packages/order_utils
diff options
context:
space:
mode:
Diffstat (limited to 'python-packages/order_utils')
-rw-r--r--python-packages/order_utils/LICENSE13
-rw-r--r--python-packages/order_utils/README.md45
-rwxr-xr-xpython-packages/order_utils/setup.py68
-rw-r--r--python-packages/order_utils/src/conf.py4
-rw-r--r--python-packages/order_utils/src/doc_static/.gitkeep0
-rw-r--r--python-packages/order_utils/src/doc_templates/.gitkeep0
-rw-r--r--python-packages/order_utils/src/index.rst8
-rw-r--r--python-packages/order_utils/src/zero_ex/__init__.py1
-rw-r--r--python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py15
-rw-r--r--python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py77
-rw-r--r--python-packages/order_utils/test/test_asset_data_utils.py39
-rw-r--r--python-packages/order_utils/tox.ini13
12 files changed, 260 insertions, 23 deletions
diff --git a/python-packages/order_utils/LICENSE b/python-packages/order_utils/LICENSE
new file mode 100644
index 000000000..9096fefaa
--- /dev/null
+++ b/python-packages/order_utils/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2017 ZeroEx Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. \ No newline at end of file
diff --git a/python-packages/order_utils/README.md b/python-packages/order_utils/README.md
new file mode 100644
index 000000000..a266694db
--- /dev/null
+++ b/python-packages/order_utils/README.md
@@ -0,0 +1,45 @@
+## 0x-order-utils
+
+0x order-related utilities for those developing on top of 0x protocol.
+
+Read the [documentation](https://0x.readthedocs.io/projects/order-utils/en/latest/)
+
+## Installing
+
+```bash
+pip install 0x-order-utils
+```
+
+## 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 Python >=3.6 installed, then:
+
+```bash
+pip install -e .[dev]
+```
+
+### Test
+
+`./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.
diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py
index 1a094cfe1..22a5f4c41 100755
--- a/python-packages/order_utils/setup.py
+++ b/python-packages/order_utils/setup.py
@@ -4,7 +4,7 @@
import subprocess # nosec
from shutil import rmtree
-from os import environ, path, remove, walk
+from os import environ, path
from sys import argv
from distutils.command.clean import clean
@@ -20,13 +20,15 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
- pytest.main()
+ exit(pytest.main())
# pylint: disable=too-many-ancestors
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 = [
@@ -73,31 +75,64 @@ class CleanCommandExtension(clean):
def run(self):
"""Run the regular clean, followed by our custom commands."""
super().run()
- rmtree("build", ignore_errors=True)
+ rmtree("dist", ignore_errors=True)
rmtree(".mypy_cache", ignore_errors=True)
rmtree(".tox", ignore_errors=True)
rmtree(".pytest_cache", ignore_errors=True)
rmtree("src/order_utils.egg-info", ignore_errors=True)
- # delete all .pyc files
- for root, _, files in walk("."):
- for file in files:
- (_, extension) = path.splitext(file)
- if extension == ".pyc":
- remove(path.join(root, file))
+
+
+# pylint: disable=too-many-ancestors
+class TestPublishCommand(distutils.command.build_py.build_py):
+ """Custom command to publish to test.pypi.org."""
+
+ description = (
+ "Publish dist/* to test.pypi.org. Run sdist & bdist_wheel first."
+ )
+
+ def run(self):
+ """Run twine to upload to test.pypi.org."""
+ subprocess.check_call( # nosec
+ (
+ "twine upload --repository-url https://test.pypi.org/legacy/"
+ + " --verbose dist/*"
+ ).split()
+ )
+
+
+# pylint: disable=too-many-ancestors
+class PublishCommand(distutils.command.build_py.build_py):
+ """Custom command to publish to pypi.org."""
+
+ description = "Publish dist/* to pypi.org. Run sdist & bdist_wheel first."
+
+ def run(self):
+ """Run twine to upload to pypi.org."""
+ subprocess.check_call("twine upload dist/*".split()) # nosec
+
+
+with open("README.md", "r") as file_handle:
+ README_MD = file_handle.read()
setup(
- name="order_utils",
- version="1.0.0",
+ name="0x-order-utils",
+ version="0.1.0",
description="Order utilities for 0x applications",
+ long_description=README_MD,
+ long_description_content_type="text/markdown",
+ url="https://github.com/0xproject/0x-monorepo/python-packages/order_utils",
author="F. Eugene Aumson",
+ author_email="feuGeneA@users.noreply.github.com",
cmdclass={
"clean": CleanCommandExtension,
"lint": LintCommand,
"test": TestCommandExtension,
+ "test_publish": TestPublishCommand,
+ "publish": PublishCommand,
},
include_package_data=True,
- install_requires=["eth-abi", "web3"],
+ install_requires=["eth-abi", "mypy_extensions", "web3"],
extras_require={
"dev": [
"bandit",
@@ -112,6 +147,7 @@ setup(
"pytest",
"sphinx",
"tox",
+ "twine",
]
},
python_requires=">=3.6, <4",
@@ -121,9 +157,10 @@ setup(
keywords=(
"ethereum cryptocurrency 0x decentralized blockchain dex exchange"
),
- packages=["zero_ex.order_utils"],
+ namespace_packages=["zero_ex"],
+ packages=["zero_ex.order_utils", "zero_ex.dev_utils"],
classifiers=[
- "Development Status :: 1 - Planning",
+ "Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Financial and Insurance Industry",
"License :: OSI Approved :: Apache Software License",
@@ -133,7 +170,10 @@ setup(
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
+ "Topic :: Internet :: WWW/HTTP",
"Topic :: Office/Business :: Financial",
+ "Topic :: Other/Nonlisted Topic",
+ "Topic :: Security :: Cryptography",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities",
],
diff --git a/python-packages/order_utils/src/conf.py b/python-packages/order_utils/src/conf.py
index e74a29d00..606cd3b2a 100644
--- a/python-packages/order_utils/src/conf.py
+++ b/python-packages/order_utils/src/conf.py
@@ -8,11 +8,11 @@ from typing import List
# pylint: disable=invalid-name
# because these variables are not named in upper case, as globals should be.
-project = "order_utils.py"
+project = "0x-order-utils"
# pylint: disable=redefined-builtin
copyright = "2018, ZeroEx, Intl."
author = "F. Eugene Aumson"
-version = "" # The short X.Y version
+version = "0.1.0" # The short X.Y version
release = "" # The full version, including alpha/beta/rc tags
extensions = [
diff --git a/python-packages/order_utils/src/doc_static/.gitkeep b/python-packages/order_utils/src/doc_static/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python-packages/order_utils/src/doc_static/.gitkeep
diff --git a/python-packages/order_utils/src/doc_templates/.gitkeep b/python-packages/order_utils/src/doc_templates/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python-packages/order_utils/src/doc_templates/.gitkeep
diff --git a/python-packages/order_utils/src/index.rst b/python-packages/order_utils/src/index.rst
index e09abcca2..22d5b0ef9 100644
--- a/python-packages/order_utils/src/index.rst
+++ b/python-packages/order_utils/src/index.rst
@@ -1,7 +1,7 @@
.. source for the sphinx-generated build/docs/web/index.html
-order_utils.py
-==============
+Python zero_ex.order_utils
+==========================
.. toctree::
:maxdepth: 2
@@ -15,7 +15,9 @@ order_utils.py
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData
-See source for properties. Sphinx does not easily generate class property docs; pull requests welcome.
+.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC721AssetData
+
+See source for class properties. Sphinx does not easily generate class property docs; pull requests welcome.
Indices and tables
==================
diff --git a/python-packages/order_utils/src/zero_ex/__init__.py b/python-packages/order_utils/src/zero_ex/__init__.py
index c3ed1562a..e90d833db 100644
--- a/python-packages/order_utils/src/zero_ex/__init__.py
+++ b/python-packages/order_utils/src/zero_ex/__init__.py
@@ -1 +1,2 @@
"""0x Python API."""
+__import__("pkg_resources").declare_namespace(__name__)
diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py
index 745d014e6..a100da567 100644
--- a/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py
+++ b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py
@@ -31,3 +31,18 @@ def assert_is_list(value: Any, name: str) -> None:
f"expected variable '{name}', with value {str(value)}, to have"
+ f" type 'list', not '{type(value).__name__}'"
)
+
+
+def assert_is_int(value: Any, name: str) -> None:
+ """If :param value: isn't of type int, raise a TypeError.
+
+ >>> try: assert_is_int('asdf', 'var')
+ ... except TypeError as type_error: print(str(type_error))
+ ...
+ expected variable 'var', with value asdf, to have type 'int', not 'str'
+ """
+ if not isinstance(value, int):
+ raise TypeError(
+ f"expected variable '{name}', with value {str(value)}, to have"
+ + f" type 'int', not '{type(value).__name__}'"
+ )
diff --git a/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py
index 451de39af..e6f9a07c1 100644
--- a/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py
+++ b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py
@@ -5,10 +5,11 @@ from mypy_extensions import TypedDict
import eth_abi
from zero_ex.dev_utils import abi_utils
-from zero_ex.dev_utils.type_assertions import assert_is_string
+from zero_ex.dev_utils.type_assertions import assert_is_string, assert_is_int
ERC20_ASSET_DATA_BYTE_LENGTH = 36
+ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53
SELECTOR_LENGTH = 10
@@ -19,6 +20,14 @@ class ERC20AssetData(TypedDict):
token_address: str
+class ERC721AssetData(TypedDict):
+ """Object interface to ERC721 asset data."""
+
+ asset_proxy_id: str
+ token_address: str
+ token_id: int
+
+
def encode_erc20_asset_data(token_address: str) -> str:
"""Encode an ERC20 token address into an asset data string.
@@ -39,7 +48,7 @@ def encode_erc20_asset_data(token_address: str) -> str:
def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
# docstring considered all one line by pylint: disable=line-too-long
- """Decode an ERC20 assetData hex string.
+ """Decode an ERC20 asset data hex string.
:param asset_data: String produced by prior call to encode_erc20_asset_data()
@@ -55,7 +64,7 @@ def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
+ f" Got {str(len(asset_data))}."
)
- asset_proxy_id: str = asset_data[0:10]
+ asset_proxy_id: str = asset_data[0:SELECTOR_LENGTH]
if asset_proxy_id != abi_utils.method_id("ERC20Token", ["address"]):
raise ValueError(
"Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be"
@@ -70,3 +79,65 @@ def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
)[0]
return {"asset_proxy_id": asset_proxy_id, "token_address": token_address}
+
+
+def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
+ # docstring considered all one line by pylint: disable=line-too-long
+ """Encode an ERC721 asset data hex string.
+
+ :param token_address: the ERC721 token's contract address.
+ :param token_id: the identifier of the asset's instance of the token.
+ :rtype: hex encoded asset data string, usable in the makerAssetData or
+ takerAssetData fields in a 0x order.
+
+ >>> encode_erc721_asset_data('0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 1)
+ '0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001'
+ """ # noqa: E501 (line too long)
+ assert_is_string(token_address, "token_address")
+ assert_is_int(token_id, "token_id")
+
+ return (
+ "0x"
+ + abi_utils.simple_encode(
+ "ERC721Token(address,uint256)", token_address, token_id
+ ).hex()
+ )
+
+
+def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData:
+ # docstring considered all one line by pylint: disable=line-too-long
+ """Decode an ERC721 asset data hex string.
+
+ >>> decode_erc721_asset_data('0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001')
+ {'asset_proxy_id': '0x02571792', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 'token_id': 1}
+ """ # noqa: E501 (line too long)
+ assert_is_string(asset_data, "asset_data")
+
+ if len(asset_data) < ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH:
+ raise ValueError(
+ "Could not decode ERC721 Asset Data. Expected length of encoded"
+ + f"data to be at least {ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH}. "
+ + f"Got {len(asset_data)}."
+ )
+
+ asset_proxy_id: str = asset_data[0:SELECTOR_LENGTH]
+ # prefer `black` formatting. pylint: disable=C0330
+ if asset_proxy_id != abi_utils.method_id(
+ "ERC721Token", ["address", "uint256"]
+ ):
+ raise ValueError(
+ "Could not decode ERC721 Asset Data. Expected Asset Proxy Id to be"
+ + f" ERC721 ("
+ + f"{abi_utils.method_id('ERC721Token', ['address', 'uint256'])}"
+ + f"), but got {asset_proxy_id}"
+ )
+
+ (token_address, token_id) = eth_abi.decode_abi(
+ ["address", "uint256"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
+ )
+
+ return {
+ "asset_proxy_id": asset_proxy_id,
+ "token_address": token_address,
+ "token_id": token_id,
+ }
diff --git a/python-packages/order_utils/test/test_asset_data_utils.py b/python-packages/order_utils/test/test_asset_data_utils.py
index eeada5873..079368714 100644
--- a/python-packages/order_utils/test/test_asset_data_utils.py
+++ b/python-packages/order_utils/test/test_asset_data_utils.py
@@ -3,9 +3,12 @@
import pytest
from zero_ex.order_utils.asset_data_utils import (
- encode_erc20_asset_data,
decode_erc20_asset_data,
+ decode_erc721_asset_data,
+ encode_erc20_asset_data,
+ encode_erc721_asset_data,
ERC20_ASSET_DATA_BYTE_LENGTH,
+ ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH,
)
@@ -33,3 +36,37 @@ def test_decode_erc20_asset_data_invalid_proxy_id():
decode_erc20_asset_data(
"0xffffffff" + (" " * ERC20_ASSET_DATA_BYTE_LENGTH)
)
+
+
+def test_encode_erc721_asset_data_type_error_on_token_address():
+ """Test that passing a non-string for token_address raises a TypeError."""
+ with pytest.raises(TypeError):
+ encode_erc721_asset_data(123, 123)
+
+
+def test_encode_erc721_asset_data_type_error_on_token_id():
+ """Test that passing a non-int for token_id raises a TypeError."""
+ with pytest.raises(TypeError):
+ encode_erc721_asset_data("asdf", "asdf")
+
+
+def test_decode_erc721_asset_data_type_error():
+ """Test that passing a non-string for asset_data raises a TypeError."""
+ with pytest.raises(TypeError):
+ decode_erc721_asset_data(123)
+
+
+def test_decode_erc721_asset_data_with_asset_data_too_short():
+ """Test that passing in too short of a string raises a ValueError."""
+ with pytest.raises(ValueError):
+ decode_erc721_asset_data(
+ " " * (ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH - 1)
+ )
+
+
+def test_decode_erc721_asset_data_invalid_proxy_id():
+ """Test that passing in too short of a string raises a ValueError."""
+ with pytest.raises(ValueError):
+ decode_erc721_asset_data(
+ "0xffffffff" + " " * (ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH - 1)
+ )
diff --git a/python-packages/order_utils/tox.ini b/python-packages/order_utils/tox.ini
index 1cce32b5f..ba7d55b56 100644
--- a/python-packages/order_utils/tox.ini
+++ b/python-packages/order_utils/tox.ini
@@ -10,3 +10,16 @@ envlist = py37
commands =
pip install -e .[dev]
python setup.py test
+
+[testenv:run_tests_against_test_deployment]
+commands =
+ # install dependencies from real PyPI
+ pip install eth-abi mypy_extensions web3 pytest
+ # install package-under-test from test PyPI
+ pip install --index-url https://test.pypi.org/legacy/ 0x-order-utils
+ pytest test
+
+[testenv:run_tests_against_deployment]
+commands =
+ pip install 0x-order-utils
+ pytest test