Skip to content

Commit

Permalink
BROKEN: Starting work on 5.0.0.
Browse files Browse the repository at this point in the history
  • Loading branch information
ColtonProvias committed Aug 15, 2016
1 parent 7195452 commit 62c7706
Show file tree
Hide file tree
Showing 17 changed files with 63 additions and 91 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[![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: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#
# 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: 33 additions & 73 deletions sqlalchemy_jsonapi/serializer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
SQLAlchemy-JSONAPI
Serializer
"""SQLAlchemy-JSONAPI Serializer.
Colton J. Provias
MIT License
"""
Expand All @@ -19,29 +18,22 @@
from ._version import __version__


class AttributeActions(Enum):
""" The actions that can be done to an attribute. """

GET = 0
SET = 1
class Actions(Enum):
""" The actions that can be performed on an attribute or relationship. """


class RelationshipActions(Enum):
""" The actions that can be performed on a relationship. """

GET = 10
APPEND = 11
SET = 12
DELETE = 13
GET = 1
APPEND = 2
SET = 3
REMOVE = 4


class Permissions(Enum):
""" The permissions that can be set. """

VIEW = 100
CREATE = 101
EDIT = 102
DELETE = 103
VIEW = 1
CREATE = 2
EDIT = 3
DELETE = 4


ALL_PERMISSIONS = {
Expand All @@ -52,58 +44,26 @@ class Permissions(Enum):
}


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):
def jsonapi_action(action, *names):
if isinstance(action, Actions):
action = [action]

def wrapped(fn):
if not hasattr(fn, '__jsonapi_action__'):
fn.__jsonapi_action__ = set()
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_desc__ = set()
fn.__jsonapi_desc__ |= 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):
"""
Decorates a function that returns a boolean representing if access is
allowed.
"""Decorate a function that returns a boolean representing access.
:param permission: The permission to check for
:param names: The names to test for. None represents the model.
Expand All @@ -128,14 +88,14 @@ def __call__(self, fn):
return fn

#: More consistent name for the decorators
permission_test = PermissionTest
jsonapi_access = 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 @@ -158,8 +118,7 @@ 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 an error if
denied.
Check a permission for a given instance or field. Raises error if denied.
:param instance: The instance to check
:param field: The field name to check or None for instance
Expand All @@ -175,10 +134,10 @@ def get_attr_desc(instance, attribute, action):
:param instance: Model instance
:param attribute: Name of the attribute
:param action: AttributeAction
:param action: Action
"""
descs = instance.__jsonapi_attribute_descriptors__.get(attribute, {})
if action == AttributeActions.GET:
if action == Actions.GET:
check_permission(instance, attribute, Permissions.VIEW)
return descs.get(action, lambda x: getattr(x, attribute))
check_permission(instance, attribute, Permissions.EDIT)
Expand All @@ -194,13 +153,13 @@ def get_rel_desc(instance, key, action):
:param action: RelationshipAction
"""
descs = instance.__jsonapi_rel_desc__.get(key, {})
if action == RelationshipActions.GET:
if action == Actions.GET:
check_permission(instance, key, Permissions.VIEW)
return descs.get(action, lambda x: getattr(x, key))
elif action == RelationshipActions.APPEND:
elif action == Actions.APPEND:
check_permission(instance, key, Permissions.CREATE)
return descs.get(action, lambda x, v: getattr(x, key).append(v))
elif action == RelationshipActions.SET:
elif action == Actions.SET:
check_permission(instance, key, Permissions.EDIT)
return descs.get(action, lambda x, v: setattr(x, key, v))
else:
Expand Down Expand Up @@ -300,7 +259,8 @@ 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 @@ -367,8 +327,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__[x]), fields[
api_type]))
lambda x: instance.__jsonapi_map_to_py__.get(x)), fields.get(
api_type)))
else:
local_fields = orm_desc_keys

Expand All @@ -379,7 +339,7 @@ def _render_full_resource(self, instance, include, fields):
api_key = instance.__jsonapi_map_to_api__[key]

try:
desc = get_rel_desc(instance, key, RelationshipActions.GET)
desc = get_rel_desc(instance, key, Actions.GET)
except PermissionDeniedError:
continue

Expand Down Expand Up @@ -446,7 +406,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, AttributeActions.GET)
desc = get_attr_desc(instance, key, Actions.GET)
if key in local_fields:
to_ret['attributes'][instance.__jsonapi_map_to_api__[
key]] = desc(instance)
Expand Down
Binary file added test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions sqlalchemy_jsonapi/tests/app.py → 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

@permission_test(Permissions.VIEW, 'password')
@jsonapi_access(Permissions.VIEW, 'password')
def view_password(self):
""" Never let the password be seen. """
return False

@permission_test(Permissions.EDIT)
@jsonapi_access(Permissions.EDIT)
def prevent_edit(self):
""" Prevent editing for no reason. """
if request.view_args['api_type'] == 'blog-posts':
return True
return False

@permission_test(Permissions.DELETE)
@jsonapi_access(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

@permission_test(Permissions.VIEW)
@jsonapi_access(Permissions.VIEW)
def allow_view(self):
""" Hide unpublished. """
return self.is_published

@permission_test(INTERACTIVE_PERMISSIONS, 'logs')
@jsonapi_access(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'))

@permission_test(INTERACTIVE_PERMISSIONS)
@jsonapi_access(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,18 +1,20 @@
from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError


# TODO: Ember-style filtering
# TODO: Simple filtering
# TODO: Complex filtering
# TODO: Bad query param

# TODO: Vanilla

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 @@ -27,7 +29,6 @@ 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 @@ -36,6 +37,9 @@ 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 @@ -47,7 +51,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(item['attributes'].keys())
assert set() == set(item['attributes'].keys())
assert len(item['relationships']) == 0


Expand All @@ -74,6 +78,9 @@ 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 @@ -101,6 +108,9 @@ 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 @@ -121,3 +131,6 @@ 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,10 +1,8 @@
import json
from uuid import uuid4

from sqlalchemy_jsonapi.errors import (
BadRequestError, PermissionDeniedError, ResourceNotFoundError,
RelatedResourceNotFoundError, RelationshipNotFoundError, ValidationError,
MissingTypeError)
from sqlalchemy_jsonapi.errors import (BadRequestError, PermissionDeniedError,
ResourceNotFoundError, ValidationError)


# TODO: Sparse Fieldsets
Expand Down
File renamed without changes.

0 comments on commit 62c7706

Please sign in to comment.