From fcf353acfee4e940bfcd3885a923a326c81dff67 Mon Sep 17 00:00:00 2001 From: Dmytro Striletskyi Date: Tue, 23 Apr 2019 17:32:41 +0300 Subject: [PATCH] Refactor account CLI commands and implementation (#26) --- README.md | 8 ++- cli/account/cli.py | 18 +++--- cli/account/forms.py | 43 ++----------- cli/account/help.py | 2 +- cli/account/interfaces.py | 2 +- cli/account/service.py | 11 +++- cli/constants.py | 1 + cli/generic/__init__.py | 0 cli/generic/forms/__init__.py | 0 cli/generic/forms/fields.py | 62 +++++++++++++++++++ cli/utils.py | 4 +- setup.cfg | 4 +- .../test_get_balance.py} | 58 ++++++++++------- 13 files changed, 134 insertions(+), 79 deletions(-) create mode 100644 cli/generic/__init__.py create mode 100644 cli/generic/forms/__init__.py create mode 100644 cli/generic/forms/fields.py rename tests/{test_account.py => account/test_get_balance.py} (77%) diff --git a/README.md b/README.md index c277fcd..1092b8d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ ### Installation -Install the package from the [PypI](https://pypi.org/project/remme-core-cli) through [pip](https://github.com/pypa/pip): +Install the package from the [PyPi](https://pypi.org/project/remme-core-cli) through [pip](https://github.com/pypa/pip): ```bash $ pip3 install remme-core-cli @@ -109,7 +109,11 @@ Get balance of the account by its address — ``remme account get-balance``: $ remme account get-balance \ --address=1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf \ --node-url=node-genesis-testnet.remme.io -368440.0 +{ + "result": { + "balance": 368440.0 + } +} ``` ## Development diff --git a/cli/account/cli.py b/cli/account/cli.py index 75ebefe..bae0761 100644 --- a/cli/account/cli.py +++ b/cli/account/cli.py @@ -1,14 +1,13 @@ """ Provide implementation of the command line interface's account commands. """ -import asyncio import sys import click from remme import Remme from cli.account.forms import GetAccountBalanceForm -from cli.account.help import GET_ACCOUNT_BALANCE_ADDRESS_ARGUMENT_HELP_MESSAGE +from cli.account.help import ADDRESS_ARGUMENT_HELP_MESSAGE from cli.account.service import Account from cli.constants import ( FAILED_EXIT_FROM_COMMAND_CODE, @@ -20,8 +19,6 @@ print_result, ) -loop = asyncio.get_event_loop() - @click.group('account', chain=True) def account_commands(): @@ -31,7 +28,7 @@ def account_commands(): pass -@click.option('--address', type=str, required=True, help=GET_ACCOUNT_BALANCE_ADDRESS_ARGUMENT_HELP_MESSAGE) +@click.option('--address', type=str, required=True, help=ADDRESS_ARGUMENT_HELP_MESSAGE) @click.option('--node-url', type=str, required=False, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url()) @account_commands.command('get-balance') def get_balance(address, node_url): @@ -44,7 +41,7 @@ def get_balance(address, node_url): }) if errors: - print_errors(errors) + print_errors(errors=errors) sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) address = arguments.get('address') @@ -54,7 +51,10 @@ def get_balance(address, node_url): 'node_address': str(node_url) + ':8080', }) - account_service = Account(service=remme) - balance = loop.run_until_complete(account_service.get_balance(address=address)) + result, errors = Account(service=remme).get_balance(address=address) + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) - print_result(balance) + print_result(result=result) diff --git a/cli/account/forms.py b/cli/account/forms.py index 1184e16..85be7a7 100644 --- a/cli/account/forms.py +++ b/cli/account/forms.py @@ -1,18 +1,11 @@ """ Provide forms for command line interface's account commands. """ -import re +from marshmallow import Schema -from marshmallow import ( - Schema, - ValidationError, - fields, - validates, -) - -from cli.constants import ( - ADDRESS_REGEXP, - DOMAIN_NAME_REGEXP, +from cli.generic.forms.fields import ( + AccountAddressField, + NodeURLField, ) @@ -21,29 +14,5 @@ class GetAccountBalanceForm(Schema): Get balance of the account form. """ - address = fields.String(required=True) - node_url = fields.String(allow_none=True, required=False) - - @validates('address') - def validate_address(self, address): - """ - Validate account address. - """ - if re.match(pattern=ADDRESS_REGEXP, string=address) is None: - raise ValidationError(f'The following address `{address}` is invalid.') - - @validates('node_url') - def validate_node_url(self, node_url): - """ - Validate node URL. - - If node URL is localhost, it means client didn't passed any URL, so nothing to validate. - """ - if node_url == 'localhost': - return - - if 'http' in node_url or 'https' in node_url: - raise ValidationError(f'Pass the following node URL `{node_url}` without protocol (http, https, etc.).') - - if re.match(pattern=DOMAIN_NAME_REGEXP, string=node_url) is None: - raise ValidationError(f'The following node URL `{node_url}` is invalid.') + address = AccountAddressField(required=True) + node_url = NodeURLField(allow_none=True, required=False) diff --git a/cli/account/help.py b/cli/account/help.py index f77a131..36fce4f 100644 --- a/cli/account/help.py +++ b/cli/account/help.py @@ -1,4 +1,4 @@ """ Provide help messages for command line interface's account commands. """ -GET_ACCOUNT_BALANCE_ADDRESS_ARGUMENT_HELP_MESSAGE = 'Account address to get a balance by.' +ADDRESS_ARGUMENT_HELP_MESSAGE = 'Account address to get a balance by.' diff --git a/cli/account/interfaces.py b/cli/account/interfaces.py index 5e7c089..12c2ea9 100644 --- a/cli/account/interfaces.py +++ b/cli/account/interfaces.py @@ -8,7 +8,7 @@ class AccountInterface: Implements account interface. """ - async def get_balance(self, address): + def get_balance(self, address): """ Get balance of the account by its address. """ diff --git a/cli/account/service.py b/cli/account/service.py index c1d076a..0d02f8d 100644 --- a/cli/account/service.py +++ b/cli/account/service.py @@ -1,10 +1,14 @@ """ Provide implementation of the account. """ +import asyncio + from accessify import implements from cli.account.interfaces import AccountInterface +loop = asyncio.get_event_loop() + @implements(AccountInterface) class Account: @@ -21,8 +25,11 @@ def __init__(self, service): """ self.service = service - async def get_balance(self, address): + def get_balance(self, address): """ Get balance of the account by its address. """ - return await self.service.token.get_balance(address=address) + balance = loop.run_until_complete(self.service.token.get_balance(address=address)) + return { + 'balance': balance, + }, None diff --git a/cli/constants.py b/cli/constants.py index 69b1240..f49d571 100644 --- a/cli/constants.py +++ b/cli/constants.py @@ -2,6 +2,7 @@ Provide constants for command line interface. """ ADDRESS_REGEXP = r'^[0-9a-f]{70}$' +HEADER_SIGNATURE_REGEXP = r'^[0-9a-f]{128}$' DOMAIN_NAME_REGEXP = r'(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]' PASSED_EXIT_FROM_COMMAND_CODE = 0 diff --git a/cli/generic/__init__.py b/cli/generic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/generic/forms/__init__.py b/cli/generic/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/generic/forms/fields.py b/cli/generic/forms/fields.py new file mode 100644 index 0000000..8606699 --- /dev/null +++ b/cli/generic/forms/fields.py @@ -0,0 +1,62 @@ +""" +Provide implementation of the custom fields. +""" +import re + +from marshmallow import ( + ValidationError, + fields, +) + +from cli.constants import ( + ADDRESS_REGEXP, + DOMAIN_NAME_REGEXP, +) + + +class AccountAddressField(fields.Field): + """ + Implements validation of the account address. + + References: + - https://marshmallow.readthedocs.io/en/3.0/custom_fields.html + """ + + def _deserialize(self, value, attr, obj, **kwargs): + """ + Validate data (account address) that was passed to field. + """ + address = value + + if re.match(pattern=ADDRESS_REGEXP, string=address) is None: + raise ValidationError(f'The following address `{address}` is invalid.') + + return address + + +class NodeURLField(fields.Field): + """ + Implements validation of the node URL. + + If node URL is localhost, it means client didn't passed any URL, so nothing to validate. + + References: + - https://marshmallow.readthedocs.io/en/3.0/custom_fields.html + """ + + def _deserialize(self, value, attr, obj, **kwargs): + """ + Validate data (node URL) that was passed to field. + """ + node_url = value + + if node_url == 'localhost': + return node_url + + if 'http' in node_url or 'https' in node_url: + raise ValidationError(f'Pass the following node URL `{node_url}` without protocol (http, https, etc.).') + + if re.match(pattern=DOMAIN_NAME_REGEXP, string=node_url) is None: + raise ValidationError(f'The following node URL `{node_url}` is invalid.') + + return node_url diff --git a/cli/utils.py b/cli/utils.py index 9f3a4d4..9914f48 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -34,7 +34,7 @@ def print_result(result): """ Print successful result to the terminal. """ - return click.echo(dict_to_pretty_json(result)) + return click.echo(dict_to_pretty_json({'result': result})) def print_errors(errors): @@ -47,7 +47,7 @@ def print_errors(errors): References: - https://click.palletsprojects.com/en/7.x/utils/#ansi-colors """ - click.secho(dict_to_pretty_json(errors), blink=True, bold=True, fg='red') + click.secho(dict_to_pretty_json({'errors': errors}), blink=True, bold=True, fg='red') def default_node_url(): diff --git a/setup.cfg b/setup.cfg index 26a3ba0..1318256 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,8 +9,8 @@ combine_as_imports=True max-line-length=120 ignore=D200, D413, D107, D100 per-file-ignores= - */__init__.py: D104, F401, D100, - tests/test_*: D205 + */__init__.py: D104, F401, D100 + */test_*: D205 [coverage:run] omit = diff --git a/tests/test_account.py b/tests/account/test_get_balance.py similarity index 77% rename from tests/test_account.py rename to tests/account/test_get_balance.py index 37b0051..0ab2860 100644 --- a/tests/test_account.py +++ b/tests/account/test_get_balance.py @@ -11,10 +11,7 @@ PASSED_EXIT_FROM_COMMAND_CODE, ) from cli.entrypoint import cli -from cli.utils import ( - dict_to_pretty_json, - return_async_value, -) +from cli.utils import dict_to_pretty_json def test_get_balance(): @@ -32,8 +29,10 @@ def test_get_balance(): NODE_IP_ADDRESS_FOR_TESTING, ]) + balance = json.loads(result.output).get('result').get('balance') + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code - assert isinstance(json.loads(result.output), int) + assert isinstance(balance, int) def test_get_balance_invalid_address(): @@ -54,9 +53,11 @@ def test_get_balance_invalid_address(): ]) expected_error = { - 'address': [ - f'The following address `{invalid_address}` is invalid.', - ], + 'errors': { + 'address': [ + f'The following address `{invalid_address}` is invalid.', + ], + }, } assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code @@ -66,12 +67,12 @@ def test_get_balance_invalid_address(): def test_get_balance_without_node_url(mocker): """ Case: get a balance of an account by address without passing node URL. - Expect: balance is returned. + Expect: balance is returned from node on localhost. """ - balance_from_localhost = 13500 + balance = 13500 - mock_account_get_balance = mocker.patch('cli.account.service.Account.get_balance') - mock_account_get_balance.return_value = return_async_value(balance_from_localhost) + mock_account_get_balance = mocker.patch('cli.account.service.loop.run_until_complete') + mock_account_get_balance.return_value = balance runner = CliRunner() result = runner.invoke(cli, [ @@ -81,9 +82,14 @@ def test_get_balance_without_node_url(mocker): '1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf', ]) + expected_result = { + 'result': { + 'balance': 13500, + }, + } + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code - assert isinstance(json.loads(result.output), int) - assert str(balance_from_localhost) in result.output + assert expected_result == json.loads(result.output) def test_get_balance_invalid_node_url(): @@ -104,9 +110,11 @@ def test_get_balance_invalid_node_url(): ]) expected_error = { - 'node_url': [ - f'The following node URL `{invalid_node_url}` is invalid.', - ], + 'errors': { + 'node_url': [ + f'The following node URL `{invalid_node_url}` is invalid.', + ], + }, } assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code @@ -131,9 +139,11 @@ def test_get_balance_node_url_with_http(): ]) expected_error = { - 'node_url': [ - f'Pass the following node URL `{node_url_with_http_protocol}` without protocol (http, https, etc.).', - ], + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_http_protocol}` without protocol (http, https, etc.).', + ], + }, } assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code @@ -158,9 +168,11 @@ def test_get_balance_node_url_with_https(): ]) expected_error = { - 'node_url': [ - f'Pass the following node URL `{node_url_with_https_protocol}` without protocol (http, https, etc.).', - ], + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_https_protocol}` without protocol (http, https, etc.).', + ], + }, } assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code