diff --git a/.coveragerc b/.coveragerc index 6d1f7ca..060f720 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,6 +9,7 @@ source = protean_flask tests parallel = true +omit = */__main__.py [report] show_missing = true diff --git a/requirements.txt b/requirements.txt index faa78af..3b2c63c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ werkzeug==0.14.1 flask==1.0.2 -click==6.7 +click==7.0 inflect==1.0.1 marshmallow==2.16.1 -r requirements/dev.txt diff --git a/setup.py b/setup.py index f1fda07..467b040 100644 --- a/setup.py +++ b/setup.py @@ -64,11 +64,11 @@ def read(*names, **kwargs): # eg: 'keyword1', 'keyword2', 'keyword3', ], install_requires=[ - 'click==6.7', + 'click==7.0', 'werkzeug==0.14.1', 'flask==1.0.2', 'inflect==1.0.1', - 'protean==0.0.4', + 'protean==0.0.6', 'marshmallow==2.16.1', # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/src/protean_flask/cli.py b/src/protean_flask/cli.py index f9f2bb9..a29cec2 100644 --- a/src/protean_flask/cli.py +++ b/src/protean_flask/cli.py @@ -17,7 +17,6 @@ import click -@click.command() -@click.argument('names', nargs=-1) -def main(names): - click.echo(repr(names)) +@click.group() +def main(): + """ Utility commands for the Protean-Flask package """ diff --git a/src/protean_flask/core/base.py b/src/protean_flask/core/base.py index 8b1c6d7..75e8bcf 100644 --- a/src/protean_flask/core/base.py +++ b/src/protean_flask/core/base.py @@ -1,5 +1,5 @@ """Module that defines entry point to the Protean Flask Application""" -from flask import Request, request, current_app +from flask import Request, request, current_app, Blueprint from protean.core.exceptions import UsecaseExecutionError from protean.utils.importlib import perform_import @@ -24,15 +24,21 @@ class Protean(object): Alternatively, you can use :meth:`init_app` to set the Flask application after it has been constructed. - :param app: the Flask application object - :type app: flask.Flask or flask.Blueprint + :param app_or_bp: the Flask application or blueprint object. """ - def __init__(self, app=None): - if app is not None: - self.init_app(app) - self.app = app + def __init__(self, app_or_bp=None): + self.app = None + self.blueprint = None + self.viewsets = [] + + if app_or_bp is not None: + self.app = app_or_bp + if isinstance(app_or_bp, Blueprint): + self.blueprint = app_or_bp + else: + self.init_app(app_or_bp) def init_app(self, app): """Perform initialization actions with the given :class:`flask.Flask` @@ -64,11 +70,11 @@ def register_viewset(self, view, endpoint, url, pk_name='identifier', `additional_routes` argument. Note that the route names have to be the same as method names """ - if additional_routes is None: - additional_routes = list() view_func = view.as_view(endpoint) - # Custom Routes + # add the custom routes to the app + if additional_routes is None: + additional_routes = list() for route in additional_routes: self.app.add_url_rule( '{}{}'.format(url, route), view_func=view_func) diff --git a/src/protean_flask/core/serializers.py b/src/protean_flask/core/serializers.py index ad1bbc8..963228a 100644 --- a/src/protean_flask/core/serializers.py +++ b/src/protean_flask/core/serializers.py @@ -45,7 +45,7 @@ def __init__(self, *args, **kwargs): entity_fields = OrderedDict() for field_name, field_obj in \ - self.opts.entity._declared_fields.items(): # pylint: w0212 + self.opts.entity.meta_.declared_fields.items(): if field_name not in self.declared_fields: entity_fields[field_name] = self.build_field(field_obj) diff --git a/src/protean_flask/core/views.py b/src/protean_flask/core/views.py index 43c4530..0c5b395 100644 --- a/src/protean_flask/core/views.py +++ b/src/protean_flask/core/views.py @@ -181,7 +181,7 @@ def _process_request(self, usecase_cls, request_object_cls, payload, """ Process the request by running the Protean Tasklet """ # Get the schema class and derive resource name schema_cls = self.get_schema_cls() - resource = inflection.underscore(schema_cls.opts.entity_cls.__name__) + resource = inflection.underscore(schema_cls.opts_.entity_cls.__name__) # Get the serializer for this class serializer = None @@ -269,8 +269,10 @@ def put(self, identifier): Expected Parameters: identifier = , identifies the entity """ - payload = request.payload - payload.update({'identifier': identifier}) + payload = { + 'identifier': identifier, + 'data': request.payload + } return self._process_request( self.usecase_cls, self.request_object_cls, payload=payload) diff --git a/src/protean_flask/core/viewsets.py b/src/protean_flask/core/viewsets.py index b5abf4b..9c024e5 100644 --- a/src/protean_flask/core/viewsets.py +++ b/src/protean_flask/core/viewsets.py @@ -48,8 +48,10 @@ def put(self, identifier): Expected Parameters: identifier = , identifies the entity """ - payload = request.payload - payload.update({'identifier': identifier}) + payload = { + 'identifier': identifier, + 'data': request.payload + } return self._process_request( self.update_usecase, self.update_request_object, payload=payload) diff --git a/tests/core/test_blueprint.py b/tests/core/test_blueprint.py new file mode 100644 index 0000000..90bdd0f --- /dev/null +++ b/tests/core/test_blueprint.py @@ -0,0 +1,80 @@ +"""Module to test View functionality and features""" + +import json +import pytest + +from protean.core.exceptions import ObjectNotFoundError +from protean.core.repository import repo_factory + +from tests.support.sample_app import app + + +class TestBlueprint: + """Class to test Blueprint functionality of flask with this package""" + + @classmethod + def setup_class(cls): + """ Setup for this test case""" + + # Create the test client + cls.client = app.test_client() + + @classmethod + def teardown_class(cls): + """ Teardown for this test case""" + + # Delete all dog objects + repo_factory.DogSchema.delete_all() + repo_factory.HumanSchema.delete_all() + + def test_show(self): + """ Test retrieving an entity using blueprint ShowAPIResource""" + + # Create a dog object + repo_factory.DogSchema.create(id=5, name='Johnny', owner='John') + + # Fetch this dog by ID + rv = self.client.get('/blueprint/dogs/5') + assert rv.status_code == 200 + + expected_resp = { + 'dog': {'age': 5, 'id': 5, 'name': 'Johnny', 'owner': 'John'} + } + assert rv.json == expected_resp + + # Test search by invalid id + rv = self.client.get('/blueprint/dogs/6') + assert rv.status_code == 404 + + # Delete the dog now + repo_factory.DogSchema.delete(5) + + def test_set_show(self): + """ Test retrieving an entity using the blueprint resource set""" + # Create a human object + repo_factory.HumanSchema.create(id=1, name='John') + + # Fetch this human by ID + rv = self.client.get('/blueprint/humans/1') + assert rv.status_code == 200 + expected_resp = { + 'human': {'contact': None, 'id': 1, 'name': 'John'} + } + assert rv.json == expected_resp + + # Delete the human now + repo_factory.HumanSchema.delete(1) + + def test_custom_route(self): + """ Test custom routes using the blueprint resource set """ + + # Create a human object + repo_factory.HumanSchema.create(id=1, name='John') + repo_factory.DogSchema.create(id=5, name='Johnny', owner='John') + + # Get the custom route + rv = self.client.get('/humans/1/my_dogs') + assert rv.status_code == 200 + assert rv.json['total'] == 1 + assert rv.json['dogs'][0] == \ + {'age': 5, 'id': 5, 'name': 'Johnny', 'owner': 'John'} diff --git a/tests/support/sample_app/__init__.py b/tests/support/sample_app/__init__.py index e2fb994..541805a 100644 --- a/tests/support/sample_app/__init__.py +++ b/tests/support/sample_app/__init__.py @@ -4,6 +4,7 @@ from protean_flask import Protean from .views import ShowDogResource, CreateDogResource, UpdateDogResource, \ DeleteDogResource, ListDogResource, flask_view, HumanResourceSet +from .blueprint import blueprint app = Flask(__name__) api = Protean(app) @@ -27,3 +28,5 @@ methods=['GET']) api.register_viewset(HumanResourceSet, 'humans', '/humans', pk_type='int', additional_routes=['//my_dogs']) + +app.register_blueprint(blueprint, url_prefix='/blueprint') diff --git a/tests/support/sample_app/blueprint.py b/tests/support/sample_app/blueprint.py new file mode 100644 index 0000000..865d688 --- /dev/null +++ b/tests/support/sample_app/blueprint.py @@ -0,0 +1,52 @@ +""" Blueprints of the sample app""" +from flask import Blueprint + +from protean_flask import Protean +from protean_flask.core.views import ShowAPIResource +from protean_flask.core.viewsets import GenericAPIResourceSet + +from .serializers import DogSerializer, HumanSerializer +from .schemas import DogSchema, HumanSchema +from .usecases import ListMyDogsUsecase, ListMyDogsRequestObject + +blueprint = Blueprint('test_blueprint', __name__) +api_bp = Protean(blueprint) + + +class ShowDogResource(ShowAPIResource): + """ View for retrieving a Dog by its ID""" + schema_cls = DogSchema + serializer_cls = DogSerializer + + +class HumanResourceSet(GenericAPIResourceSet): + """ Resource Set for the Human Entity""" + schema_cls = HumanSchema + serializer_cls = HumanSerializer + + def my_dogs(self, identifier): + """ List all the dogs belonging to the Human""" + # Run the usecase and get the related dogs + payload = {'identifier': identifier} + response_object = self._process_request( + ListMyDogsUsecase, ListMyDogsRequestObject, payload=payload, + no_serialization=True) + + # Serialize the results and return the response + serializer = DogSerializer(many=True) + items = serializer.dump(response_object.value.items) + result = { + 'dogs': items.data, + 'total': response_object.value.total, + 'page': response_object.value.page + } + return result, response_object.code.value + + +# Setup the routes +blueprint.add_url_rule( + '/dogs/', view_func=ShowDogResource.as_view('show_dog'), + methods=['GET']) +api_bp.register_viewset( + HumanResourceSet, 'humans', '/humans', pk_type='int', + additional_routes=['//my_dogs']) diff --git a/tests/test_protean_flask.py b/tests/test_protean_flask.py index e140bba..71f0099 100644 --- a/tests/test_protean_flask.py +++ b/tests/test_protean_flask.py @@ -10,5 +10,5 @@ def test_main(): runner = CliRunner() result = runner.invoke(main, []) - assert result.output == '()\n' + assert 'Utility commands for the Protean-Flask package' in result.output assert result.exit_code == 0