Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor account CLI commands and implementation #26

Merged
merged 4 commits into from
Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions cli/account/cli.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,8 +19,6 @@
print_result,
)

loop = asyncio.get_event_loop()


@click.group('account', chain=True)
def account_commands():
Expand All @@ -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):
Expand All @@ -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')
Expand All @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it's needed here, if Account(service=remme).get_balance(address=address) always return error=None.

Copy link
Contributor Author

@dmytrostriletskyi dmytrostriletskyi Apr 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Alladin9393, I think, In the future, we will have general exception cathing in every service. Could we leave it here although there are no errors for now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print_errors(errors=errors)
sys.exit(FAILED_EXIT_FROM_COMMAND_CODE)

print_result(balance)
print_result(result=result)
43 changes: 6 additions & 37 deletions cli/account/forms.py
Original file line number Diff line number Diff line change
@@ -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,
)


Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an extra check.

Suggested change
address = AccountAddressField(required=True)
address = AccountAddressField(required=False)

Because of click.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Alladin9393, imagine we will migrate this code from CLI to Web. Do we need to pay additional attention to how CLI handle arguments? No, we don't. We just need to cut the CLI. I mean forms do not know about the CLI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node_url = NodeURLField(allow_none=True, required=False)
2 changes: 1 addition & 1 deletion cli/account/help.py
Original file line number Diff line number Diff line change
@@ -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.'
2 changes: 1 addition & 1 deletion cli/account/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down
11 changes: 9 additions & 2 deletions cli/account/service.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
1 change: 1 addition & 0 deletions cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Empty file added cli/generic/__init__.py
Empty file.
Empty file added cli/generic/forms/__init__.py
Empty file.
62 changes: 62 additions & 0 deletions cli/generic/forms/fields.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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():
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
58 changes: 35 additions & 23 deletions tests/test_account.py → tests/account/test_get_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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():
Expand All @@ -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
Expand All @@ -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, [
Expand All @@ -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():
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down