From 0068a4b922ce99569668e5b0a986d31b79815442 Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH] Revert "BROKEN: Starting work on 5.0.0." This reverts commit 62c7706e612051523898309893b1e8d3bb8714d6. --- README.md | 2 - requirements.txt | 1 + sqlalchemy_jsonapi/serializer.py | 106 ++++++++++++------ {tests => sqlalchemy_jsonapi/tests}/app.py | 12 +- .../tests}/conftest.py | 0 .../tests}/test_collection_get.py | 27 ++--- .../tests}/test_collection_post.py | 0 .../tests}/test_related_get.py | 0 .../tests}/test_relationship_delete.py | 0 .../tests}/test_relationship_get.py | 0 .../tests}/test_relationship_patch.py | 0 .../tests}/test_relationship_post.py | 0 .../tests}/test_resource_delete.py | 0 .../tests}/test_resource_get.py | 0 .../tests}/test_resource_patch.py | 6 +- .../tests}/test_serializer.py | 0 test.png | Bin 550 -> 0 bytes 17 files changed, 91 insertions(+), 63 deletions(-) rename {tests => sqlalchemy_jsonapi/tests}/app.py (94%) rename {tests => sqlalchemy_jsonapi/tests}/conftest.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_collection_get.py (95%) rename {tests => sqlalchemy_jsonapi/tests}/test_collection_post.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_related_get.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_relationship_delete.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_relationship_get.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_relationship_patch.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_relationship_post.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_resource_delete.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_resource_get.py (100%) rename {tests => sqlalchemy_jsonapi/tests}/test_resource_patch.py (96%) rename {tests => sqlalchemy_jsonapi/tests}/test_serializer.py (100%) delete mode 100644 test.png diff --git a/README.md b/README.md index f74e78d..476c826 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [![Build Status](https://travis-ci.org/ColtonProvias/sqlalchemy-jsonapi.svg?branch=master)](https://travis-ci.org/ColtonProvias/sqlalchemy-jsonapi) -**WARNING: The master branch is currently breaking backwards compatibility and thus has been bumped to 5.0.0. Builds are likely to fail during 5.0.0 development.** - [JSON API](http://jsonapi.org/) implementation for use with [SQLAlchemy](http://www.sqlalchemy.org/). diff --git a/requirements.txt b/requirements.txt index 002def7..9b44940 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ # # pip-compile --output-file requirements.txt requirements.in # + bcrypt==2.0.0 blinker==1.4 cffi==1.7.0 # via bcrypt diff --git a/sqlalchemy_jsonapi/serializer.py b/sqlalchemy_jsonapi/serializer.py index 74c101e..1185c87 100644 --- a/sqlalchemy_jsonapi/serializer.py +++ b/sqlalchemy_jsonapi/serializer.py @@ -1,5 +1,6 @@ -"""SQLAlchemy-JSONAPI Serializer. - +""" +SQLAlchemy-JSONAPI +Serializer Colton J. Provias MIT License """ @@ -18,22 +19,29 @@ from ._version import __version__ -class Actions(Enum): - """ The actions that can be performed on an attribute or relationship. """ +class AttributeActions(Enum): + """ The actions that can be done to an attribute. """ + + GET = 0 + SET = 1 - GET = 1 - APPEND = 2 - SET = 3 - REMOVE = 4 + +class RelationshipActions(Enum): + """ The actions that can be performed on a relationship. """ + + GET = 10 + APPEND = 11 + SET = 12 + DELETE = 13 class Permissions(Enum): """ The permissions that can be set. """ - VIEW = 1 - CREATE = 2 - EDIT = 3 - DELETE = 4 + VIEW = 100 + CREATE = 101 + EDIT = 102 + DELETE = 103 ALL_PERMISSIONS = { @@ -44,15 +52,45 @@ class Permissions(Enum): } -def jsonapi_action(action, *names): - if isinstance(action, Actions): +def attr_descriptor(action, *names): + """ + Wrap a function that allows for getting or setting of an attribute. This + allows for specific handling of an attribute when it comes to serializing + and deserializing. + + :param action: The AttributeActions that this descriptor performs + :param names: A list of names of the attributes this references + """ + if isinstance(action, AttributeActions): action = [action] def wrapped(fn): if not hasattr(fn, '__jsonapi_action__'): fn.__jsonapi_action__ = set() - fn.__jsonapi_desc__ = set() - fn.__jsonapi_desc__ |= set(names) + fn.__jsonapi_desc_for_attrs__ = set() + fn.__jsonapi_desc_for_attrs__ |= set(names) + fn.__jsonapi_action__ |= set(action) + return fn + + return wrapped + + +def relationship_descriptor(action, *names): + """ + Wrap a function for modification of a relationship. This allows for + specific handling for serialization and deserialization. + + :param action: The RelationshipActions that this descriptor performs + :param names: A list of names of the relationships this references + """ + if isinstance(action, RelationshipActions): + action = [action] + + def wrapped(fn): + if not hasattr(fn, '__jsonapi_action__'): + fn.__jsonapi_action__ = set() + fn.__jsonapi_desc_for_rels__ = set() + fn.__jsonapi_desc_for_rels__ |= set(names) fn.__jsonapi_action__ |= set(action) return fn @@ -60,10 +98,12 @@ def wrapped(fn): class PermissionTest(object): - """Authorize access to a model, resource, or specific field.""" + """ Authorize access to a model, resource, or specific field. """ def __init__(self, permission, *names): - """Decorate a function that returns a boolean representing access. + """ + Decorates a function that returns a boolean representing if access is + allowed. :param permission: The permission to check for :param names: The names to test for. None represents the model. @@ -88,14 +128,14 @@ def __call__(self, fn): return fn #: More consistent name for the decorators -jsonapi_access = PermissionTest +permission_test = PermissionTest class JSONAPIResponse(object): - """Wrapper for JSON API Responses.""" + """ Wrapper for JSON API Responses. """ def __init__(self): - """Default the status code and data.""" + """ Default the status code and data. """ self.status_code = 200 self.data = { 'jsonapi': {'version': '1.0'}, @@ -118,7 +158,8 @@ def get_permission_test(model, field, permission, instance=None): def check_permission(instance, field, permission): """ - Check a permission for a given instance or field. Raises error if denied. + Check a permission for a given instance or field. Raises an error if + denied. :param instance: The instance to check :param field: The field name to check or None for instance @@ -134,10 +175,10 @@ def get_attr_desc(instance, attribute, action): :param instance: Model instance :param attribute: Name of the attribute - :param action: Action + :param action: AttributeAction """ descs = instance.__jsonapi_attribute_descriptors__.get(attribute, {}) - if action == Actions.GET: + if action == AttributeActions.GET: check_permission(instance, attribute, Permissions.VIEW) return descs.get(action, lambda x: getattr(x, attribute)) check_permission(instance, attribute, Permissions.EDIT) @@ -153,13 +194,13 @@ def get_rel_desc(instance, key, action): :param action: RelationshipAction """ descs = instance.__jsonapi_rel_desc__.get(key, {}) - if action == Actions.GET: + if action == RelationshipActions.GET: check_permission(instance, key, Permissions.VIEW) return descs.get(action, lambda x: getattr(x, key)) - elif action == Actions.APPEND: + elif action == RelationshipActions.APPEND: check_permission(instance, key, Permissions.CREATE) return descs.get(action, lambda x, v: getattr(x, key).append(v)) - elif action == Actions.SET: + elif action == RelationshipActions.SET: check_permission(instance, key, Permissions.EDIT) return descs.get(action, lambda x, v: setattr(x, key, v)) else: @@ -259,8 +300,7 @@ def _lazy_relationship(self, api_type, obj_id, rel_key): return { 'self': '{}/{}/{}/relationships/{}'.format(self.prefix, api_type, obj_id, rel_key), - 'related': '{}/{}/{}/{}'.format(self.prefix, api_type, obj_id, - rel_key) + 'related': '{}/{}/{}/{}'.format(self.prefix, api_type, obj_id, rel_key) } def _get_relationship(self, resource, rel_key, permission): @@ -327,8 +367,8 @@ def _render_full_resource(self, instance, include, fields): attrs_to_ignore = {'__mapper__', 'id'} if api_type in fields.keys(): local_fields = list(map(( - lambda x: instance.__jsonapi_map_to_py__.get(x)), fields.get( - api_type))) + lambda x: instance.__jsonapi_map_to_py__[x]), fields[ + api_type])) else: local_fields = orm_desc_keys @@ -339,7 +379,7 @@ def _render_full_resource(self, instance, include, fields): api_key = instance.__jsonapi_map_to_api__[key] try: - desc = get_rel_desc(instance, key, Actions.GET) + desc = get_rel_desc(instance, key, RelationshipActions.GET) except PermissionDeniedError: continue @@ -406,7 +446,7 @@ def _render_full_resource(self, instance, include, fields): for key in set(orm_desc_keys) - attrs_to_ignore: try: - desc = get_attr_desc(instance, key, Actions.GET) + desc = get_attr_desc(instance, key, AttributeActions.GET) if key in local_fields: to_ret['attributes'][instance.__jsonapi_map_to_api__[ key]] = desc(instance) diff --git a/tests/app.py b/sqlalchemy_jsonapi/tests/app.py similarity index 94% rename from tests/app.py rename to sqlalchemy_jsonapi/tests/app.py index 74e0f1a..ab262ce 100644 --- a/tests/app.py +++ b/sqlalchemy_jsonapi/tests/app.py @@ -74,19 +74,19 @@ def validate_password(self, key, password): assert len(password) >= 5, 'Password must be 5 characters or longer.' return password - @jsonapi_access(Permissions.VIEW, 'password') + @permission_test(Permissions.VIEW, 'password') def view_password(self): """ Never let the password be seen. """ return False - @jsonapi_access(Permissions.EDIT) + @permission_test(Permissions.EDIT) def prevent_edit(self): """ Prevent editing for no reason. """ if request.view_args['api_type'] == 'blog-posts': return True return False - @jsonapi_access(Permissions.DELETE) + @permission_test(Permissions.DELETE) def allow_delete(self): """ Just like a popular social media site, we won't delete users. """ return False @@ -115,12 +115,12 @@ def validate_title(self, key, title): title) <= 100, 'Must be 5 to 100 characters long.' return title - @jsonapi_access(Permissions.VIEW) + @permission_test(Permissions.VIEW) def allow_view(self): """ Hide unpublished. """ return self.is_published - @jsonapi_access(INTERACTIVE_PERMISSIONS, 'logs') + @permission_test(INTERACTIVE_PERMISSIONS, 'logs') def prevent_altering_of_logs(self): return False @@ -157,7 +157,7 @@ class Log(Timestamp, db.Model): lazy='joined', backref=backref('logs', lazy='dynamic')) - @jsonapi_access(INTERACTIVE_PERMISSIONS) + @permission_test(INTERACTIVE_PERMISSIONS) def block_interactive(cls): return False diff --git a/tests/conftest.py b/sqlalchemy_jsonapi/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to sqlalchemy_jsonapi/tests/conftest.py diff --git a/tests/test_collection_get.py b/sqlalchemy_jsonapi/tests/test_collection_get.py similarity index 95% rename from tests/test_collection_get.py rename to sqlalchemy_jsonapi/tests/test_collection_get.py index 9cc3903..a66a7e6 100644 --- a/tests/test_collection_get.py +++ b/sqlalchemy_jsonapi/tests/test_collection_get.py @@ -1,7 +1,11 @@ from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError -# TODO: Vanilla +# TODO: Ember-style filtering +# TODO: Simple filtering +# TODO: Complex filtering +# TODO: Bad query param + def test_200_with_no_querystring(bunch_of_posts, client): response = client.get('/api/blog-posts').validate(200) @@ -9,12 +13,6 @@ def test_200_with_no_querystring(bunch_of_posts, client): assert response.json_data['data'][0]['id'] -# TODO: Bad Query Param - - -# TODO: Resource Inclusions - - def test_200_with_single_included_model(bunch_of_posts, client): response = client.get('/api/blog-posts/?include=author').validate(200) assert response.json_data['data'][0]['type'] == 'blog-posts' @@ -29,6 +27,7 @@ def test_200_with_including_model_and_including_inbetween(bunch_of_posts, for data in response.json_data['included']: assert data['type'] in ['blog-posts', 'users'] + def test_200_with_multiple_includes(bunch_of_posts, client): response = client.get('/api/blog-posts/?include=comments,author').validate( 200) @@ -37,9 +36,6 @@ def test_200_with_multiple_includes(bunch_of_posts, client): assert data['type'] in ['blog-comments', 'users'] -# TODO: Sparse Fieldsets - - def test_200_with_single_field(bunch_of_posts, client): response = client.get( '/api/blog-posts/?fields[blog-posts]=title').validate(200) @@ -51,7 +47,7 @@ def test_200_with_bad_field(bunch_of_posts, client): response = client.get( '/api/blog-posts/?fields[blog-posts]=titles').validate(200) for item in response.json_data['data']: - assert set() == set(item['attributes'].keys()) + assert {} == set(item['attributes'].keys()) assert len(item['relationships']) == 0 @@ -78,9 +74,6 @@ def test_200_with_single_field_across_a_relationship(bunch_of_posts, client): assert {'author'} == set(item['relationships'].keys()) -# TODO: Sorting - - def test_200_sorted_response(bunch_of_posts, client): response = client.get('/api/blog-posts/?sort=title').validate(200) title_list = [x['attributes']['title'] for x in response.json_data['data']] @@ -108,9 +101,6 @@ def test_409_when_given_a_missing_field_for_sorting(bunch_of_posts, client): 409, NotSortableError) -# TODO: Pagination - - def test_200_paginated_response_by_page(bunch_of_posts, client): response = client.get( '/api/blog-posts/?page[number]=2&page[size]=5').validate(200) @@ -131,6 +121,3 @@ def test_200_when_pagination_is_out_of_range(bunch_of_posts, client): def test_400_when_provided_crap_data_for_pagination(bunch_of_posts, client): client.get('/api/blog-posts/?page[offset]=5&page[limit]=crap').validate( 400, BadRequestError) - - -# TODO: Filtering diff --git a/tests/test_collection_post.py b/sqlalchemy_jsonapi/tests/test_collection_post.py similarity index 100% rename from tests/test_collection_post.py rename to sqlalchemy_jsonapi/tests/test_collection_post.py diff --git a/tests/test_related_get.py b/sqlalchemy_jsonapi/tests/test_related_get.py similarity index 100% rename from tests/test_related_get.py rename to sqlalchemy_jsonapi/tests/test_related_get.py diff --git a/tests/test_relationship_delete.py b/sqlalchemy_jsonapi/tests/test_relationship_delete.py similarity index 100% rename from tests/test_relationship_delete.py rename to sqlalchemy_jsonapi/tests/test_relationship_delete.py diff --git a/tests/test_relationship_get.py b/sqlalchemy_jsonapi/tests/test_relationship_get.py similarity index 100% rename from tests/test_relationship_get.py rename to sqlalchemy_jsonapi/tests/test_relationship_get.py diff --git a/tests/test_relationship_patch.py b/sqlalchemy_jsonapi/tests/test_relationship_patch.py similarity index 100% rename from tests/test_relationship_patch.py rename to sqlalchemy_jsonapi/tests/test_relationship_patch.py diff --git a/tests/test_relationship_post.py b/sqlalchemy_jsonapi/tests/test_relationship_post.py similarity index 100% rename from tests/test_relationship_post.py rename to sqlalchemy_jsonapi/tests/test_relationship_post.py diff --git a/tests/test_resource_delete.py b/sqlalchemy_jsonapi/tests/test_resource_delete.py similarity index 100% rename from tests/test_resource_delete.py rename to sqlalchemy_jsonapi/tests/test_resource_delete.py diff --git a/tests/test_resource_get.py b/sqlalchemy_jsonapi/tests/test_resource_get.py similarity index 100% rename from tests/test_resource_get.py rename to sqlalchemy_jsonapi/tests/test_resource_get.py diff --git a/tests/test_resource_patch.py b/sqlalchemy_jsonapi/tests/test_resource_patch.py similarity index 96% rename from tests/test_resource_patch.py rename to sqlalchemy_jsonapi/tests/test_resource_patch.py index d15a73a..5ee3b40 100644 --- a/tests/test_resource_patch.py +++ b/sqlalchemy_jsonapi/tests/test_resource_patch.py @@ -1,8 +1,10 @@ import json from uuid import uuid4 -from sqlalchemy_jsonapi.errors import (BadRequestError, PermissionDeniedError, - ResourceNotFoundError, ValidationError) +from sqlalchemy_jsonapi.errors import ( + BadRequestError, PermissionDeniedError, ResourceNotFoundError, + RelatedResourceNotFoundError, RelationshipNotFoundError, ValidationError, + MissingTypeError) # TODO: Sparse Fieldsets diff --git a/tests/test_serializer.py b/sqlalchemy_jsonapi/tests/test_serializer.py similarity index 100% rename from tests/test_serializer.py rename to sqlalchemy_jsonapi/tests/test_serializer.py diff --git a/test.png b/test.png deleted file mode 100644 index ba872e25f55e7fb1b48fb481936187ff8ef5bf03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 550 zcmV+>0@?kEP)00Bw?00000Q&KCa0005)Nkl)gH!5%DBShkSTo89=tg%fz z8S&e*Bsl7*qmDZ2=#i+^80&5{Ge~WFIb49Fub{IeX#=wfn3VwrNs=DXV+1(*3YwaQ z<_&B~AF>6w^IJb!9o-{N09@@|0v!EwjK8b}*HLi-k^Y4MNB;s1l2(8?4FZNzt)x=H zju+tQGjZQXFo=1({j}{_lGe9tb+lx{`|GE0F~#%6^tj!DR!6J1Y6oAFptONjhLRmi zAMf7k2)!Edd@)HGTiTv2464h1w0>E%8b``hM}>W znXnV|t|u%$BJ7UZS_kjm>I7}x+6LXFKH%}l{It42?|Q