diff --git a/README.md b/README.md index 4002ad2..80a129c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ From Source: ``` git clone https://github.com/steemit/steem-python.git cd steem-python -python3 setup.py install # python setup.py install for 2.7 +pip install build +python -m build +pip install . ``` ## Homebrew Build Prereqs diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8fe2f47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 0634abb..6c48413 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -description-file=README.md +description_file=README.md [tool:pytest] norecursedirs=dist docs build deploy diff --git a/setup.py b/setup.py index d7877ab..88e4a74 100644 --- a/setup.py +++ b/setup.py @@ -1,175 +1,96 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -# Note: To use the 'upload' functionality of this file, you must: -# $ pip install twine +""" +WARNING: + Direct invocation of setup.py is deprecated. + Please use "pip install ." or "python -m build" (with a proper pyproject.toml) + to build and install this package. +""" import io import os -import sys -from shutil import rmtree - -from setuptools import find_packages, setup, Command -from setuptools.command.test import test as TestCommand - -# Package meta-data. -NAME = 'steem' -DESCRIPTION = 'Official python steem library.' -URL = 'https://github.com/steemit/steem-python' -EMAIL = 'john@steemit.com' -AUTHOR = 'Steemit' - -# What packages are required for this module to be executed? -REQUIRED = [ - 'appdirs', - 'certifi', - 'ecdsa>=0.13', - 'funcy', - 'futures ; python_version < "3.0.0"', - 'future', - 'langdetect', - 'prettytable', - 'pycrypto>=1.9.1', - 'pylibscrypt>=1.6.1', - 'scrypt>=0.8.0', - 'toolz', - 'ujson', - 'urllib3', - 'voluptuous', - 'w3lib' -] -TEST_REQUIRED = [ - 'pep8', - 'pytest', - 'pytest-pylint ; python_version >= "3.4.0"', - 'pytest-xdist', - 'pytest-runner', - 'pytest-pep8', - 'pytest-cov', - 'yapf', - 'autopep8' -] - -BUILD_REQUIRED = [ - 'twine', - 'pypandoc', - 'recommonmark' - 'wheel', - 'setuptools', - 'sphinx', - 'sphinx_rtd_theme' -] -# The rest you shouldn't have to touch too much :) -# ------------------------------------------------ -# Except, perhaps the License and Trove Classifiers! -# If you do change the License, remember to change the Trove Classifier for that! +import setuptools here = os.path.abspath(os.path.dirname(__file__)) -# Import the README and use it as the long-description. -# Note: this will only work if 'README.rst' is present in your MANIFEST.in file! -# with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: -# long_description = '\n' + f.read() - - -class PyTest(TestCommand): - user_options = [('pytest-args=', 'a', "Arguments to pass into py.test")] - - def initialize_options(self): - TestCommand.initialize_options(self) - try: - from multiprocessing import cpu_count - self.pytest_args = ['-n', str(cpu_count()), '--boxed'] - except (ImportError, NotImplementedError): - self.pytest_args = ['-n', '1', '--boxed'] - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - import pytest - - errno = pytest.main(self.pytest_args) - sys.exit(errno) - - -class UploadCommand(Command): - """Support setup.py upload.""" - - description = 'Build and publish the package.' - user_options = [] - - @staticmethod - def status(s): - """Prints things in bold.""" - print('\033[1m{0}\033[0m'.format(s)) - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - try: - self.status('Removing previous builds…') - rmtree(os.path.join(here, 'dist')) - except OSError: - pass - - self.status('Building Source and Wheel (universal) distribution…') - os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) - - self.status('Uploading the package to PyPi via Twine…') - os.system('twine upload dist/*') - - sys.exit() - - -# Where the magic happens: -setup( - name=NAME, - version='1.0.2', - description=DESCRIPTION, - keywords=['steem', 'steemit', 'cryptocurrency', 'blockchain'], - # long_description=long_description, - author=AUTHOR, - author_email=EMAIL, - url=URL, - packages=find_packages(exclude=('tests','scripts')), +# Read the long description from the README file (if available) +try: + with io.open(os.path.join(here, "README.rst"), encoding="utf-8") as f: + long_description = f.read() +except FileNotFoundError: + long_description = "Official python steem library." + +setuptools.setup( + name="steem", + version="1.0.2", + author="Steemit", + author_email="john@steemit.com", + description="Official python steem library.", + long_description=long_description, + long_description_content_type="text/x-rst", + url="https://github.com/steemit/steem-python", + packages=setuptools.find_packages(exclude=("tests", "scripts")), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + python_requires=">=3.5", + install_requires=[ + "appdirs", + "certifi", + "ecdsa>=0.13", + "funcy", + 'futures; python_version < "3.0.0"', + "future", + "langdetect", + "pick>=2.4.0", + "prettytable", + "pycryptodome>=3.20.0", + "pylibscrypt>=1.6.1", + "scrypt>=0.8.0", + "toolz", + "ujson", + "urllib3", + "voluptuous", + "w3lib", + ], entry_points={ - 'console_scripts': [ - 'piston=steem.cli:legacyentry', - 'steempy=steem.cli:legacyentry', - 'steemtail=steem.cli:steemtailentry', - ], + "console_scripts": [ + "piston=steem.cli:legacyentry", + "steempy=steem.cli:legacyentry", + "steemtail=steem.cli:steemtailentry", + ], }, - install_requires=REQUIRED, extras_require={ - 'dev': TEST_REQUIRED + BUILD_REQUIRED, - 'build': BUILD_REQUIRED, - 'test': TEST_REQUIRED - }, - tests_require=TEST_REQUIRED, - include_package_data=True, - license='MIT', - - classifiers=[ - # Trove classifiers - # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Development Status :: 4 - Beta' - ], - # $ setup.py publish support. - cmdclass={ - 'upload': UploadCommand, - 'test': PyTest + "dev": [ + "pytest", + "pytest-cov", + "pytest-xdist", + "autopep8", + "yapf", + "twine", + "pypandoc", + "recommonmark", + "wheel", + "setuptools", + "sphinx", + "sphinx_rtd_theme", + ], + "test": [ + "pytest", + "pytest-cov", + "pytest-xdist", + "autopep8", + "yapf", + ], }, ) diff --git a/steem/utils.py b/steem/utils.py index 9da991e..a42bacb 100755 --- a/steem/utils.py +++ b/steem/utils.py @@ -24,9 +24,7 @@ # https://github.com/matiasb/python-unidiff/blob/master/unidiff/constants.py#L37 # @@ (source offset, length) (target offset, length) @@ (section header) -RE_HUNK_HEADER = re.compile( - r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?\ @@[ ]?(.*)$", - flags=re.MULTILINE) +RE_HUNK_HEADER = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?\ @@[ ]?(.*)$", flags=re.MULTILINE) # ensure deterministec language detection DetectorFactory.seed = 0 @@ -82,34 +80,29 @@ def chunkify(iterable, chunksize=10000): def ensure_decoded(thing): if not thing: - logger.debug('ensure_decoded thing is logically False') + logger.debug("ensure_decoded thing is logically False") return None if isinstance(thing, (list, dict)): - logger.debug('ensure_decoded thing is already decoded') + logger.debug("ensure_decoded thing is already decoded") return thing single_encoded_dict = double_encoded_dict = None try: single_encoded_dict = json.loads(thing) if isinstance(single_encoded_dict, dict): - logger.debug('ensure_decoded thing is single encoded dict') + logger.debug("ensure_decoded thing is single encoded dict") return single_encoded_dict elif isinstance(single_encoded_dict, str): - logger.debug('ensure_decoded thing is single encoded str') + logger.debug("ensure_decoded thing is single encoded str") if single_encoded_dict == "": - logger.debug( - 'ensure_decoded thing is single encoded str == ""') + logger.debug('ensure_decoded thing is single encoded str == ""') return None else: double_encoded_dict = json.loads(single_encoded_dict) - logger.debug('ensure_decoded thing is double encoded') + logger.debug("ensure_decoded thing is double encoded") return double_encoded_dict except Exception as e: - extra = dict( - thing=thing, - single_encoded_dict=single_encoded_dict, - double_encoded_dict=double_encoded_dict, - error=e) - logger.error('ensure_decoded error', extra=extra) + extra = dict(thing=thing, single_encoded_dict=single_encoded_dict, double_encoded_dict=double_encoded_dict, error=e) + logger.error("ensure_decoded error", extra=extra) return None @@ -137,31 +130,30 @@ def extract_keys_from_meta(meta, keys): elif isinstance(item, (list, tuple)): extracted.extend(item) else: - logger.warning('unusual item in meta: %s', item) + logger.warning("unusual item in meta: %s", item) return extracted def build_comment_url(parent_permlink=None, author=None, permlink=None): - return '/'.join([parent_permlink, author, permlink]) + return "/".join([parent_permlink, author, permlink]) def canonicalize_url(url, **kwargs): try: canonical_url = w3lib.url.canonicalize_url(url, **kwargs) except Exception as e: - logger.warning('url preparation error', extra=dict(url=url, error=e)) + logger.warning("url preparation error", extra=dict(url=url, error=e)) return None if canonical_url != url: - logger.debug('canonical_url changed %s to %s', url, canonical_url) + logger.debug("canonical_url changed %s to %s", url, canonical_url) try: parsed_url = urlparse(canonical_url) if not parsed_url.scheme and not parsed_url.netloc: - _log = dict( - url=url, canonical_url=canonical_url, parsed_url=parsed_url) - logger.warning('bad url encountered', extra=_log) + _log = dict(url=url, canonical_url=canonical_url, parsed_url=parsed_url) + logger.warning("bad url encountered", extra=_log) return None except Exception as e: - logger.warning('url parse error', extra=dict(url=url, error=e)) + logger.warning("url parse error", extra=dict(url=url, error=e)) return None return canonical_url @@ -172,7 +164,7 @@ def findall_patch_hunks(body=None): def detect_language(text): if not text or len(text) < MIN_TEXT_LENGTH_FOR_DETECTION: - logger.debug('not enough text to perform langdetect') + logger.debug("not enough text to perform langdetect") return None try: return detect(text) @@ -202,7 +194,7 @@ def parse_time(block_time): """Take a string representation of time from the blockchain, and parse it into datetime object. """ - return datetime.strptime(block_time, '%Y-%m-%dT%H:%M:%S') + return datetime.strptime(block_time, "%Y-%m-%dT%H:%M:%S") def time_diff(time1, time2): @@ -210,8 +202,7 @@ def time_diff(time1, time2): def keep_in_dict(obj, allowed_keys=list()): - """ Prune a class or dictionary of all but allowed keys. - """ + """Prune a class or dictionary of all but allowed keys.""" if type(obj) == dict: items = obj.items() else: @@ -221,8 +212,7 @@ def keep_in_dict(obj, allowed_keys=list()): def remove_from_dict(obj, remove_keys=list()): - """ Prune a class or dictionary of specified keys. - """ + """Prune a class or dictionary of specified keys.""" if type(obj) == dict: items = obj.items() else: @@ -244,21 +234,20 @@ def construct_identifier(*args): if len(args) == 1: op = args[0] - author, permlink = op['author'], op['permlink'] + author, permlink = op["author"], op["permlink"] elif len(args) == 2: author, permlink = args else: - raise ValueError( - 'construct_identifier() received unparsable arguments') + raise ValueError("construct_identifier() received unparsable arguments") # remove the @ sign in case it was passed in by the user. - author = author.replace('@', '') + author = author.replace("@", "") fields = dict(author=author, permlink=permlink) return "{author}/{permlink}".format(**fields) -def json_expand(json_op, key_name='json'): - """ Convert a string json object to Python dict in an op. """ +def json_expand(json_op, key_name="json"): + """Convert a string json object to Python dict in an op.""" if type(json_op) == dict and key_name in json_op and json_op[key_name]: try: return update_in(json_op, [key_name], json.loads) @@ -270,9 +259,9 @@ def json_expand(json_op, key_name='json'): def sanitize_permlink(permlink): permlink = permlink.strip() - permlink = re.sub("_|\s|\.", "-", permlink) - permlink = re.sub("[^\w-]", "", permlink) - permlink = re.sub("[^a-zA-Z0-9-]", "", permlink) + permlink = re.sub(r"_|\s|\.", "-", permlink) + permlink = re.sub(r"[^\w-]", "", permlink) + permlink = re.sub(r"[^a-zA-Z0-9-]", "", permlink) permlink = permlink.lower() return permlink @@ -292,53 +281,49 @@ def derive_permlink(title, parent_permlink=None): def resolve_identifier(identifier): # in case the user supplied the @ sign. - identifier = identifier.replace('@', '') + identifier = identifier.replace("@", "") - match = re.match("([\w\-\.]*)/([\w\-]*)", identifier) + match = re.match(r"([\w\-\.]*)/([\w\-]*)", identifier) if not hasattr(match, "group"): raise ValueError("Invalid identifier") return match.group(1), match.group(2) def fmt_time(t): - """ Properly Format Time for permlinks - """ + """Properly Format Time for permlinks""" return datetime.utcfromtimestamp(t).strftime("%Y%m%dt%H%M%S%Z") def fmt_time_string(t): - """ Properly Format Time for permlinks - """ - return datetime.strptime(t, '%Y-%m-%dT%H:%M:%S') + """Properly Format Time for permlinks""" + return datetime.strptime(t, "%Y-%m-%dT%H:%M:%S") def fmt_time_from_now(secs=0): - """ Properly Format Time that is `x` seconds in the future + """Properly Format Time that is `x` seconds in the future - :param int secs: Seconds to go in the future (`x>0`) or the - past (`x<0`) - :return: Properly formated time for Graphene (`%Y-%m-%dT%H:%M:%S`) - :rtype: str + :param int secs: Seconds to go in the future (`x>0`) or the + past (`x<0`) + :return: Properly formated time for Graphene (`%Y-%m-%dT%H:%M:%S`) + :rtype: str """ - return datetime.utcfromtimestamp(time.time() + int(secs)).strftime( - '%Y-%m-%dT%H:%M:%S') + return datetime.utcfromtimestamp(time.time() + int(secs)).strftime("%Y-%m-%dT%H:%M:%S") def env_unlocked(): - """ Check if wallet passphrase is provided as ENV variable. """ - return os.getenv('UNLOCK', False) + """Check if wallet passphrase is provided as ENV variable.""" + return os.getenv("UNLOCK", False) # todo remove these def strfage(time, fmt=None): - """ Format time/age - """ + """Format time/age""" if not hasattr(time, "days"): # dirty hack now = datetime.utcnow() if isinstance(time, str): - time = datetime.strptime(time, '%Y-%m-%dT%H:%M:%S') - time = (now - time) + time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S") + time = now - time d = {"days": time.days} d["hours"], rem = divmod(time.seconds, 3600) @@ -355,8 +340,7 @@ def strfage(time, fmt=None): def strfdelta(tdelta, fmt): - """ Format time/age - """ + """Format time/age""" if not tdelta or not hasattr(tdelta, "days"): # dirty hack return None @@ -367,7 +351,7 @@ def strfdelta(tdelta, fmt): def is_valid_account_name(name): - return re.match('^[a-z][a-z0-9\-.]{2,15}$', name) + return re.match(r"^[a-z][a-z0-9\-.]{2,15}$", name) def compat_compose_dictionary(dictionary, **kwargs): @@ -394,20 +378,18 @@ def compat_json(data, ignore_dicts=False): """ # if this is a unicode string, return its string representation if isinstance(data, unicode): - return data.encode('utf-8') + return data.encode("utf-8") # if this is a list of values, return list of byte-string values if isinstance(data, list): return [compat_json(item, ignore_dicts=True) for item in data] # if this is a dictionary, return dictionary of byte-string keys and values # but only if we haven't already byte-string it if isinstance(data, dict) and not ignore_dicts: - return { - compat_json(key, ignore_dicts=True): compat_json(value, ignore_dicts=True) - for key, value in data.iteritems() - } + return {compat_json(key, ignore_dicts=True): compat_json(value, ignore_dicts=True) for key, value in data.iteritems()} # if it's anything else, return it in its original form return data + def compat_bytes(item, encoding=None): """ This method is required because Python 2.7 `bytes` is simply an alias for `str`. Without this method, @@ -442,7 +424,7 @@ def __bytes__(self): :param encoding: optional encoding parameter to handle the Python 3.6 two argument 'bytes' method. :return: a bytes object that functions the same across 3.6 and 2.7 """ - if hasattr(item, '__bytes__'): + if hasattr(item, "__bytes__"): return item.__bytes__() else: if encoding: @@ -461,7 +443,7 @@ def compat_chr(item): :param item: a length 1 string who's `chr` method needs to be invoked :return: the unichr code point of the single character string, item """ - if sys.version >= '3.0': + if sys.version >= "3.0": return chr(item) else: return unichr(item) diff --git a/steembase/bip38.py b/steembase/bip38.py index b5f1b22..29bb9c7 100644 --- a/steembase/bip38.py +++ b/steembase/bip38.py @@ -13,7 +13,7 @@ try: from Crypto.Cipher import AES except ImportError: - raise ImportError("Missing dependency: pycrypto") + raise ImportError("Missing dependency: pycryptodome") SCRYPT_MODULE = os.environ.get('SCRYPT_MODULE', None) if not SCRYPT_MODULE: diff --git a/steembase/transactions.py b/steembase/transactions.py index df79641..5aef114 100644 --- a/steembase/transactions.py +++ b/steembase/transactions.py @@ -326,7 +326,7 @@ def sign(self, wifkeys, chain=None): # lenR = sigder[3] lenS = sigder[5 + lenR] - if lenR is 32 and lenS is 32: + if int(lenR) == 32 and int(lenS) == 32: # Derive the recovery parameter # i = self.recoverPubkeyParameter(