Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
31 changes: 31 additions & 0 deletions explorer/rest/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,37 @@ def api_get_nem_accounts():

return jsonify(results)

@app.route('/api/nem/namespace/<name>')
def api_get_nem_namespace_by_name(name):
result = nem_api_facade.get_namespace_by_name(name)

if not result:
abort(404)

return jsonify(result)

@app.route('/api/nem/namespaces')
def api_get_nem_namespaces():
try:
limit = int(request.args.get('limit', 10))
offset = int(request.args.get('offset', 0))
sort = request.args.get('sort', 'DESC')

if limit < 0 or offset < 0:
raise ValueError('Limit and offset must be greater than or equal to 0')
if sort.upper() not in ['ASC', 'DESC']:
raise ValueError('Sort must be either ASC or DESC')

except ValueError as error:
abort(400, error)

results = nem_api_facade.get_namespaces(
pagination=Pagination(limit, offset),
sort=sort
)

return jsonify(results)


def setup_error_handlers(app):
@app.errorhandler(404)
Expand Down
73 changes: 73 additions & 0 deletions explorer/rest/rest/db/NemDatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from rest.model.Account import AccountView
from rest.model.Block import BlockView
from rest.model.Namespace import NamespaceView

from .DatabaseConnection import DatabaseConnectionPool

Expand Down Expand Up @@ -91,6 +92,25 @@ def _create_account_view(self, result): # pylint: disable=no-self-use,too-many-
cosignatories=[str(Address(address)) for address in cosignatories] if cosignatories else None
)

def _create_namespace_view(self, result): # pylint: disable=no-self-use
(
root_namespace,
owner,
registered_height,
registered_timestamp,
expiration_height,
sub_namespaces
) = result

return NamespaceView(
root_namespace=root_namespace,
owner=str(PublicKey(owner)),
registered_height=registered_height,
registered_timestamp=str(registered_timestamp),
expiration_height=expiration_height,
sub_namespaces=sub_namespaces
)

def _generate_account_query(self, where_condition, order_condition='', limit_condition=''): # pylint: disable=no-self-use
"""Base account query."""

Expand Down Expand Up @@ -138,6 +158,25 @@ def _generate_block_query(self, where_condition, order_condition='', limit_condi
{limit_condition}
'''

def _generate_namespace_query(self, where_condition='', order_condition='', limit_condition=''): # pylint: disable=no-self-use
"""Base namespace query."""

return f'''
SELECT
root_namespace,
owner,
registered_height,
b.timestamp AS registered_timestamp,
expiration_height ,
sub_namespaces
FROM namespaces n
left join blocks b
on n.registered_height = b.height
{where_condition}
{order_condition}
{limit_condition}
'''

def _get_account(self, where_condition, query_bytes):
"""Gets account by where clause."""

Expand Down Expand Up @@ -217,3 +256,37 @@ def get_accounts(self, pagination, sorting, is_harvesting):
results = cursor.fetchall()

return [self._create_account_view(result) for result in results]

def get_namespace_by_name(self, name):
"""Gets namespace by root namespace or sub namespace name."""

where_condition = 'WHERE n.root_namespace = %s or %s = ANY(n.sub_namespaces)'

sql = self._generate_namespace_query(where_condition)

with self.connection() as connection:
cursor = connection.cursor()
cursor.execute(sql, (name, name))
result = cursor.fetchone()

return self._create_namespace_view(result) if result else None

def get_namespaces(self, pagination, sort):
"""Gets namespaces pagination in database."""

order_condition = f' ORDER BY registered_height {sort}'
limit_condition = ' LIMIT %s OFFSET %s'

sql = self._generate_namespace_query(
order_condition=order_condition,
limit_condition=limit_condition
)

params = [pagination.limit, pagination.offset]

with self.connection() as connection:
cursor = connection.cursor()
cursor.execute(sql, params)
results = cursor.fetchall()

return [self._create_namespace_view(result) for result in results]
14 changes: 14 additions & 0 deletions explorer/rest/rest/facade/NemRestFacade.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,17 @@ async def get_health(self):
'lastDBHeight': last_db_height,
'errors': errors
}

def get_namespace_by_name(self, name):
"""Gets namespace by root namespace or sub namespace name."""

namespace = self.nem_db.get_namespace_by_name(name)

return namespace.to_dict() if namespace else None

def get_namespaces(self, pagination, sort):
"""Gets namespaces pagination."""

namespaces = self.nem_db.get_namespaces(pagination, sort)

return [namespace.to_dict() for namespace in namespaces]
34 changes: 34 additions & 0 deletions explorer/rest/rest/model/Namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class NamespaceView:
def __init__(self, root_namespace, owner, registered_height, registered_timestamp, expiration_height, sub_namespaces):
"""Create Namespace view."""

# pylint: disable=too-many-arguments,too-many-positional-arguments

self.root_namespace = root_namespace
self.owner = owner
self.registered_height = registered_height
self.registered_timestamp = registered_timestamp
self.expiration_height = expiration_height
self.sub_namespaces = sub_namespaces

def __eq__(self, other):
return isinstance(other, NamespaceView) and all([
self.root_namespace == other.root_namespace,
self.owner == other.owner,
self.registered_height == other.registered_height,
self.registered_timestamp == other.registered_timestamp,
self.expiration_height == other.expiration_height,
self.sub_namespaces == other.sub_namespaces
])

def to_dict(self):
"""Formats the namespace info as a dictionary."""

return {
'rootNamespace': self.root_namespace,
'owner': self.owner,
'registeredHeight': self.registered_height,
'registeredTimestamp': self.registered_timestamp,
'expirationHeight': self.expiration_height,
'subNamespaces': self.sub_namespaces
}
51 changes: 49 additions & 2 deletions explorer/rest/tests/db/test_NemDatabase.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest import Pagination, Sorting
from rest.db.NemDatabase import NemDatabase

from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, ACCOUNTS, BLOCK_VIEWS, DatabaseTestBase
from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, ACCOUNTS, BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase

# region test data

Expand All @@ -13,10 +13,14 @@

EXPECTED_ACCOUNT_VIEW_2 = ACCOUNT_VIEWS[1]

EXPECTED_NAMESPACE_VIEW_1 = NAMESPACE_VIEWS[0]

EXPECTED_NAMESPACE_VIEW_2 = NAMESPACE_VIEWS[1]

# endregion


class NemDatabaseTest(DatabaseTestBase):
class NemDatabaseTest(DatabaseTestBase): # pylint: disable=too-many-public-methods

def setUp(self):
super().setUp()
Expand Down Expand Up @@ -109,3 +113,46 @@ def test_can_query_accounts_sorted_by_balance_desc(self):
self._assert_can_query_accounts(Pagination(10, 0), Sorting('BALANCE', 'desc'), [EXPECTED_ACCOUNT_VIEW_2, EXPECTED_ACCOUNT_VIEW_1])

# endregion

# region namespace

def _assert_can_query_namespace_by_name(self, namespace, expected_namespace):
# Act:
namespace_view = self.nem_db.get_namespace_by_name(namespace)

# Assert:
self.assertEqual(expected_namespace, namespace_view)

def test_can_query_namespace_by_root_namespace(self):
self._assert_can_query_namespace_by_name('root', EXPECTED_NAMESPACE_VIEW_1)

def test_can_query_namespace_by_sub_namespace(self):
self._assert_can_query_namespace_by_name('root_sub.sub_1', EXPECTED_NAMESPACE_VIEW_2)

def test_cannot_query_nonexistent_namespace(self):
self._assert_can_query_namespace_by_name('nonexistent', None)

# endregion

# region namespaces

def _assert_can_query_namespaces_with_filter(self, pagination, sort, expected_namespaces):
# Act:
namespaces_view = self.nem_db.get_namespaces(pagination, sort)

# Assert:
self.assertEqual(expected_namespaces, namespaces_view)

def test_can_query_namespaces_filtered_limit_offset_0(self):
self._assert_can_query_namespaces_with_filter(Pagination(1, 0), 'desc', [EXPECTED_NAMESPACE_VIEW_2])

def test_can_query_namespaces_filtered_offset_1(self):
self._assert_can_query_namespaces_with_filter(Pagination(1, 1), 'desc', [EXPECTED_NAMESPACE_VIEW_1])

def test_can_query_namespaces_sorted_by_registered_height_asc(self):
self._assert_can_query_namespaces_with_filter(Pagination(10, 0), 'asc', [EXPECTED_NAMESPACE_VIEW_1, EXPECTED_NAMESPACE_VIEW_2])

def test_can_query_namespaces_sorted_by_registered_height_desc(self):
self._assert_can_query_namespaces_with_filter(Pagination(10, 0), 'desc', [EXPECTED_NAMESPACE_VIEW_2, EXPECTED_NAMESPACE_VIEW_1])

# endregion
65 changes: 64 additions & 1 deletion explorer/rest/tests/facade/test_NemRestFacade.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest.facade.NemRestFacade import NemRestFacade
from rest.model.common import Pagination, RestConfig, Sorting

from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, BLOCK_VIEWS, DatabaseTestBase
from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase

# region test data

Expand All @@ -18,6 +18,10 @@

EXPECTED_ACCOUNT_2 = ACCOUNT_VIEWS[1].to_dict()

EXPECTED_NAMESPACE_1 = NAMESPACE_VIEWS[0].to_dict()

EXPECTED_NAMESPACE_2 = NAMESPACE_VIEWS[1].to_dict()

# endregion


Expand Down Expand Up @@ -210,3 +214,62 @@ def test_can_retrieve_health_with_node_exception(self, mock_chain_height):
self.assertEqual([{'type': 'synchronization', 'message': 'Connection refused'}], result['errors'])

# endregion

# region namespace

def _assert_can_retrieve_namespace_by_name(self, name, expected_namespace):
# Act:
namespace = self.nem_rest_facade.get_namespace_by_name(name)

# Assert:
self.assertEqual(expected_namespace, namespace)

def test_can_retrieve_namespace_by_root_namespace_name(self):
self._assert_can_retrieve_namespace_by_name(name='root', expected_namespace=EXPECTED_NAMESPACE_1)

def test_can_retrieve_namespace_by_sub_namespace_name(self):
self._assert_can_retrieve_namespace_by_name(name='root_sub.sub_1', expected_namespace=EXPECTED_NAMESPACE_2)

def test_returns_none_for_nonexistent_namespace_name(self):
self._assert_can_retrieve_namespace_by_name(name='nonexistent', expected_namespace=None)

# endregion

# region namespaces

def _assert_can_retrieve_namespaces(self, pagination, sort, expected_namespaces):
# Act:
namespaces = self.nem_rest_facade.get_namespaces(pagination, sort)

# Assert:
self.assertEqual(expected_namespaces, namespaces)

def test_can_retrieve_namespaces_filtered_by_limit(self):
self._assert_can_retrieve_namespaces(
pagination=Pagination(1, 0),
sort='DESC',
expected_namespaces=[EXPECTED_NAMESPACE_2]
)

def test_can_retrieve_namespaces_filtered_by_offset(self):
self._assert_can_retrieve_namespaces(
pagination=Pagination(1, 1),
sort='DESC',
expected_namespaces=[EXPECTED_NAMESPACE_1]
)

def test_can_retrieve_namespaces_sorted_by_registered_height_asc(self):
self._assert_can_retrieve_namespaces(
pagination=Pagination(10, 0),
sort='ASC',
expected_namespaces=[EXPECTED_NAMESPACE_1, EXPECTED_NAMESPACE_2]
)

def test_can_retrieve_namespaces_sorted_by_registered_height_desc(self):
self._assert_can_retrieve_namespaces(
pagination=Pagination(10, 0),
sort='DESC',
expected_namespaces=[EXPECTED_NAMESPACE_2, EXPECTED_NAMESPACE_1]
)

# endregion
Loading