Skip to content

Commit 62c7706

Browse files
committed
BROKEN: Starting work on 5.0.0.
1 parent 7195452 commit 62c7706

17 files changed

+63
-91
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
[![Build Status](https://travis-ci.org/ColtonProvias/sqlalchemy-jsonapi.svg?branch=master)](https://travis-ci.org/ColtonProvias/sqlalchemy-jsonapi)
44

5+
**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.**
6+
57
[JSON API](http://jsonapi.org/) implementation for use with
68
[SQLAlchemy](http://www.sqlalchemy.org/).
79

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#
55
# pip-compile --output-file requirements.txt requirements.in
66
#
7-
87
bcrypt==2.0.0
98
blinker==1.4
109
cffi==1.7.0 # via bcrypt

sqlalchemy_jsonapi/serializer.py

Lines changed: 33 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
"""
2-
SQLAlchemy-JSONAPI
3-
Serializer
1+
"""SQLAlchemy-JSONAPI Serializer.
2+
43
Colton J. Provias
54
MIT License
65
"""
@@ -19,29 +18,22 @@
1918
from ._version import __version__
2019

2120

22-
class AttributeActions(Enum):
23-
""" The actions that can be done to an attribute. """
24-
25-
GET = 0
26-
SET = 1
21+
class Actions(Enum):
22+
""" The actions that can be performed on an attribute or relationship. """
2723

28-
29-
class RelationshipActions(Enum):
30-
""" The actions that can be performed on a relationship. """
31-
32-
GET = 10
33-
APPEND = 11
34-
SET = 12
35-
DELETE = 13
24+
GET = 1
25+
APPEND = 2
26+
SET = 3
27+
REMOVE = 4
3628

3729

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

41-
VIEW = 100
42-
CREATE = 101
43-
EDIT = 102
44-
DELETE = 103
33+
VIEW = 1
34+
CREATE = 2
35+
EDIT = 3
36+
DELETE = 4
4537

4638

4739
ALL_PERMISSIONS = {
@@ -52,58 +44,26 @@ class Permissions(Enum):
5244
}
5345

5446

55-
def attr_descriptor(action, *names):
56-
"""
57-
Wrap a function that allows for getting or setting of an attribute. This
58-
allows for specific handling of an attribute when it comes to serializing
59-
and deserializing.
60-
61-
:param action: The AttributeActions that this descriptor performs
62-
:param names: A list of names of the attributes this references
63-
"""
64-
if isinstance(action, AttributeActions):
47+
def jsonapi_action(action, *names):
48+
if isinstance(action, Actions):
6549
action = [action]
6650

6751
def wrapped(fn):
6852
if not hasattr(fn, '__jsonapi_action__'):
6953
fn.__jsonapi_action__ = set()
70-
fn.__jsonapi_desc_for_attrs__ = set()
71-
fn.__jsonapi_desc_for_attrs__ |= set(names)
72-
fn.__jsonapi_action__ |= set(action)
73-
return fn
74-
75-
return wrapped
76-
77-
78-
def relationship_descriptor(action, *names):
79-
"""
80-
Wrap a function for modification of a relationship. This allows for
81-
specific handling for serialization and deserialization.
82-
83-
:param action: The RelationshipActions that this descriptor performs
84-
:param names: A list of names of the relationships this references
85-
"""
86-
if isinstance(action, RelationshipActions):
87-
action = [action]
88-
89-
def wrapped(fn):
90-
if not hasattr(fn, '__jsonapi_action__'):
91-
fn.__jsonapi_action__ = set()
92-
fn.__jsonapi_desc_for_rels__ = set()
93-
fn.__jsonapi_desc_for_rels__ |= set(names)
54+
fn.__jsonapi_desc__ = set()
55+
fn.__jsonapi_desc__ |= set(names)
9456
fn.__jsonapi_action__ |= set(action)
9557
return fn
9658

9759
return wrapped
9860

9961

10062
class PermissionTest(object):
101-
""" Authorize access to a model, resource, or specific field. """
63+
"""Authorize access to a model, resource, or specific field."""
10264

10365
def __init__(self, permission, *names):
104-
"""
105-
Decorates a function that returns a boolean representing if access is
106-
allowed.
66+
"""Decorate a function that returns a boolean representing access.
10767
10868
:param permission: The permission to check for
10969
:param names: The names to test for. None represents the model.
@@ -128,14 +88,14 @@ def __call__(self, fn):
12888
return fn
12989

13090
#: More consistent name for the decorators
131-
permission_test = PermissionTest
91+
jsonapi_access = PermissionTest
13292

13393

13494
class JSONAPIResponse(object):
135-
""" Wrapper for JSON API Responses. """
95+
"""Wrapper for JSON API Responses."""
13696

13797
def __init__(self):
138-
""" Default the status code and data. """
98+
"""Default the status code and data."""
13999
self.status_code = 200
140100
self.data = {
141101
'jsonapi': {'version': '1.0'},
@@ -158,8 +118,7 @@ def get_permission_test(model, field, permission, instance=None):
158118

159119
def check_permission(instance, field, permission):
160120
"""
161-
Check a permission for a given instance or field. Raises an error if
162-
denied.
121+
Check a permission for a given instance or field. Raises error if denied.
163122
164123
:param instance: The instance to check
165124
:param field: The field name to check or None for instance
@@ -175,10 +134,10 @@ def get_attr_desc(instance, attribute, action):
175134
176135
:param instance: Model instance
177136
:param attribute: Name of the attribute
178-
:param action: AttributeAction
137+
:param action: Action
179138
"""
180139
descs = instance.__jsonapi_attribute_descriptors__.get(attribute, {})
181-
if action == AttributeActions.GET:
140+
if action == Actions.GET:
182141
check_permission(instance, attribute, Permissions.VIEW)
183142
return descs.get(action, lambda x: getattr(x, attribute))
184143
check_permission(instance, attribute, Permissions.EDIT)
@@ -194,13 +153,13 @@ def get_rel_desc(instance, key, action):
194153
:param action: RelationshipAction
195154
"""
196155
descs = instance.__jsonapi_rel_desc__.get(key, {})
197-
if action == RelationshipActions.GET:
156+
if action == Actions.GET:
198157
check_permission(instance, key, Permissions.VIEW)
199158
return descs.get(action, lambda x: getattr(x, key))
200-
elif action == RelationshipActions.APPEND:
159+
elif action == Actions.APPEND:
201160
check_permission(instance, key, Permissions.CREATE)
202161
return descs.get(action, lambda x, v: getattr(x, key).append(v))
203-
elif action == RelationshipActions.SET:
162+
elif action == Actions.SET:
204163
check_permission(instance, key, Permissions.EDIT)
205164
return descs.get(action, lambda x, v: setattr(x, key, v))
206165
else:
@@ -300,7 +259,8 @@ def _lazy_relationship(self, api_type, obj_id, rel_key):
300259
return {
301260
'self': '{}/{}/{}/relationships/{}'.format(self.prefix, api_type,
302261
obj_id, rel_key),
303-
'related': '{}/{}/{}/{}'.format(self.prefix, api_type, obj_id, rel_key)
262+
'related': '{}/{}/{}/{}'.format(self.prefix, api_type, obj_id,
263+
rel_key)
304264
}
305265

306266
def _get_relationship(self, resource, rel_key, permission):
@@ -367,8 +327,8 @@ def _render_full_resource(self, instance, include, fields):
367327
attrs_to_ignore = {'__mapper__', 'id'}
368328
if api_type in fields.keys():
369329
local_fields = list(map((
370-
lambda x: instance.__jsonapi_map_to_py__[x]), fields[
371-
api_type]))
330+
lambda x: instance.__jsonapi_map_to_py__.get(x)), fields.get(
331+
api_type)))
372332
else:
373333
local_fields = orm_desc_keys
374334

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

381341
try:
382-
desc = get_rel_desc(instance, key, RelationshipActions.GET)
342+
desc = get_rel_desc(instance, key, Actions.GET)
383343
except PermissionDeniedError:
384344
continue
385345

@@ -446,7 +406,7 @@ def _render_full_resource(self, instance, include, fields):
446406

447407
for key in set(orm_desc_keys) - attrs_to_ignore:
448408
try:
449-
desc = get_attr_desc(instance, key, AttributeActions.GET)
409+
desc = get_attr_desc(instance, key, Actions.GET)
450410
if key in local_fields:
451411
to_ret['attributes'][instance.__jsonapi_map_to_api__[
452412
key]] = desc(instance)

test.png

550 Bytes
Loading

sqlalchemy_jsonapi/tests/app.py renamed to tests/app.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,19 @@ def validate_password(self, key, password):
7474
assert len(password) >= 5, 'Password must be 5 characters or longer.'
7575
return password
7676

77-
@permission_test(Permissions.VIEW, 'password')
77+
@jsonapi_access(Permissions.VIEW, 'password')
7878
def view_password(self):
7979
""" Never let the password be seen. """
8080
return False
8181

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

89-
@permission_test(Permissions.DELETE)
89+
@jsonapi_access(Permissions.DELETE)
9090
def allow_delete(self):
9191
""" Just like a popular social media site, we won't delete users. """
9292
return False
@@ -115,12 +115,12 @@ def validate_title(self, key, title):
115115
title) <= 100, 'Must be 5 to 100 characters long.'
116116
return title
117117

118-
@permission_test(Permissions.VIEW)
118+
@jsonapi_access(Permissions.VIEW)
119119
def allow_view(self):
120120
""" Hide unpublished. """
121121
return self.is_published
122122

123-
@permission_test(INTERACTIVE_PERMISSIONS, 'logs')
123+
@jsonapi_access(INTERACTIVE_PERMISSIONS, 'logs')
124124
def prevent_altering_of_logs(self):
125125
return False
126126

@@ -157,7 +157,7 @@ class Log(Timestamp, db.Model):
157157
lazy='joined',
158158
backref=backref('logs', lazy='dynamic'))
159159

160-
@permission_test(INTERACTIVE_PERMISSIONS)
160+
@jsonapi_access(INTERACTIVE_PERMISSIONS)
161161
def block_interactive(cls):
162162
return False
163163

File renamed without changes.

sqlalchemy_jsonapi/tests/test_collection_get.py renamed to tests/test_collection_get.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError
22

33

4-
# TODO: Ember-style filtering
5-
# TODO: Simple filtering
6-
# TODO: Complex filtering
7-
# TODO: Bad query param
8-
4+
# TODO: Vanilla
95

106
def test_200_with_no_querystring(bunch_of_posts, client):
117
response = client.get('/api/blog-posts').validate(200)
128
assert response.json_data['data'][0]['type'] == 'blog-posts'
139
assert response.json_data['data'][0]['id']
1410

1511

12+
# TODO: Bad Query Param
13+
14+
15+
# TODO: Resource Inclusions
16+
17+
1618
def test_200_with_single_included_model(bunch_of_posts, client):
1719
response = client.get('/api/blog-posts/?include=author').validate(200)
1820
assert response.json_data['data'][0]['type'] == 'blog-posts'
@@ -27,7 +29,6 @@ def test_200_with_including_model_and_including_inbetween(bunch_of_posts,
2729
for data in response.json_data['included']:
2830
assert data['type'] in ['blog-posts', 'users']
2931

30-
3132
def test_200_with_multiple_includes(bunch_of_posts, client):
3233
response = client.get('/api/blog-posts/?include=comments,author').validate(
3334
200)
@@ -36,6 +37,9 @@ def test_200_with_multiple_includes(bunch_of_posts, client):
3637
assert data['type'] in ['blog-comments', 'users']
3738

3839

40+
# TODO: Sparse Fieldsets
41+
42+
3943
def test_200_with_single_field(bunch_of_posts, client):
4044
response = client.get(
4145
'/api/blog-posts/?fields[blog-posts]=title').validate(200)
@@ -47,7 +51,7 @@ def test_200_with_bad_field(bunch_of_posts, client):
4751
response = client.get(
4852
'/api/blog-posts/?fields[blog-posts]=titles').validate(200)
4953
for item in response.json_data['data']:
50-
assert {} == set(item['attributes'].keys())
54+
assert set() == set(item['attributes'].keys())
5155
assert len(item['relationships']) == 0
5256

5357

@@ -74,6 +78,9 @@ def test_200_with_single_field_across_a_relationship(bunch_of_posts, client):
7478
assert {'author'} == set(item['relationships'].keys())
7579

7680

81+
# TODO: Sorting
82+
83+
7784
def test_200_sorted_response(bunch_of_posts, client):
7885
response = client.get('/api/blog-posts/?sort=title').validate(200)
7986
title_list = [x['attributes']['title'] for x in response.json_data['data']]
@@ -101,6 +108,9 @@ def test_409_when_given_a_missing_field_for_sorting(bunch_of_posts, client):
101108
409, NotSortableError)
102109

103110

111+
# TODO: Pagination
112+
113+
104114
def test_200_paginated_response_by_page(bunch_of_posts, client):
105115
response = client.get(
106116
'/api/blog-posts/?page[number]=2&page[size]=5').validate(200)
@@ -121,3 +131,6 @@ def test_200_when_pagination_is_out_of_range(bunch_of_posts, client):
121131
def test_400_when_provided_crap_data_for_pagination(bunch_of_posts, client):
122132
client.get('/api/blog-posts/?page[offset]=5&page[limit]=crap').validate(
123133
400, BadRequestError)
134+
135+
136+
# TODO: Filtering

0 commit comments

Comments
 (0)