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 mosaics pagination endpoint #711

Draft
wants to merge 4 commits into
base: explorer/rest-mosaic-endpoint
Choose a base branch
from
Draft
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
15 changes: 15 additions & 0 deletions explorer/rest/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ def api_get_nem_mosaic_by_name(name):
abort(404)
return jsonify(result)

@app.route('/api/nem/mosaics')
def api_get_nem_mosaics():
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_mosaics(limit=limit, offset=offset, sort=sort))


def setup_error_handlers(app):
@app.errorhandler(404)
Expand Down
96 changes: 61 additions & 35 deletions explorer/rest/rest/db/NemDatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,44 @@ def _generate_namespace_sql_query(self, where_condition=None): # pylint: disabl
n.sub_namespaces
'''

def _generate_mosaic_sql_query(self): # pylint: disable=no-self-use
"""Base SQL query for mosaics."""

return '''
SELECT
m1.namespace_name,
m1.description,
m1.creator,
m1.registered_height as mosaic_registered_height,
b2.timestamp as mosaic_registered_timestamp,
m1.initial_supply,
m1.total_supply,
m1.divisibility,
m1.supply_mutable,
m1.transferable,
m1.levy_type,
m1.levy_namespace_name,
CASE
WHEN m1.levy_namespace_name = 'nem.xem' THEN 6
WHEN m1.levy_namespace_name IS NULL THEN NULL
ELSE m2.divisibility
END AS levy_namespace_divisibility,
m1.levy_fee,
m1.levy_recipient,
n.registered_height AS root_namespace_registered_height,
b1.timestamp AS root_namespace_registered_timestamp,
n.expiration_height
FROM mosaics m1
LEFT JOIN mosaics m2
ON m1.levy_namespace_name = m2.namespace_name AND m1.levy_namespace_name IS NOT NULL
LEFT JOIN namespaces n
ON m1.root_namespace = n.root_namespace
LEFT JOIN blocks b1
ON b1.height = n.registered_height
LEFT JOIN blocks b2
ON b2.height = m1.registered_height
'''

def _create_block_view(self, result):
harvest_public_key = PublicKey(_format_bytes(result[7]))
return BlockView(
Expand Down Expand Up @@ -221,43 +259,31 @@ def get_namespaces(self, limit, offset, sort):
def get_mosaic(self, namespace_name):
"""Gets mosaic by namespace name in database."""

sql = self._generate_mosaic_sql_query()

sql += ' WHERE m1.namespace_name = %s'

params = (namespace_name,)

with self.connection() as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT
m1.namespace_name,
m1.description,
m1.creator,
m1.registered_height as mosaic_registered_height,
b2.timestamp as mosaic_registered_timestamp,
m1.initial_supply,
m1.total_supply,
m1.divisibility,
m1.supply_mutable,
m1.transferable,
m1.levy_type,
m1.levy_namespace_name,
CASE
WHEN m1.levy_namespace_name = 'nem.xem' THEN 6
WHEN m1.levy_namespace_name IS NULL THEN NULL
ELSE m2.divisibility
END AS levy_namespace_divisibility,
m1.levy_fee,
m1.levy_recipient,
n.registered_height AS root_namespace_registered_height,
b1.timestamp AS root_namespace_registered_timestamp,
n.expiration_height
FROM mosaics m1
LEFT JOIN mosaics m2
ON m1.levy_namespace_name = m2.namespace_name AND m1.levy_namespace_name IS NOT NULL
LEFT JOIN namespaces n
ON m1.root_namespace = n.root_namespace
LEFT JOIN blocks b1
ON b1.height = n.registered_height
LEFT JOIN blocks b2
ON b2.height = m1.registered_height
WHERE m1.namespace_name = %s
''', (namespace_name,))
cursor.execute(sql, params)
result = cursor.fetchone()

return self._create_mosaic_view(result) if result else None

def get_mosaics(self, limit, offset, sort):
"""Gets mosaics pagination in database."""

sql = self._generate_mosaic_sql_query()

sql += f' ORDER BY m1.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_mosaic_view(result) for result in results]
7 changes: 7 additions & 0 deletions explorer/rest/rest/facade/NemRestFacade.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,10 @@ def get_mosaic(self, name):
mosaic = self.nem_db.get_mosaic(name)

return mosaic.to_dict() if mosaic else None

def get_mosaics(self, limit, offset, sort):
"""Gets mosaics pagination."""

mosaics = self.nem_db.get_mosaics(limit, offset, sort)

return [mosaic.to_dict() for mosaic in mosaics]
34 changes: 33 additions & 1 deletion explorer/rest/tests/db/test_NemDatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
PaginationQueryParams = namedtuple('PaginationQueryParams', ['limit', 'offset', 'sort'])


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

# region block tests

Expand Down Expand Up @@ -129,3 +129,35 @@ def test_cannot_query_nonexistent_mosaic(self):
self._assert_can_query_mosaic_by_name('non-exist-mosaic', None)

# endregion

# region mosaics tests

def _assert_can_query_mosaics_with_filter(self, query_params, expected_mosaics):
# Arrange:
nem_db = NemDatabase(self.db_config, self.network_name)

# Act:
mosaics_view = nem_db.get_mosaics(query_params.limit, query_params.offset, query_params.sort)

# Assert:
self.assertEqual(expected_mosaics, mosaics_view)

def test_can_query_mosaics_filtered_limit(self):
self._assert_can_query_mosaics_with_filter(PaginationQueryParams(1, 0, 'desc'), [MOSAIC_VIEWS[1]])

def test_can_query_mosaics_filtered_offset_0(self):
self._assert_can_query_mosaics_with_filter(PaginationQueryParams(1, 0, 'desc'), [MOSAIC_VIEWS[1]])

def test_can_query_mosaics_filtered_offset_1(self):
self._assert_can_query_mosaics_with_filter(PaginationQueryParams(1, 1, 'desc'), [MOSAIC_VIEWS[0]])

def test_can_query_mosaics_sorted_by_id_asc(self):
self._assert_can_query_mosaics_with_filter(PaginationQueryParams(10, 0, 'asc'), [MOSAIC_VIEWS[0], MOSAIC_VIEWS[1]])

def test_can_query_mosaics_sorted_by_id_desc(self):
self._assert_can_query_mosaics_with_filter(
PaginationQueryParams(10, 0, 'desc'),
[MOSAIC_VIEWS[1], MOSAIC_VIEWS[0]]
)

# endregion
28 changes: 28 additions & 0 deletions explorer/rest/tests/facade/test_NemRestFacade.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

EXPECTED_MOSAIC_1 = MOSAIC_VIEWS[0].to_dict()

EXPECTED_MOSAIC_2 = MOSAIC_VIEWS[1].to_dict()

# endregion


Expand Down Expand Up @@ -129,3 +131,29 @@ def test_returns_none_for_nonexistent_mosaic(self):
self._assert_can_retrieve_mosaic('non_existing_mosaic', None)

# endregion

# region mosaic tests

def _assert_can_retrieve_mosaics(self, query_params, expected_mosaics):
# Arrange:
nem_rest_facade = NemRestFacade(self.db_config, self.network_name)

# Act:
mosaics = nem_rest_facade.get_mosaics(query_params.limit, query_params.offset, query_params.sort)

# Assert:
self.assertEqual(expected_mosaics, mosaics)

def test_mosaics_filtered_by_limit(self):
self._assert_can_retrieve_mosaics(PaginationQueryParams(1, 0, 'desc'), [EXPECTED_MOSAIC_2])

def test_mosaics_filtered_by_offset(self):
self._assert_can_retrieve_mosaics(PaginationQueryParams(1, 1, 'desc'), [EXPECTED_MOSAIC_1])

def test_mosaics_sorted_by_id_asc(self):
self._assert_can_retrieve_mosaics(PaginationQueryParams(10, 0, 'asc'), [EXPECTED_MOSAIC_1, EXPECTED_MOSAIC_2])

def test_mosaics_sorted_by_id_desc(self):
self._assert_can_retrieve_mosaics(PaginationQueryParams(10, 0, 'desc'), [EXPECTED_MOSAIC_2, EXPECTED_MOSAIC_1])

# endregion
62 changes: 54 additions & 8 deletions explorer/rest/tests/test/DatabaseTestUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@
'nem.xem',
15000,
'NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3'
),
Mosaic(
'dragon',
'dragon.hunter',
'hunter information',
'8D07F90FB4BBE7715FA327C926770166A11BE2E494A970605F2E12557F66C9B9',
2,
100,
100,
6,
False,
True,
None,
None,
None,
None
)
]

Expand All @@ -141,14 +157,24 @@
'2015-03-29 20:34:19',
NAMESPACES[1].expiration_height,
[],
[{
'namespaceName': 'dragon',
'mosaicName': 'dragonfly',
'totalSupply': 100,
'divisibility': 0,
'registeredHeight': 2,
'registeredTimestamp': '2015-03-29 20:34:19'
}]
[
{
'namespaceName': 'dragon',
'mosaicName': 'hunter',
'totalSupply': 100,
'divisibility': 6,
'registeredHeight': 2,
'registeredTimestamp': '2015-03-29 20:34:19'
},
{
'namespaceName': 'dragon',
'mosaicName': 'dragonfly',
'totalSupply': 100,
'divisibility': 0,
'registeredHeight': 2,
'registeredTimestamp': '2015-03-29 20:34:19'
}
]
)
]

Expand All @@ -172,6 +198,26 @@
2,
'2015-03-29 20:34:19',
525602
),
MosaicView(
'hunter',
'dragon',
MOSAICS[1].description,
Address('NANEMOABLAGR72AZ2RV3V4ZHDCXW25XQ73O7OBT5'),
MOSAICS[1].registered_height,
'2015-03-29 20:34:19',
MOSAICS[1].initial_supply,
MOSAICS[1].total_supply,
MOSAICS[1].divisibility,
MOSAICS[1].supply_mutable,
MOSAICS[1].transferable,
None,
None,
None,
None,
2,
'2015-03-29 20:34:19',
525602
)
]

Expand Down
75 changes: 75 additions & 0 deletions explorer/rest/tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,78 @@ def test_api_nem_mosaic_non_exist(client): # pylint: disable=redefined-outer-na
})

# endregion


# region /mosaics

def _get_api_nem_mosaics(client, **query_params): # pylint: disable=redefined-outer-name
query_string = '&'.join(f'{key}={val}' for key, val in query_params.items())
return client.get(f'/api/nem/mosaics?{query_string}')


def _assert_get_api_nem_mosaics(client, expected_status_code, expected_result, **query_params): # pylint: disable=redefined-outer-name
# Act:
response = _get_api_nem_mosaics(client, **query_params)

# Assert:
_assert_status_code_and_headers(response, expected_status_code)
assert expected_result == response.json


def test_api_nem_mosaics_without_params(client): # pylint: disable=redefined-outer-name, invalid-name
# Act:
response = _get_api_nem_mosaics(client)

# Assert:
_assert_status_code_and_headers(response, 200)
assert [MOSAIC_VIEWS[1].to_dict(), MOSAIC_VIEWS[0].to_dict()] == response.json


def test_api_nem_mosaics_applies_limit(client): # pylint: disable=redefined-outer-name, invalid-name
_assert_get_api_nem_mosaics(client, 200, [MOSAIC_VIEWS[1].to_dict()], limit=1)


def test_api_nem_mosaics_applies_offset(client): # pylint: disable=redefined-outer-name, invalid-name
_assert_get_api_nem_mosaics(client, 200, [MOSAIC_VIEWS[0].to_dict()], offset=1)


def test_api_nem_mosaics_applies_sorted_by_id_desc(client): # pylint: disable=redefined-outer-name, invalid-name
_assert_get_api_nem_mosaics(client, 200, [MOSAIC_VIEWS[1].to_dict(), MOSAIC_VIEWS[0].to_dict()], sort='desc')


def test_api_nem_mosaics_applies_sorted_by_id_asc(client): # pylint: disable=redefined-outer-name, invalid-name
_assert_get_api_nem_mosaics(client, 200, [MOSAIC_VIEWS[0].to_dict(), MOSAIC_VIEWS[1].to_dict()], sort='asc')


def test_api_nem_mosaics_with_all_params(client): # pylint: disable=redefined-outer-name, invalid-name
_assert_get_api_nem_mosaics(client, 200, [MOSAIC_VIEWS[1].to_dict()], limit=1, offset=1, sort='asc')


def _assert_get_api_nem_mosaics_fail(client, expected_status_code, **query_params): # pylint: disable=redefined-outer-name
# Act:
response = _get_api_nem_mosaics(client, **query_params)

# Assert:
_assert_status_code_and_headers(response, expected_status_code)
assert {
'message': 'Bad request',
'status': expected_status_code
} == response.json


def test_api_nem_mosaics_invalid_limit(client): # pylint: disable=redefined-outer-name, invalid-name
_assert_get_api_nem_mosaics_fail(client, 400, limit=-1)
_assert_get_api_nem_mosaics_fail(client, 400, limit='invalid')


def test_api_nem_mosaics_invalid_offset(client): # pylint: disable=redefined-outer-name, invalid-name
_assert_get_api_nem_mosaics_fail(client, 400, offset=-1)
_assert_get_api_nem_mosaics_fail(client, 400, offset='invalid')


def test_api_nem_mosaics_invalid_sort(client): # pylint: disable=redefined-outer-name
_assert_get_api_nem_mosaics_fail(client, 400, sort=-1)
_assert_get_api_nem_mosaics_fail(client, 400, sort='invalid')


# endregion