Skip to content

Commit

Permalink
Closer to spec with dasherized names, links. Also lazy loaded relatio…
Browse files Browse the repository at this point in the history
…nships.
  • Loading branch information
ColtonProvias committed Jan 21, 2016
1 parent afd358d commit 6ba44ed
Show file tree
Hide file tree
Showing 17 changed files with 223 additions and 181 deletions.
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# SQLAlchemy-JSONAPI Changelog

## 4.0.0

*2016-01-20*

* BREAKING: Keys and types are now dasherized instead of underscored to fit assumptions of spec implementation
* BREAKING: Relationships are now lazy by default. Using the include query parameter will trigger an eager load.
* Added links to relationships

## 3.0.2

*2015-10-05*
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
requirements.append('enum34')

setup(name='SQLAlchemy-JSONAPI',
version='3.0.1',
version='4.0.0',
url='http://github.com/coltonprovias/sqlalchemy-jsonapi',
license='MIT',
author='Colton J. Provias',
Expand Down
2 changes: 1 addition & 1 deletion sqlalchemy_jsonapi/flaskext.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def _setup_adapter(self, namespace, route_prefix):
:param namespace: Prefix for generated endpoints
:param route_prefix: Prefix for route patterns
"""
self.serializer = JSONAPI(self.sqla.Model)
self.serializer = JSONAPI(self.sqla.Model, prefix='{}://{}{}'.format(self.app.config['PREFERRED_URL_SCHEME'], self.app.config['SERVER_NAME'], route_prefix))
for view in views:
method, endpoint = view
pattern = route_prefix + endpoint.value
Expand Down
159 changes: 98 additions & 61 deletions sqlalchemy_jsonapi/serializer.py

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions sqlalchemy_jsonapi/tests/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def view_password(self):
@permission_test(Permissions.EDIT)
def prevent_edit(self):
""" Prevent editing for no reason. """
if request.view_args['api_type'] == 'posts':
if request.view_args['api_type'] == 'blog-posts':
return True
return False

Expand All @@ -90,7 +90,7 @@ def allow_delete(self):
return False


class Post(Timestamp, db.Model):
class BlogPost(Timestamp, db.Model):
"""Post model, as if this is a blog."""

__tablename__ = 'posts'
Expand Down Expand Up @@ -124,7 +124,7 @@ def prevent_altering_of_logs(self):
return False


class Comment(Timestamp, db.Model):
class BlogComment(Timestamp, db.Model):
"""Comment for each Post."""

__tablename__ = 'comments'
Expand All @@ -134,7 +134,7 @@ class Comment(Timestamp, db.Model):
author_id = Column(UUIDType, ForeignKey('users.id'), nullable=False)
content = Column(UnicodeText, nullable=False)

post = relationship('Post',
post = relationship('BlogPost',
lazy='joined',
backref=backref('comments',
lazy='dynamic'))
Expand All @@ -150,7 +150,7 @@ class Log(Timestamp, db.Model):
post_id = Column(UUIDType, ForeignKey('posts.id'))
user_id = Column(UUIDType, ForeignKey('users.id'))

post = relationship('Post',
post = relationship('BlogPost',
lazy='joined',
backref=backref('logs',
lazy='dynamic'))
Expand All @@ -167,7 +167,7 @@ def block_interactive(cls):
api = FlaskJSONAPI(app, db)


@api.wrap_handler(['posts'], [Method.GET], [Endpoint.COLLECTION])
@api.wrap_handler(['blog-posts'], [Method.GET], [Endpoint.COLLECTION])
def sample_override(next, *args, **kwargs):
return next(*args, **kwargs)

Expand Down
12 changes: 6 additions & 6 deletions sqlalchemy_jsonapi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from flask.testing import FlaskClient
from sqlalchemy.orm import sessionmaker
from app import db as db_
from app import app, User, Post, Comment, Log
from app import app, User, BlogPost, BlogComment, Log
from faker import Faker

Session = sessionmaker()
Expand Down Expand Up @@ -87,7 +87,7 @@ def user(session):

@pytest.fixture
def post(user, session):
new_post = Post(author=user,
new_post = BlogPost(author=user,
title=fake.sentence(),
content=fake.paragraph(),
is_published=True)
Expand All @@ -98,7 +98,7 @@ def post(user, session):

@pytest.fixture
def unpublished_post(user, session):
new_post = Post(author=user,
new_post = BlogPost(author=user,
title=fake.sentence(),
content=fake.paragraph(),
is_published=False)
Expand All @@ -110,19 +110,19 @@ def unpublished_post(user, session):
@pytest.fixture
def bunch_of_posts(user, session):
for x in range(30):
new_post = Post(author=user,
new_post = BlogPost(author=user,
title=fake.sentence(),
content=fake.paragraph(),
is_published=fake.boolean())
session.add(new_post)
new_post.comments.append(Comment(author=user,
new_post.comments.append(BlogComment(author=user,
content=fake.paragraph()))
session.commit()


@pytest.fixture
def comment(user, post, session):
new_comment = Comment(author=user, post=post, content=fake.paragraph())
new_comment = BlogComment(author=user, post=post, content=fake.paragraph())
session.add(new_comment)
session.commit()
return new_comment
Expand Down
46 changes: 23 additions & 23 deletions sqlalchemy_jsonapi/tests/test_collection_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,50 @@


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


def test_200_with_single_included_model(bunch_of_posts, client):
response = client.get('/api/posts/?include=author').validate(200)
assert response.json_data['data'][0]['type'] == 'posts'
response = client.get('/api/blog-posts/?include=author').validate(200)
assert response.json_data['data'][0]['type'] == 'blog-posts'
assert response.json_data['included'][0]['type'] == 'users'


def test_200_with_including_model_and_including_inbetween(bunch_of_posts,
client):
response = client.get('/api/comments/?include=post.author').validate(200)
assert response.json_data['data'][0]['type'] == 'comments'
response = client.get('/api/blog-comments/?include=post.author').validate(200)
assert response.json_data['data'][0]['type'] == 'blog-comments'
for data in response.json_data['included']:
assert data['type'] in ['posts', 'users']
assert data['type'] in ['blog-posts', 'users']


def test_200_with_multiple_includes(bunch_of_posts, client):
response = client.get('/api/posts/?include=comments,author').validate(200)
assert response.json_data['data'][0]['type'] == 'posts'
response = client.get('/api/blog-posts/?include=comments,author').validate(200)
assert response.json_data['data'][0]['type'] == 'blog-posts'
for data in response.json_data['included']:
assert data['type'] in ['comments', 'users']
assert data['type'] in ['blog-comments', 'users']


def test_200_with_single_field(bunch_of_posts, client):
response = client.get('/api/posts/?fields[posts]=title').validate(200)
response = client.get('/api/blog-posts/?fields[blog-posts]=title').validate(200)
for item in response.json_data['data']:
assert {'title'} == set(item['attributes'].keys())
assert len(item['relationships']) == 0


def test_200_with_multiple_fields(bunch_of_posts, client):
response = client.get('/api/posts/?fields[posts]=title,content').validate(
response = client.get('/api/blog-posts/?fields[blog-posts]=title,content,is-published').validate(
200)
for item in response.json_data['data']:
assert {'title', 'content'} == set(item['attributes'].keys())
assert {'title', 'content', 'is-published'} == set(item['attributes'].keys())
assert len(item['relationships']) == 0


def test_200_with_single_field_across_a_relationship(bunch_of_posts, client):
response = client.get(
'/api/posts/?fields[posts]=title,content&fields[comments]=author&include=comments').validate(
'/api/blog-posts/?fields[blog-posts]=title,content&fields[blog-comments]=author&include=comments').validate(
200)
for item in response.json_data['data']:
assert {'title', 'content'} == set(item['attributes'].keys())
Expand All @@ -58,48 +58,48 @@ def test_200_with_single_field_across_a_relationship(bunch_of_posts, client):


def test_200_sorted_response(bunch_of_posts, client):
response = client.get('/api/posts/?sort=title').validate(200)
response = client.get('/api/blog-posts/?sort=title').validate(200)
title_list = [x['attributes']['title'] for x in response.json_data['data']]
assert sorted(title_list) == title_list


def test_200_descending_sorted_response(bunch_of_posts, client):
response = client.get('/api/posts/?sort=-title').validate(200)
response = client.get('/api/blog-posts/?sort=-title').validate(200)
title_list = [x['attributes']['title'] for x in response.json_data['data']]
assert sorted(title_list, key=None, reverse=True) == title_list


def test_200_sorted_response_with_multiple_criteria(bunch_of_posts, client):
response = client.get('/api/posts/?sort=title,-created').validate(200)
response = client.get('/api/blog-posts/?sort=title,-created').validate(200)
title_list = [x['attributes']['title'] for x in response.json_data['data']]
assert sorted(title_list, key=None, reverse=False) == title_list


def test_409_when_given_relationship_for_sorting(bunch_of_posts, client):
client.get('/api/posts/?sort=author').validate(409, NotSortableError)
client.get('/api/blog-posts/?sort=author').validate(409, NotSortableError)


def test_409_when_given_a_missing_field_for_sorting(bunch_of_posts, client):
client.get('/api/posts/?sort=never_gonna_give_you_up').validate(
client.get('/api/blog-posts/?sort=never_gonna_give_you_up').validate(
409, NotSortableError)


def test_200_paginated_response_by_page(bunch_of_posts, client):
response = client.get('/api/posts/?page[number]=2&page[size]=5').validate(
response = client.get('/api/blog-posts/?page[number]=2&page[size]=5').validate(
200)
assert len(response.json_data['data']) == 5


def test_200_paginated_response_by_offset(bunch_of_posts, client):
response = client.get('/api/posts/?page[offset]=5&page[limit]=5').validate(
response = client.get('/api/blog-posts/?page[offset]=5&page[limit]=5').validate(
200)
assert len(response.json_data['data']) == 5


def test_200_when_pagination_is_out_of_range(bunch_of_posts, client):
client.get('/api/posts/?page[offset]=999999&page[limit]=5').validate(200)
client.get('/api/blog-posts/?page[offset]=999999&page[limit]=5').validate(200)


def test_400_when_provided_crap_data_for_pagination(bunch_of_posts, client):
client.get('/api/posts/?page[offset]=5&page[limit]=crap').validate(
client.get('/api/blog-posts/?page[offset]=5&page[limit]=crap').validate(
400, BadRequestError)
14 changes: 7 additions & 7 deletions sqlalchemy_jsonapi/tests/test_collection_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ def test_200_resource_creation(client):
def test_200_resource_creation_with_relationships(user, client):
payload = {
'data': {
'type': 'posts',
'type': 'blog-posts',
'attributes': {
'title': 'Some title',
'content': 'Hello, World!',
'is_published': True
'is-published': True
},
'relationships': {
'author': {
Expand All @@ -48,13 +48,13 @@ def test_200_resource_creation_with_relationships(user, client):
}
}
}
response = client.post('/api/posts/',
response = client.post('/api/blog-posts/',
data=json.dumps(payload),
content_type='application/vnd.api+json').validate(
201)
assert response.json_data['data']['type'] == 'posts'
assert response.json_data['data']['type'] == 'blog-posts'
post_id = response.json_data['data']['id']
response = client.get('/api/posts/{}/'.format(post_id)).validate(200)
response = client.get('/api/blog-posts/{}/?include=author'.format(post_id)).validate(200)
assert response.json_data['data']['relationships']['author']['data'][
'id'
] == str(user.id)
Expand Down Expand Up @@ -87,7 +87,7 @@ def test_409_when_id_already_exists(user, client):


def test_409_when_type_doesnt_match_endpoint(client):
payload = {'data': {'type': 'posts'}}
payload = {'data': {'type': 'blog-posts'}}
client.post('/api/users/',
data=json.dumps(payload),
content_type='application/vnd.api+json').validate(
Expand Down Expand Up @@ -147,4 +147,4 @@ def test_409_for_wrong_field_name(client):
client.post('/api/users/',
data=json.dumps(payload),
content_type='application/vnd.api+json').validate(
400, BadRequestError)
409, ValidationError)
8 changes: 4 additions & 4 deletions sqlalchemy_jsonapi/tests/test_related_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@


def test_200_result_of_to_one(post, client):
response = client.get('/api/posts/{}/author/'.format(post.id)).validate(
response = client.get('/api/blog-posts/{}/author/'.format(post.id)).validate(
200)
assert response.json_data['data']['type'] == 'users'


def test_200_collection_of_to_many(comment, client):
response = client.get('/api/posts/{}/comments/'.format(
response = client.get('/api/blog-posts/{}/comments/'.format(
comment.post.id)).validate(200)
assert len(response.json_data['data']) > 0


def test_404_when_relationship_not_found(post, client):
client.get('/api/posts/{}/last_comment/'.format(
client.get('/api/blog-posts/{}/last_comment/'.format(
post.id)).validate(404, RelationshipNotFoundError)


def test_404_when_resource_not_found(client):
client.get('/api/posts/{}/comments/'.format(uuid4())).validate(
client.get('/api/blog-posts/{}/comments/'.format(uuid4())).validate(
404, ResourceNotFoundError)
12 changes: 6 additions & 6 deletions sqlalchemy_jsonapi/tests/test_relationship_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@


def test_200_on_deletion_from_to_many(comment, client):
payload = {'data': [{'type': 'comments', 'id': str(comment.id)}]}
payload = {'data': [{'type': 'blog-comments', 'id': str(comment.id)}]}
response = client.delete(
'/api/posts/{}/relationships/comments/'.format(
'/api/blog-posts/{}/relationships/comments/'.format(
comment.post.id),
data=json.dumps(payload),
content_type='application/vnd.api+json').validate(200)
Expand All @@ -22,14 +22,14 @@ def test_200_on_deletion_from_to_many(comment, client):


def test_404_on_resource_not_found(client):
client.delete('/api/posts/{}/relationships/comments/'.format(uuid4()),
client.delete('/api/blog-posts/{}/relationships/comments/'.format(uuid4()),
data='{}',
content_type='application/vnd.api+json').validate(
404, ResourceNotFoundError)


def test_404_on_relationship_not_found(post, client):
client.delete('/api/posts/{}/relationships/comment/'.format(
client.delete('/api/blog-posts/{}/relationships/comment/'.format(
post.id),
data='{}',
content_type='application/vnd.api+json').validate(
Expand All @@ -45,14 +45,14 @@ def test_403_on_permission_denied(user, client):


def test_409_on_to_one_provided(post, client):
client.delete('/api/posts/{}/relationships/author/'.format(
client.delete('/api/blog-posts/{}/relationships/author/'.format(
post.id),
data='{"data": {}}',
content_type='application/vnd.api+json').validate(
409, ValidationError)


def test_409_missing_content_type_header(post, client):
client.delete('/api/posts/{}/relationships/comment/'.format(
client.delete('/api/blog-posts/{}/relationships/comment/'.format(
post.id),
data='{}').validate(409, MissingContentTypeError)
Loading

0 comments on commit 6ba44ed

Please sign in to comment.