Skip to content

Commit

Permalink
Merge pull request #54 from igieon/custom_query_manager
Browse files Browse the repository at this point in the history
Ability to use custom query string manager
  • Loading branch information
mahenzon authored Jun 15, 2021
2 parents dfe00fb + 6e5424d commit 620a96f
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 29 deletions.
8 changes: 8 additions & 0 deletions docs/resource_manager.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ Flask-COMBO-JSONAPI provides three kinds of resource managers with default metho

You can rewrite each default method implementation to customize it. If you rewrite all default methods of a resource manager or if you rewrite a method and disable access to others, you don't have to set any attributes of your resource manager.

All url are pased via helper class **QueryStringManager**, which make parsing url query string according json-api. If you want override implementation class used you can do it for 1 resource via attribute.
:qs_manager_class: default implementation via **QueryStringManager**

or globally via:
.. code-block::python
api = Api(blueprint=api_blueprint, qs_manager_class=CustomQS)
Required attributes
-------------------

Expand Down
8 changes: 7 additions & 1 deletion flask_combo_jsonapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
class Api(object):
"""The main class of the Api"""

def __init__(self, app=None, blueprint=None, decorators=None, plugins=None):
def __init__(self, app=None, blueprint=None, decorators=None, plugins=None, qs_manager_class=None):
"""Initialize an instance of the Api
:param app: the flask application
:param blueprint: a flask blueprint
:param tuple decorators: a tuple of decorators plugged to each resource methods
:param plugins: list of plugins
:param qs_manager_class: custom query string manager used in whole API
"""
self.app = app
self._app = app
Expand All @@ -29,6 +31,7 @@ def __init__(self, app=None, blueprint=None, decorators=None, plugins=None):
self.resource_registry = []
self.decorators = decorators or tuple()
self.plugins = plugins if plugins is not None else []
self.qs_manager_class = qs_manager_class

if app is not None:
self.init_app(app, blueprint)
Expand Down Expand Up @@ -82,6 +85,9 @@ def route(self, resource, view, *urls, **kwargs):
pass
setattr(resource, 'plugins', self.plugins)

if self.qs_manager_class:
setattr(resource, 'qs_manager_class', self.qs_manager_class)

resource.view = view
url_rule_options = kwargs.get('url_rule_options') or dict()

Expand Down
12 changes: 7 additions & 5 deletions flask_combo_jsonapi/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def __new__(cls, name, bases, d):
class Resource(MethodView):
"""Base resource class"""

qs_manager_class = QSManager

def __new__(cls):
"""Constructor of a resource instance"""
if hasattr(cls, "_data_layer"):
Expand Down Expand Up @@ -117,7 +119,7 @@ def get(self, *args, **kwargs):
"""Retrieve a collection of objects"""
self.before_get(args, kwargs)

qs = QSManager(request.args, self.schema)
qs = self.qs_manager_class(request.args, self.schema)

objects_count, objects = self.get_collection(qs, kwargs)

Expand Down Expand Up @@ -152,7 +154,7 @@ def post(self, *args, **kwargs):
"""Create an object"""
json_data = request.json or {}

qs = QSManager(request.args, self.schema)
qs = self.qs_manager_class(request.args, self.schema)

schema = compute_schema(self.schema, getattr(self, "post_schema_kwargs", dict()), qs, qs.include)

Expand Down Expand Up @@ -231,7 +233,7 @@ def get(self, *args, **kwargs):
"""Get object details"""
self.before_get(args, kwargs)

qs = QSManager(request.args, self.schema)
qs = self.qs_manager_class(request.args, self.schema)

obj = self.get_object(kwargs, qs)

Expand Down Expand Up @@ -263,7 +265,7 @@ def patch(self, *args, **kwargs):
"""Update an object"""
json_data = request.json or {}

qs = QSManager(request.args, self.schema)
qs = self.qs_manager_class(request.args, self.schema)
schema_kwargs = getattr(self, "patch_schema_kwargs", dict())
schema_kwargs.update({"partial": True})

Expand Down Expand Up @@ -385,7 +387,7 @@ def get(self, *args, **kwargs):
"data": data,
}

qs = QSManager(request.args, self.schema)
qs = self.qs_manager_class(request.args, self.schema)
if qs.include:
schema = compute_schema(self.schema, dict(), qs, qs.include)

Expand Down
166 changes: 143 additions & 23 deletions tests/test_sqlalchemy_data_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def engine(
person_tag_model, person_single_tag_model, person_model,
computer_model, string_json_attribute_person_model,
address_model
):
):
engine = create_engine("sqlite:///:memory:")
person_tag_model.metadata.create_all(engine)
person_single_tag_model.metadata.create_all(engine)
Expand Down Expand Up @@ -208,6 +208,17 @@ def computer(session, computer_model):
session_.commit()


@pytest.fixture()
def computer_2(session, computer_model):
computer_ = computer_model(serial="2")
session_ = session
session_.add(computer_)
session_.commit()
yield computer_
session_.delete(computer_)
session_.commit()


@pytest.fixture()
def address(session, address_model):
address_ = address_model(state='NYC')
Expand Down Expand Up @@ -400,7 +411,7 @@ class PersonList(ResourceList):
data_layer = {
"model": person_model,
"session": session,
"mzthods": {"before_create_object": before_create_object},
"methods": {"before_create_object": before_create_object},
}
get_decorators = [dummy_decorator]
post_decorators = [dummy_decorator]
Expand All @@ -410,6 +421,43 @@ class PersonList(ResourceList):
yield PersonList


@pytest.fixture(scope="module")
def custom_query_string_manager():
class QS(QSManager):
def _simple_filters(self, dict_):
return [{"name": key, "op": "in" if isinstance(value, list) else "eq", "val": value}
for (key, value) in dict_.items()]

yield QS


@pytest.fixture(scope="module")
def person_list_custom_qs_manager(session, person_model, person_schema, custom_query_string_manager):
class PersonList(ResourceList):
schema = person_schema
data_layer = {
"model": person_model,
"session": session,
}
get_schema_kwargs = dict()
qs_manager_class = custom_query_string_manager

yield PersonList


@pytest.fixture(scope="module")
def person_list_2(session, person_model, person_schema):
class PersonList(ResourceList):
schema = person_schema
data_layer = {
"model": person_model,
"session": session,
}
get_schema_kwargs = dict()

yield PersonList


@pytest.fixture(scope="module")
def person_detail(session, person_model, dummy_decorator, person_schema, before_update_object, before_delete_object):
class PersonDetail(ResourceDetail):
Expand Down Expand Up @@ -507,7 +555,7 @@ def fixed_count_for_collection_count():

@pytest.fixture(scope="module")
def computer_list_resource_with_disable_collection_count(
session, computer_model, computer_schema, fixed_count_for_collection_count
session, computer_model, computer_schema, fixed_count_for_collection_count
):
class ComputerList(ResourceList):
disable_collection_count = True, fixed_count_for_collection_count
Expand Down Expand Up @@ -538,7 +586,7 @@ class ComputerOwnerRelationship(ResourceRelationship):

@pytest.fixture(scope="module")
def string_json_attribute_person_detail(
session, string_json_attribute_person_model, string_json_attribute_person_schema
session, string_json_attribute_person_model, string_json_attribute_person_schema
):
class StringJsonAttributePersonDetail(ResourceDetail):
schema = string_json_attribute_person_schema
Expand All @@ -562,27 +610,42 @@ def api_blueprint(client):
yield bp


@pytest.fixture(scope="module")
def register_routes_custom_qs(
client,
app,
api_blueprint,
custom_query_string_manager,
person_list_2,
):
api = Api(blueprint=api_blueprint, qs_manager_class=custom_query_string_manager)
api.route(person_list_2, "person_list_qs", "/qs/persons")
api.init_app(app)


@pytest.fixture(scope="module")
def register_routes(
client,
app,
api_blueprint,
person_list,
person_detail,
person_computers,
person_list_raise_jsonapiexception,
person_list_raise_exception,
person_list_response,
person_list_without_schema,
computer_list,
computer_detail,
computer_list_resource_with_disable_collection_count,
computer_owner,
string_json_attribute_person_detail,
string_json_attribute_person_list,
client,
app,
api_blueprint,
person_list,
person_detail,
person_computers,
person_list_custom_qs_manager,
person_list_raise_jsonapiexception,
person_list_raise_exception,
person_list_response,
person_list_without_schema,
computer_list,
computer_detail,
computer_list_resource_with_disable_collection_count,
computer_owner,
string_json_attribute_person_detail,
string_json_attribute_person_list,
):
api = Api(blueprint=api_blueprint)
api.route(person_list, "person_list", "/persons")
api.route(person_list_custom_qs_manager, "person_list_custom_qs_manager", "/persons_qs")
api.route(person_detail, "person_detail", "/persons/<int:person_id>")
api.route(person_computers, "person_computers", "/persons/<int:person_id>/relationships/computers")
api.route(person_computers, "person_computers_owned", "/persons/<int:person_id>/relationships/computers-owned")
Expand Down Expand Up @@ -775,6 +838,47 @@ def test_get_list_with_simple_filter(client, register_routes, person, person_2):
)
response = client.get("/persons" + "?" + querystring, content_type="application/vnd.api+json")
assert response.status_code == 200
assert response.json["meta"]["count"] == 1


def test_get_list_with_simple_filter_relationship_custom_qs(session, client, register_routes, person, person_2,
computer, computer_2):
computer.person = person
computer_2.person = person_2
session.commit()
with client:
querystring = urlencode(
{
"filter[computers.id]": f'{computer_2.id},{computer.id}',
"include": "computers",
"sort": "-name",
}
)
response = client.get("/persons_qs" + "?" + querystring, content_type="application/vnd.api+json")
assert response.status_code == 200
assert len(response.json['data']) == 2
assert response.json['data'][0]['id'] == str(person_2.person_id)
assert response.json['data'][1]['id'] == str(person.person_id)


def test_get_list_with_simple_filter_relationship_custom_qs_api(session, client, register_routes_custom_qs, person,
person_2, computer, computer_2):
computer.person = person
computer_2.person = person_2
session.commit()
with client:
querystring = urlencode(
{
"filter[computers.id]": f'{computer_2.id},{computer.id}',
"include": "computers",
"sort": "-name",
}
)
response = client.get("/qs/persons" + "?" + querystring, content_type="application/vnd.api+json")
assert response.status_code == 200
assert len(response.json['data']) == 2
assert response.json['data'][0]['id'] == str(person_2.person_id)
assert response.json['data'][1]['id'] == str(person.person_id)


def test_get_list_disable_pagination(client, register_routes):
Expand Down Expand Up @@ -1154,6 +1258,22 @@ class PersonDetail(ResourceDetail):
PersonDetail()


def test_get_list_with_simple_filter_relationship_error(session, client, register_routes, person, person_2,
computer, computer_2):
computer.person = person
computer_2.person = person_2
session.commit()
with client:
querystring = urlencode(
{
"filter[computers.id]": f'{computer_2.id},{computer.id}',
"include": "computers"
}
)
response = client.get("/persons" + "?" + querystring, content_type="application/vnd.api+json")
assert response.status_code == 500


def test_get_list_jsonapiexception(client, register_routes):
with client:
response = client.get("/persons_jsonapiexception", content_type="application/vnd.api+json")
Expand Down Expand Up @@ -1547,7 +1667,7 @@ def test_post_relationship_missing_type(client, register_routes, computer, perso


def test_post_relationship_missing_id(client, register_routes, computer, person):
payload = {"data": [{"type": "computer",}]}
payload = {"data": [{"type": "computer", }]}

with client:
response = client.post(
Expand Down Expand Up @@ -1629,7 +1749,7 @@ def test_patch_relationship_missing_type(client, register_routes, computer, pers


def test_patch_relationship_missing_id(client, register_routes, computer, person):
payload = {"data": [{"type": "computer",}]}
payload = {"data": [{"type": "computer", }]}

with client:
response = client.patch(
Expand Down Expand Up @@ -1711,7 +1831,7 @@ def test_delete_relationship_missing_type(client, register_routes, computer, per


def test_delete_relationship_missing_id(client, register_routes, computer, person):
payload = {"data": [{"type": "computer",}]}
payload = {"data": [{"type": "computer", }]}

with client:
response = client.delete(
Expand Down

0 comments on commit 620a96f

Please sign in to comment.