Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1131 from gleybersonandrade/fixes
Browse files Browse the repository at this point in the history
Update APIServer to use Flask blueprints
  • Loading branch information
hdiogenes committed Sep 8, 2020
2 parents c6d8db9 + d60183e commit a55c1bb
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 21 deletions.
29 changes: 23 additions & 6 deletions kytos/core/api_server.py
Expand Up @@ -12,7 +12,7 @@
from urllib.error import HTTPError, URLError
from urllib.request import urlopen, urlretrieve

from flask import Flask, jsonify, request, send_file
from flask import Blueprint, Flask, jsonify, request, send_file
from flask_cors import CORS
from flask_socketio import SocketIO, join_room, leave_room
from werkzeug.exceptions import HTTPException
Expand Down Expand Up @@ -98,7 +98,8 @@ def register_rest_endpoint(self, url, function, methods):
stacklevel=2)
if url.startswith('/'):
url = url[1:]
self._start_endpoint(f'/kytos/{url}', function, methods=methods)
self._start_endpoint(self.app, f'/kytos/{url}', function,
methods=methods)

def start_api(self):
"""Start this APIServer instance API.
Expand All @@ -123,7 +124,8 @@ def register_core_endpoint(self, rule, function, **options):
Not used by NApps, but controller.
"""
self._start_endpoint(self._CORE_PREFIX + rule, function, **options)
self._start_endpoint(self.app, self._CORE_PREFIX + rule, function,
**options)

def _register_web_ui(self):
"""Register routes to the admin-ui homepage."""
Expand Down Expand Up @@ -291,15 +293,27 @@ def store_route_params(function):
def register_napp_endpoints(self, napp):
"""Add all NApp REST endpoints with @rest decorator.
We are using Flask Blueprints to register these endpoints. Blueprints
are essentially the Flask equivalent of Python modules and are used to
keep related logic and assets grouped and separated from one another.
URLs will be prefixed with ``/api/{username}/{napp_name}/``.
Args:
napp (Napp): Napp instance to register new endpoints.
"""
# Create a Flask Blueprint for a specific NApp
napp_blueprint = Blueprint(napp.napp_id, __name__)

# Start all endpoints for this NApp
for function in self._get_decorated_functions(napp):
for rule, options in function.route_params:
absolute_rule = self.get_absolute_rule(rule, napp)
self._start_endpoint(absolute_rule, function, **options)
self._start_endpoint(napp_blueprint, absolute_rule, function,
**options)

# Register this Flask Blueprint in the Flask App
self.app.register_blueprint(napp_blueprint)

@staticmethod
def _get_decorated_functions(napp):
Expand All @@ -323,14 +337,14 @@ def get_absolute_rule(cls, rule, napp):

# END decorator methods

def _start_endpoint(self, rule, function, **options):
def _start_endpoint(self, app, rule, function, **options):
"""Start ``function``'s endpoint.
Forward parameters to ``Flask.add_url_rule`` mimicking Flask
``@route`` decorator.
"""
endpoint = options.pop('endpoint', None)
self.app.add_url_rule(rule, endpoint, function, **options)
app.add_url_rule(rule, endpoint, function, **options)
self.log.info('Started %s - %s', rule,
', '.join(options.get('methods', self.DEFAULT_METHODS)))

Expand All @@ -357,6 +371,9 @@ def remove_napp_endpoints(self, napp):
self.app.url_map._rules.pop(index)
# pylint: enable=protected-access

# Remove the Flask Blueprint of this NApp from the Flask App
self.app.blueprints.pop(napp.napp_id)

self.log.info('The Rest endpoints from %s were disabled.', prefix)

def register_core_napp_services(self):
Expand Down
40 changes: 28 additions & 12 deletions tests/unit/test_core/test_api_server.py
Expand Up @@ -393,13 +393,15 @@ class RESTNApp: # pylint: disable=too-few-public-methods
def __init__(self):
self.username = 'test'
self.name = 'MyNApp'
self.napp_id = 'test/MyNApp'


class TestAPIDecorator(unittest.TestCase):
"""@rest should have the same effect as ``Flask.route``."""

@classmethod
def test_flask_call(cls):
@patch('kytos.core.api_server.Blueprint')
def test_flask_call(cls, mock_blueprint):
"""@rest params should be forwarded to Flask."""
rule = 'rule'
# Use sentinels to be sure they are not changed.
Expand All @@ -412,9 +414,12 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods
def my_endpoint(self):
"""Do nothing."""

blueprint = Mock()
mock_blueprint.return_value = blueprint

napp = MyNApp()
server = cls._mock_api_server(napp)
server.app.add_url_rule.assert_called_once_with(
cls._mock_api_server(napp)
blueprint.add_url_rule.assert_called_once_with(
'/api/test/MyNApp/' + rule, None, napp.my_endpoint, **options)

@classmethod
Expand All @@ -426,6 +431,7 @@ class MyNApp: # pylint: disable=too-few-public-methods
def __init__(self):
self.username = 'test'
self.name = 'MyNApp'
self.napp_id = 'test/MyNApp'

napp = MyNApp()
server = cls._mock_api_server(napp)
Expand All @@ -447,9 +453,11 @@ def __init__(self):
# pylint: disable=protected-access
server.app.url_map._rules.pop.assert_called_once_with(0)
# pylint: enable=protected-access
server.app.blueprints.pop.assert_called_once_with(napp.napp_id)

@classmethod
def test_rule_with_slash(cls):
@patch('kytos.core.api_server.Blueprint')
def test_rule_with_slash(cls, mock_blueprint):
"""There should be no double slashes in a rule."""
class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods
"""API decorator example usage."""
Expand All @@ -458,10 +466,13 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods
def my_endpoint(self):
"""Do nothing."""

cls._assert_rule_is_added(MyNApp)
blueprint = Mock()
mock_blueprint.return_value = blueprint
cls._assert_rule_is_added(MyNApp, blueprint)

@classmethod
def test_rule_from_classmethod(cls):
@patch('kytos.core.api_server.Blueprint')
def test_rule_from_classmethod(cls, mock_blueprint):
"""Use class methods as endpoints as well."""
class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods
"""API decorator example usage."""
Expand All @@ -471,10 +482,13 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods
def my_endpoint(cls):
"""Do nothing."""

cls._assert_rule_is_added(MyNApp)
blueprint = Mock()
mock_blueprint.return_value = blueprint
cls._assert_rule_is_added(MyNApp, blueprint)

@classmethod
def test_rule_from_staticmethod(cls):
@patch('kytos.core.api_server.Blueprint')
def test_rule_from_staticmethod(cls, mock_blueprint):
"""Use static methods as endpoints as well."""
class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods
"""API decorator example usage."""
Expand All @@ -484,14 +498,16 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods
def my_endpoint():
"""Do nothing."""

cls._assert_rule_is_added(MyNApp)
blueprint = Mock()
mock_blueprint.return_value = blueprint
cls._assert_rule_is_added(MyNApp, blueprint)

@classmethod
def _assert_rule_is_added(cls, napp_class):
def _assert_rule_is_added(cls, napp_class, blueprint):
"""Assert Flask's add_url_rule was called with the right parameters."""
napp = napp_class()
server = cls._mock_api_server(napp)
server.app.add_url_rule.assert_called_once_with(
cls._mock_api_server(napp)
blueprint.add_url_rule.assert_called_once_with(
'/api/test/MyNApp/rule', None, napp.my_endpoint)

@staticmethod
Expand Down
1 change: 0 additions & 1 deletion tests/unit/test_core/test_auth.py
Expand Up @@ -56,7 +56,6 @@ def _get_controller_mock(self):
@staticmethod
def get_auth_test_client(auth):
"""Return a flask api test client."""
auth.controller.api_server.register_napp_endpoints(auth)
return auth.controller.api_server.app.test_client()

@patch('kytos.core.auth.Auth._create_superuser')
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/test_core/test_controller.py
Expand Up @@ -60,7 +60,8 @@ def test_websocket_log_usage(path, log_manager):
# Restore original state
logging.root.handlers = handlers_bak

def test_unload_napp_listener(self):
@patch('kytos.core.api_server.APIServer.remove_napp_endpoints')
def test_unload_napp_listener(self, _):
"""Call NApp shutdown listener on unload."""
username, napp_name = 'test', 'napp'
listener = self._add_napp(username, napp_name)
Expand All @@ -69,7 +70,8 @@ def test_unload_napp_listener(self):
self.controller.unload_napp(username, napp_name)
listener.assert_called()

def test_unload_napp_other_listener(self):
@patch('kytos.core.api_server.APIServer.remove_napp_endpoints')
def test_unload_napp_other_listener(self, _):
"""Should not call other NApps' shutdown listener on unload."""
username, napp_name = 'test', 'napp1'
self._add_napp(username, napp_name)
Expand Down

0 comments on commit a55c1bb

Please sign in to comment.