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

[explorer/rest]: added namespace and namespaces endpoint #702

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from
Open
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
23 changes: 23 additions & 0 deletions explorer/rest/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,29 @@ def api_get_nem_blocks():

return jsonify(nem_api_facade.get_blocks(limit=limit, offset=offset, min_height=min_height, sort=sort))

@app.route('/api/nem/namespace/<name>')
def api_get_nem_namespace_by_name(name):
result = nem_api_facade.get_namespace(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 or sort.upper() not in ['ASC', 'DESC']:
raise ValueError()

except ValueError:
abort(400)

return jsonify(nem_api_facade.get_namespaces(limit=limit, offset=offset, sort=sort))


def setup_error_handlers(app):
@app.errorhandler(404)
Expand Down
106 changes: 106 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 symbolchain.Network import NetworkLocator

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

from .DatabaseConnection import DatabaseConnectionPool

Expand All @@ -24,6 +25,48 @@ def __init__(self, db_config, network_name):
super().__init__(db_config)
self.network = NetworkLocator.find_by_name(Network.NETWORKS, network_name)

def _generate_namespace_sql_query(self, where_condition=None): # pylint: disable=no-self-use
"""Base SQL query for namespaces."""

where_clause = f'WHERE {where_condition}' if where_condition else ''

return f'''
SELECT
n.id,
n.root_namespace,
n.owner,
n.registered_height,
b1.timestamp AS registered_timestamp,
n.expiration_height,
n.sub_namespaces,
CASE
WHEN COUNT(m.namespace_name) = 0 THEN '[]'
ELSE json_agg(json_build_object(
'namespace_name', namespace_name,
'total_supply', m.total_supply,
'divisibility', m.divisibility,
'registered_height', m.registered_height,
'registered_timestamp', b2.timestamp
))
END AS mosaics
FROM namespaces n
LEFT JOIN mosaics m
ON n.root_namespace = m.root_namespace
LEFT JOIN blocks b1
ON n.registered_height = b1.height
LEFT JOIN blocks b2
ON m.registered_height = b2.height
{where_clause}
GROUP BY
n.id,
n.root_namespace,
n.owner,
n.registered_height,
b1.timestamp,
n.expiration_height,
n.sub_namespaces
'''

def _create_block_view(self, result):
harvest_public_key = PublicKey(_format_bytes(result[7]))
return BlockView(
Expand All @@ -38,6 +81,37 @@ def _create_block_view(self, result):
size=result[9]
)

def _create_namespace_view(self, result):
owner_public_key = PublicKey(_format_bytes(result[2]))

mosaics = []

if result[7] != []:
# Formatting mosaics info
for mosaic in result[7]:
namespace_mosaic_name = mosaic['namespace_name'].split('.')
namespace_name = '.'.join(namespace_mosaic_name[:-1])
mosaic_name = namespace_mosaic_name[-1]

mosaics.append({
'namespaceName': namespace_name,
'mosaicName': mosaic_name,
'totalSupply': mosaic['total_supply'],
'divisibility': mosaic['divisibility'],
'registeredHeight': mosaic['registered_height'],
'registeredTimestamp': mosaic['registered_timestamp'].replace('T', ' ')
})

return NamespaceView(
root_namespace=result[1],
owner=self.network.public_key_to_address(owner_public_key),
registered_height=result[3],
registered_timestamp=str(result[4]),
expiration_height=result[5],
sub_namespaces=result[6],
mosaics=mosaics
)

def get_block(self, height):
"""Gets block by height in database."""

Expand Down Expand Up @@ -67,3 +141,35 @@ def get_blocks(self, limit, offset, min_height, sort):
results = cursor.fetchall()

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

def get_namespace(self, namespace):
"""Gets namespace by name in database."""

sql = self._generate_namespace_sql_query(
'n.root_namespace = %s or %s = ANY(n.sub_namespaces)'
)
params = (namespace, namespace)

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

return self._create_namespace_view(result) if result else None

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

sql = self._generate_namespace_sql_query()
sql += f'''
ORDER BY n.id {sort}
LIMIT %s OFFSET %s
'''
params = (limit, 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 @@ -22,3 +22,17 @@ def get_blocks(self, limit, offset, min_height, sort):
blocks = self.nem_db.get_blocks(limit, offset, min_height, sort)

return [block.to_dict() for block in blocks]

def get_namespace(self, name):
"""Gets namespace by root namespace name."""

namespace = self.nem_db.get_namespace(name)

return namespace.to_dict() if namespace else None

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

namespaces = self.nem_db.get_namespaces(limit, offset, sort)

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

# pylint: disable=too-many-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
self.mosaics = mosaics

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,
self.mosaics == other.mosaics
])

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

return {
'rootNamespace': self.root_namespace,
'owner': str(self.owner),
'registeredHeight': self.registered_height,
'registeredTimestamp': str(self.registered_timestamp),
'expirationHeight': self.expiration_height,
'subNamespaces': self.sub_namespaces,
'mosaics': self.mosaics
}
59 changes: 58 additions & 1 deletion explorer/rest/tests/db/test_NemDatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@

from rest.db.NemDatabase import NemDatabase

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

BlockQueryParams = namedtuple('BlockQueryParams', ['limit', 'offset', 'min_height', 'sort'])
PaginationQueryParams = namedtuple('PaginationQueryParams', ['limit', 'offset', 'sort'])

# region test data

EXPECTED_BLOCK_VIEW_1 = BLOCK_VIEWS[0]

EXPECTED_BLOCK_VIEW_2 = BLOCK_VIEWS[1]

EXPECTED_NAMESPACE_VIEW_1 = NAMESPACE_VIEWS[0]

EXPECTED_NAMESPACE_VIEW_2 = NAMESPACE_VIEWS[1]

# endregion


class NemDatabaseTest(DatabaseTestBase):

# region block tests

def _assert_can_query_block_by_height(self, height, expected_block):
# Arrange:
nem_db = NemDatabase(self.db_config, self.network_name)
Expand Down Expand Up @@ -66,3 +73,53 @@ def test_can_query_blocks_sorted_by_height_asc(self):

def test_can_query_blocks_sorted_by_height_desc(self):
self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 0, 'desc'), [EXPECTED_BLOCK_VIEW_2, EXPECTED_BLOCK_VIEW_1])

# endregion

# region namespace tests

def _assert_can_query_namespace_by_name(self, name, expected_namespace):
# Arrange:
nem_db = NemDatabase(self.db_config, self.network_name)

# Act:
namespace_view = nem_db.get_namespace(name)

# Assert:
self.assertEqual(expected_namespace, namespace_view)

def _assert_can_query_namespaces_with_filter(self, query_params, expected_namespaces):
# Arrange:
nem_db = NemDatabase(self.db_config, self.network_name)

# Act:
namespaces_view = nem_db.get_namespaces(query_params.limit, query_params.offset, query_params.sort)

# Assert:
self.assertEqual(expected_namespaces, namespaces_view)

def test_can_query_namespace_by_name(self):
self._assert_can_query_namespace_by_name('oxford', EXPECTED_NAMESPACE_VIEW_1)

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

def test_can_query_namespaces_filtered_limit(self):
self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 0, 'desc'), [EXPECTED_NAMESPACE_VIEW_2])

def test_can_query_namespaces_filtered_offset_0(self):
self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 0, 'desc'), [EXPECTED_NAMESPACE_VIEW_2])

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

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

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

# endregion
54 changes: 52 additions & 2 deletions explorer/rest/tests/facade/test_NemRestFacade.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
from rest.facade.NemRestFacade import NemRestFacade

from ..db.test_NemDatabase import BlockQueryParams
from ..test.DatabaseTestUtils import BLOCK_VIEWS, DatabaseTestBase
from ..db.test_NemDatabase import BlockQueryParams, PaginationQueryParams
from ..test.DatabaseTestUtils import BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase

# region test data

EXPECTED_BLOCK_1 = BLOCK_VIEWS[0].to_dict()

EXPECTED_BLOCK_2 = BLOCK_VIEWS[1].to_dict()

EXPECTED_NAMESPACE_1 = NAMESPACE_VIEWS[0].to_dict()

EXPECTED_NAMESPACE_2 = NAMESPACE_VIEWS[1].to_dict()

# endregion


class TestNemRestFacade(DatabaseTestBase):

# region block tests

def _assert_can_retrieve_block(self, height, expected_block):
# Arrange:
nem_rest_facade = NemRestFacade(self.db_config, self.network_name)
Expand Down Expand Up @@ -57,3 +63,47 @@ def test_blocks_sorted_by_height_asc(self):

def test_blocks_sorted_by_height_desc(self):
self._assert_can_retrieve_blocks(BlockQueryParams(10, 0, 0, 'desc'), [EXPECTED_BLOCK_2, EXPECTED_BLOCK_1])

# endregion

# region namespace tests

def _assert_can_retrieve_namespace(self, name, expected_namespace):
# Arrange:
nem_rest_facade = NemRestFacade(self.db_config, self.network_name)

# Act:
namespace = nem_rest_facade.get_namespace(name)

# Assert:
self.assertEqual(expected_namespace, namespace)

def _assert_can_retrieve_namespaces(self, query_params, expected_namespaces):
# Arrange:
nem_rest_facade = NemRestFacade(self.db_config, self.network_name)

# Act:
namespaces = nem_rest_facade.get_namespaces(query_params.limit, query_params.offset, query_params.sort)

# Assert:
self.assertEqual(expected_namespaces, namespaces)

def test_retrieve_namespace_by_name(self):
self._assert_can_retrieve_namespace('oxford', EXPECTED_NAMESPACE_1)

def test_returns_none_for_nonexistent_namespace(self):
self._assert_can_retrieve_namespace('non_existing_namespace', None)

def test_namespaces_filtered_by_limit(self):
self._assert_can_retrieve_namespaces(PaginationQueryParams(1, 0, 'desc'), [EXPECTED_NAMESPACE_2])

def test_namespaces_filtered_by_offset(self):
self._assert_can_retrieve_namespaces(PaginationQueryParams(1, 1, 'desc'), [EXPECTED_NAMESPACE_1])

def test_namespaces_sorted_by_id_asc(self):
self._assert_can_retrieve_namespaces(PaginationQueryParams(10, 0, 'asc'), [EXPECTED_NAMESPACE_1, EXPECTED_NAMESPACE_2])

def test_namespaces_sorted_by_id_desc(self):
self._assert_can_retrieve_namespaces(PaginationQueryParams(10, 0, 'desc'), [EXPECTED_NAMESPACE_2, EXPECTED_NAMESPACE_1])

# endregion
Loading