Skip to content

Commit

Permalink
Revert "BROKEN: Starting work on 5.0.0."
Browse files Browse the repository at this point in the history
This reverts commit 62c7706.
  • Loading branch information
Anderycks committed Feb 8, 2017
1 parent 5461eed commit 0068a4b
Show file tree
Hide file tree
Showing 17 changed files with 91 additions and 63 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 73 additions & 33 deletions sqlalchemy_jsonapi/serializer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""SQLAlchemy-JSONAPI Serializer.
"""
SQLAlchemy-JSONAPI
Serializer
Colton J. Provias
MIT License
"""
Expand All @@ -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 = {
Expand All @@ -44,26 +52,58 @@ 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

return wrapped


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.
Expand All @@ -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'},
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions tests/app.py → sqlalchemy_jsonapi/tests/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
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)
assert response.json_data['data'][0]['type'] == 'blog-posts'
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'
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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


Expand All @@ -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']]
Expand Down Expand Up @@ -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)
Expand All @@ -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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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
Expand Down
File renamed without changes.
Binary file removed test.png
Binary file not shown.

0 comments on commit 0068a4b

Please sign in to comment.