From 5461eed413bff9fa79d92bb8dbb41762fba081c0 Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 1/9] Revert "Starting tests rewrite." This reverts commit 223d144d69d58d577629457a0944d6feb078c23a. --- tests/app.py | 199 +++++++++------- tests/conftest.py | 81 +++++-- tests/jsonapi_schema.json | 383 ------------------------------ tests/test_collection_get.py | 39 +-- tests/test_collection_post.py | 67 +++--- tests/test_related_get.py | 3 +- tests/test_relationship_delete.py | 43 ++-- tests/test_relationship_get.py | 3 +- tests/test_relationship_patch.py | 80 +++---- tests/test_relationship_post.py | 37 ++- tests/test_resource_delete.py | 4 +- tests/test_resource_get.py | 4 +- tests/test_resource_patch.py | 77 +++--- tests/test_serializer.py | 3 +- 14 files changed, 334 insertions(+), 689 deletions(-) delete mode 100644 tests/jsonapi_schema.json diff --git a/tests/app.py b/tests/app.py index b45507c..74e0f1a 100644 --- a/tests/app.py +++ b/tests/app.py @@ -1,9 +1,6 @@ """ SQLAlchemy JSONAPI Test App. -This app implements the backend of a web forum. It's rather simple but -provides us with a more comprehensive example for testing. - Colton Provias MIT License """ @@ -12,15 +9,13 @@ from flask import Flask, request from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import Boolean, Column, Enum, ForeignKey, Unicode, UnicodeText +from sqlalchemy import Boolean, Column, ForeignKey, Unicode, UnicodeText from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import backref, relationship, validates -from sqlalchemy_utils import (EmailType, IPAddressType, PasswordType, - Timestamp, URLType, UUIDType) - -import enum - -# ================================ APP CONFIG ================================ +from sqlalchemy_jsonapi import (INTERACTIVE_PERMISSIONS, Endpoint, + FlaskJSONAPI, Method, Permissions, + permission_test) +from sqlalchemy_utils import EmailType, PasswordType, Timestamp, UUIDType app = Flask(__name__) @@ -31,21 +26,29 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' app.config['SQLALCHEMY_ECHO'] = False -#api = FlaskJSONAPI(app, db) -# ================================== MODELS ================================== +class User(Timestamp, db.Model): + """Quick and dirty user model.""" - -class User(db.Model, Timestamp): + #: If __jsonapi_type__ is not provided, it will use the class name instead. __tablename__ = 'users' - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) + id = Column(UUIDType, default=uuid4, primary_key=True) + username = Column(Unicode(30), unique=True, nullable=False) email = Column(EmailType, nullable=False) - display_name = Column(Unicode(100), nullable=False) - password = Column(PasswordType(schemes=['bcrypt']), nullable=False) + password = Column( + PasswordType(schemes=['bcrypt']), + nullable=False) is_admin = Column(Boolean, default=False) - last_ip_address = Column(IPAddressType) - website = Column(URLType) + + @hybrid_property + def total_comments(self): + """ + Total number of comments. + + Provides an example of a computed property. + """ + return self.comments.count() @validates('email') def validate_email(self, key, email): @@ -53,95 +56,119 @@ def validate_email(self, key, email): assert '@' in email, 'Not an email' return email + @validates('username') + def validate_username(self, key, username): + """ + Check the length of the username. + + Here's hoping nobody submits something in unicode that is 31 characters + long!! + """ + assert len(username) >= 4 and len( + username) <= 30, 'Must be 4 to 30 characters long.' + return username + + @validates('password') + def validate_password(self, key, password): + """Validate a password's length.""" + assert len(password) >= 5, 'Password must be 5 characters or longer.' + return password + + @jsonapi_access(Permissions.VIEW, 'password') + def view_password(self): + """ Never let the password be seen. """ + return False + + @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 + + @jsonapi_access(Permissions.DELETE) + def allow_delete(self): + """ Just like a popular social media site, we won't delete users. """ + return False + + +class BlogPost(Timestamp, db.Model): + """Post model, as if this is a blog.""" -class Forum(db.Model, Timestamp): - __tablename__ = 'forums' - - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - name = Column(Unicode(255), nullable=False) - can_public_read = Column(Boolean, default=True) - can_public_write = Column(Boolean, default=True) - - -class Thread(db.Model, Timestamp): - __tablename__ = 'threads' - - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - forum_id = Column(UUIDType, ForeignKey('forums.id'), nullable=False) - started_by_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) - title = Column(Unicode(255), nullable=False) - - -class Post(db.Model, Timestamp): __tablename__ = 'posts' - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - user_id = Column(UUIDType, ForeignKey('posts.id'), nullable=False) + id = Column(UUIDType, default=uuid4, primary_key=True) + title = Column(Unicode(100), nullable=False) + slug = Column(Unicode(100)) content = Column(UnicodeText, nullable=False) - is_removed = Column(Boolean, default=False) - + is_published = Column(Boolean, default=False) + author_id = Column(UUIDType, ForeignKey('users.id')) -class ReportTypes(enum.Enum): - USER = 0 - POST = 1 + author = relationship('User', + lazy='joined', + backref=backref('posts', lazy='dynamic')) + @validates('title') + def validate_title(self, key, title): + """Keep titles from getting too long.""" + assert len(title) >= 5 or len( + title) <= 100, 'Must be 5 to 100 characters long.' + return title -class Report(db.Model, Timestamp): - __tablename__ = 'reports' + @jsonapi_access(Permissions.VIEW) + def allow_view(self): + """ Hide unpublished. """ + return self.is_published - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - report_type = Column(Enum(ReportTypes), nullable=False) - reporter_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) - complaint = Column(UnicodeText, nullable=False) + @jsonapi_access(INTERACTIVE_PERMISSIONS, 'logs') + def prevent_altering_of_logs(self): + return False - __mapper_args__ = { - 'polymorphic_identity': 'employee', - 'polymorphic_on': report_type, - 'with_polymorphic': '*' - } +class BlogComment(Timestamp, db.Model): + """Comment for each Post.""" -class UserReport(db.Model): - __tablename__ = 'user_reports' + __tablename__ = 'comments' - id = Column( - UUIDType, - ForeignKey('reports.id'), - default=uuid4, - primary_key=True, - nullable=False) - user_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) - - __mapper_args__ = {'polymorphic_identity': ReportTypes.USER} + id = Column(UUIDType, default=uuid4, primary_key=True) + post_id = Column(UUIDType, ForeignKey('posts.id')) + author_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) + content = Column(UnicodeText, nullable=False) + post = relationship('BlogPost', + lazy='joined', + backref=backref('comments', lazy='dynamic')) + author = relationship('User', + lazy='joined', + backref=backref('comments', + lazy='dynamic')) -class PostReport(db.Model): - __tablename__ = 'post_reports' - id = Column( - UUIDType, - ForeignKey('reports.id'), - default=uuid4, - primary_key=True, - nullable=False) - post_id = Column(UUIDType, ForeignKey('posts.id'), nullable=False) +class Log(Timestamp, db.Model): + __tablename__ = 'logs' + id = Column(UUIDType, default=uuid4, primary_key=True) + post_id = Column(UUIDType, ForeignKey('posts.id')) + user_id = Column(UUIDType, ForeignKey('users.id')) - __mapper_args__ = {'polymorphic_identity': ReportTypes.POST} + post = relationship('BlogPost', + lazy='joined', + backref=backref('logs', lazy='dynamic')) + user = relationship('User', + lazy='joined', + backref=backref('logs', lazy='dynamic')) -# ============================== EVENT HANDLERS ============================== + @jsonapi_access(INTERACTIVE_PERMISSIONS) + def block_interactive(cls): + return False -@app.before_request -def handle_auth(): - pass +api = FlaskJSONAPI(app, db) -# ============================== API OVERRIDES ============================== -#@api.wrap_handler(['blog-posts'], [Method.GET], [Endpoint.COLLECTION]) -#def sample_override(next, *args, **kwargs): -# return next(*args, **kwargs) +@api.wrap_handler(['blog-posts'], [Method.GET], [Endpoint.COLLECTION]) +def sample_override(next, *args, **kwargs): + return next(*args, **kwargs) -# ================================ APP RUNNER ================================ if __name__ == '__main__': app.run() diff --git a/tests/conftest.py b/tests/conftest.py index 67c026c..9e93e06 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,24 +7,18 @@ import json -import jsonschema import pytest -from addict import Dict -from faker import Faker from flask import Response from flask.testing import FlaskClient from sqlalchemy.orm import sessionmaker - from app import db as db_ -from app import app +from app import app, User, BlogPost, BlogComment, Log +from faker import Faker Session = sessionmaker() fake = Faker() -with open('tests/jsonapi_schema.json', 'r') as f: - api_schema = json.load(f) - @pytest.yield_fixture(scope='session') def flask_app(): @@ -63,9 +57,7 @@ def validate(self, status_code, error=None): assert self.status_code == status_code assert self.headers['Content-Type'] == 'application/vnd.api+json' if status_code != 204: - json_data = json.loads(self.data.decode()) - jsonschema.validate(json_data, api_schema) - self.json_data = Dict(json_data) + self.json_data = json.loads(self.data.decode()) if error: assert self.status_code == error.status_code assert self.json_data['errors'][0]['code'] == error.code @@ -77,7 +69,68 @@ def validate(self, status_code, error=None): @pytest.fixture def client(flask_app): """Set up the testing client.""" - with FlaskClient( - flask_app, use_cookies=True, - response_wrapper=TestingResponse) as c: + with FlaskClient(flask_app, + use_cookies=True, + response_wrapper=TestingResponse) as c: return c + + +@pytest.fixture +def user(session): + new_user = User(email=fake.email(), + password=fake.sentence(), + username=fake.user_name()) + session.add(new_user) + session.commit() + return new_user + + +@pytest.fixture +def post(user, session): + new_post = BlogPost(author=user, + title=fake.sentence(), + content=fake.paragraph(), + is_published=True) + session.add(new_post) + session.commit() + return new_post + + +@pytest.fixture +def unpublished_post(user, session): + new_post = BlogPost(author=user, + title=fake.sentence(), + content=fake.paragraph(), + is_published=False) + session.add(new_post) + session.commit() + return new_post + + +@pytest.fixture +def bunch_of_posts(user, session): + for x in range(30): + new_post = BlogPost(author=user, + title=fake.sentence(), + content=fake.paragraph(), + is_published=fake.boolean()) + session.add(new_post) + new_post.comments.append(BlogComment(author=user, + content=fake.paragraph())) + session.commit() + + +@pytest.fixture +def comment(user, post, session): + new_comment = BlogComment(author=user, post=post, content=fake.paragraph()) + session.add(new_comment) + session.commit() + return new_comment + + +@pytest.fixture +def log(user, post, session): + new_log = Log(user=user, post=post) + session.add(new_log) + session.commit() + return new_log diff --git a/tests/jsonapi_schema.json b/tests/jsonapi_schema.json deleted file mode 100644 index 32132bc..0000000 --- a/tests/jsonapi_schema.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "JSON API Schema", - "description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org", - "oneOf": [ - { - "$ref": "#/definitions/success" - }, - { - "$ref": "#/definitions/failure" - }, - { - "$ref": "#/definitions/info" - } - ], - "definitions": { - "success": { - "type": "object", - "required": [ - "data" - ], - "properties": { - "data": { - "$ref": "#/definitions/data" - }, - "included": { - "description": "To reduce the number of HTTP requests, servers **MAY** allow responses that include related resources along with the requested primary resources. Such responses are called \"compound documents\".", - "type": "array", - "items": { - "$ref": "#/definitions/resource" - }, - "uniqueItems": true - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "links": { - "description": "Link members related to the primary data.", - "allOf": [ - { - "$ref": "#/definitions/links" - }, - { - "$ref": "#/definitions/pagination" - } - ] - }, - "jsonapi": { - "$ref": "#/definitions/jsonapi" - } - }, - "additionalProperties": false - }, - "failure": { - "type": "object", - "required": [ - "errors" - ], - "properties": { - "errors": { - "type": "array", - "items": { - "$ref": "#/definitions/error" - }, - "uniqueItems": true - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "jsonapi": { - "$ref": "#/definitions/jsonapi" - } - }, - "additionalProperties": false - }, - "info": { - "type": "object", - "required": [ - "meta" - ], - "properties": { - "meta": { - "$ref": "#/definitions/meta" - }, - "links": { - "$ref": "#/definitions/links" - }, - "jsonapi": { - "$ref": "#/definitions/jsonapi" - } - }, - "additionalProperties": false - }, - "meta": { - "description": "Non-standard meta-information that can not be represented as an attribute or relationship.", - "type": "object", - "additionalProperties": true - }, - "data": { - "description": "The document's \"primary data\" is a representation of the resource or collection of resources targeted by a request.", - "oneOf": [ - { - "$ref": "#/definitions/resource" - }, - { - "description": "An array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections.", - "type": "array", - "items": { - "$ref": "#/definitions/resource" - }, - "uniqueItems": true - }, - { - "description": "null if the request is one that might correspond to a single resource, but doesn't currently.", - "type": "null" - } - ] - }, - "resource": { - "description": "\"Resource objects\" appear in a JSON API document to represent resources.", - "type": "object", - "required": [ - "type", - "id" - ], - "properties": { - "type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "attributes": { - "$ref": "#/definitions/attributes" - }, - "relationships": { - "$ref": "#/definitions/relationships" - }, - "links": { - "$ref": "#/definitions/links" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - }, - "links": { - "description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.", - "type": "object", - "properties": { - "self": { - "description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.", - "type": "string", - "format": "uri" - }, - "related": { - "$ref": "#/definitions/link" - } - }, - "additionalProperties": true - }, - "link": { - "description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.", - "oneOf": [ - { - "description": "A string containing the link's URL.", - "type": "string", - "format": "uri" - }, - { - "type": "object", - "required": [ - "href" - ], - "properties": { - "href": { - "description": "A string containing the link's URL.", - "type": "string", - "format": "uri" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - } - ] - }, - "attributes": { - "description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.", - "type": "object", - "patternProperties": { - "^(?!relationships$|links$)\\w[-\\w_]*$": { - "description": "Attributes may contain any valid JSON value." - } - }, - "additionalProperties": false - }, - "relationships": { - "description": "Members of the relationships object (\"relationships\") represent references from the resource object in which it's defined to other resource objects.", - "type": "object", - "patternProperties": { - "^\\w[-\\w_]*$": { - "properties": { - "links": { - "$ref": "#/definitions/links" - }, - "data": { - "description": "Member, whose value represents \"resource linkage\".", - "oneOf": [ - { - "$ref": "#/definitions/relationshipToOne" - }, - { - "$ref": "#/definitions/relationshipToMany" - } - ] - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "relationshipToOne": { - "description": "References to other resource objects in a to-one (\"relationship\"). Relationships can be specified by including a member in a resource's links object.", - "anyOf": [ - { - "$ref": "#/definitions/empty" - }, - { - "$ref": "#/definitions/linkage" - } - ] - }, - "relationshipToMany": { - "description": "An array of objects each containing \"type\" and \"id\" members for to-many relationships.", - "type": "array", - "items": { - "$ref": "#/definitions/linkage" - }, - "uniqueItems": true - }, - "empty": { - "description": "Describes an empty to-one relationship.", - "type": "null" - }, - "linkage": { - "description": "The \"type\" and \"id\" to non-empty members.", - "type": "object", - "required": [ - "type", - "id" - ], - "properties": { - "type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - }, - "pagination": { - "type": "object", - "properties": { - "first": { - "description": "The first page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - }, - "last": { - "description": "The last page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - }, - "prev": { - "description": "The previous page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - }, - "next": { - "description": "The next page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - } - } - }, - "jsonapi": { - "description": "An object describing the server's implementation", - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - }, - "error": { - "type": "object", - "properties": { - "id": { - "description": "A unique identifier for this particular occurrence of the problem.", - "type": "string" - }, - "links": { - "$ref": "#/definitions/links" - }, - "status": { - "description": "The HTTP status code applicable to this problem, expressed as a string value.", - "type": "string" - }, - "code": { - "description": "An application-specific error code, expressed as a string value.", - "type": "string" - }, - "title": { - "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.", - "type": "string" - }, - "detail": { - "description": "A human-readable explanation specific to this occurrence of the problem.", - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "pointer": { - "description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].", - "type": "string" - }, - "parameter": { - "description": "A string indicating which query parameter caused the error.", - "type": "string" - } - } - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - } - } -} \ No newline at end of file diff --git a/tests/test_collection_get.py b/tests/test_collection_get.py index 6a1bbed..9cc3903 100644 --- a/tests/test_collection_get.py +++ b/tests/test_collection_get.py @@ -1,43 +1,17 @@ -from schema import Schema -from jsonschema import validate +from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError -def test_200_with_no_querystring(): - pass - - -def describe_bad_query_params(): - def test_bad_query_param(): - pass - -def describe_resource_inclusions(): - pass - -def describe_sparse_fieldsets(): - pass - -def describe_sorting(): - pass - -def describe_pagination(): - pass - -def describe_filtering(): - pass - - -"""from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError - # 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 @@ -55,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) @@ -63,6 +36,7 @@ def test_200_with_multiple_includes(bunch_of_posts, client): for data in response.json_data['included']: assert data['type'] in ['blog-comments', 'users'] + # TODO: Sparse Fieldsets @@ -73,7 +47,6 @@ def test_200_with_single_field(bunch_of_posts, client): assert {'title'} == set(item['attributes'].keys()) assert len(item['relationships']) == 0 - def test_200_with_bad_field(bunch_of_posts, client): response = client.get( '/api/blog-posts/?fields[blog-posts]=titles').validate(200) @@ -104,6 +77,7 @@ def test_200_with_single_field_across_a_relationship(bunch_of_posts, client): assert len(item['attributes']) == 0 assert {'author'} == set(item['relationships'].keys()) + # TODO: Sorting @@ -133,6 +107,7 @@ def test_409_when_given_a_missing_field_for_sorting(bunch_of_posts, client): client.get('/api/blog-posts/?sort=never_gonna_give_you_up').validate( 409, NotSortableError) + # TODO: Pagination @@ -157,5 +132,5 @@ 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/tests/test_collection_post.py index df30f27..d01dab6 100644 --- a/tests/test_collection_post.py +++ b/tests/test_collection_post.py @@ -1,4 +1,4 @@ -"""import json +import json from sqlalchemy_jsonapi.errors import ( InvalidTypeForEndpointError, MissingTypeError, PermissionDeniedError, @@ -10,7 +10,6 @@ # TODO: Bad query param - def test_200_resource_creation(client): payload = { 'data': { @@ -57,8 +56,8 @@ def test_200_resource_creation_with_relationships(user, client): assert response.json_data['data']['type'] == 'blog-posts' assert len(response.json_data['included']) == 0 post_id = response.json_data['data']['id'] - response = client.get('/api/blog-posts/{}/'.format(post_id)).validate(200) - + response = client.get('/api/blog-posts/{}/'.format( + post_id)).validate(200) def test_200_resource_creation_with_relationships_and_include(user, client): payload = { @@ -93,7 +92,6 @@ def test_200_resource_creation_with_relationships_and_include(user, client): response = client.get('/api/blog-posts/{}/?include=author'.format( post_id)).validate(200) - def test_200_resource_creation_with_sparse_fieldset(client): payload = { 'data': { @@ -110,19 +108,17 @@ def test_200_resource_creation_with_sparse_fieldset(client): data=json.dumps(payload), content_type='application/vnd.api+json').validate(201) assert response.json_data['data']['type'] == 'users' - assert set(response.json_data['data']['attributes'].keys()) == set( - ['username']) + assert set(response.json_data['data']['attributes'].keys()) == set(['username']) user_id = response.json_data['data']['id'] response = client.get('/api/users/{}/'.format(user_id)).validate(200) def test_403_when_access_is_denied(client): payload = {'data': {'type': 'logs'}} - client.post( - '/api/logs/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.post('/api/logs/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_409_when_id_already_exists(user, client): @@ -137,24 +133,23 @@ def test_409_when_id_already_exists(user, client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_when_type_doesnt_match_endpoint(client): payload = {'data': {'type': 'blog-posts'}} - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 409, InvalidTypeForEndpointError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, InvalidTypeForEndpointError) def test_409_when_missing_content_type(client): - client.post( - '/api/users/', data='{}').validate(409, MissingContentTypeError) + client.post('/api/users/', + data='{}').validate(409, MissingContentTypeError) def test_409_when_missing_type(client): @@ -167,11 +162,10 @@ def test_409_when_missing_type(client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, - MissingTypeError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, MissingTypeError) def test_409_for_invalid_value(client): @@ -185,10 +179,10 @@ def test_409_for_invalid_value(client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_for_wrong_field_name(client): @@ -203,8 +197,7 @@ def test_409_for_wrong_field_name(client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) -""" + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) diff --git a/tests/test_related_get.py b/tests/test_related_get.py index cf16a6c..7345174 100644 --- a/tests/test_related_get.py +++ b/tests/test_related_get.py @@ -1,4 +1,4 @@ -"""from uuid import uuid4 +from uuid import uuid4 from sqlalchemy_jsonapi.errors import (RelationshipNotFoundError, ResourceNotFoundError) @@ -33,4 +33,3 @@ def test_404_when_relationship_not_found(post, client): def test_404_when_resource_not_found(client): client.get('/api/blog-posts/{}/comments/'.format(uuid4())).validate( 404, ResourceNotFoundError) -""" diff --git a/tests/test_relationship_delete.py b/tests/test_relationship_delete.py index f3d0d13..04b07e1 100644 --- a/tests/test_relationship_delete.py +++ b/tests/test_relationship_delete.py @@ -1,4 +1,4 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import ( @@ -28,38 +28,33 @@ def test_200_on_deletion_from_to_many(comment, client): def test_404_on_resource_not_found(client): - client.delete( - '/api/blog-posts/{}/relationships/comments/'.format(uuid4()), - data='{}', - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + 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/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}', - content_type='application/vnd.api+json').validate( - 404, RelationshipNotFoundError) + client.delete('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}', + content_type='application/vnd.api+json').validate( + 404, RelationshipNotFoundError) def test_403_on_permission_denied(user, client): - client.delete( - '/api/users/{}/relationships/logs/'.format(user.id), - data='{"data": []}', - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.delete('/api/users/{}/relationships/logs/'.format(user.id), + data='{"data": []}', + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_409_on_to_one_provided(post, client): - client.delete( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data='{"data": {}}', - content_type='application/vnd.api+json').validate(409, ValidationError) + 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/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}').validate(409, MissingContentTypeError) -""" + client.delete('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}').validate(409, MissingContentTypeError) diff --git a/tests/test_relationship_get.py b/tests/test_relationship_get.py index a996726..ef48592 100644 --- a/tests/test_relationship_get.py +++ b/tests/test_relationship_get.py @@ -1,4 +1,4 @@ -"""from sqlalchemy_jsonapi.errors import ( +from sqlalchemy_jsonapi.errors import ( RelationshipNotFoundError, ResourceNotFoundError, PermissionDeniedError) from uuid import uuid4 @@ -36,4 +36,3 @@ def test_404_on_relationship_not_found(post, client): def test_403_on_permission_denied(unpublished_post, client): client.get('/api/blog-posts/{}/relationships/comment/'.format( unpublished_post.id)).validate(403, PermissionDeniedError) -""" diff --git a/tests/test_relationship_patch.py b/tests/test_relationship_patch.py index 07e98ef..9c889ee 100644 --- a/tests/test_relationship_patch.py +++ b/tests/test_relationship_patch.py @@ -1,4 +1,4 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import (PermissionDeniedError, @@ -52,75 +52,69 @@ def test_200_on_to_many_set_to_empty(post, client): def test_409_on_to_one_set_to_empty_list(post, client): payload = {'data': []} - client.patch( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_to_many_set_to_null(post, client): payload = {'data': None} - client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_404_on_resource_not_found(client): - client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(uuid4()), - data='{}', - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/relationships/comments/'.format(uuid4()), + data='{}', + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_404_on_relationship_not_found(client, post): - client.patch( - '/api/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}', - content_type='application/vnd.api+json').validate( - 404, RelationshipNotFoundError) + client.patch('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}', + content_type='application/vnd.api+json').validate( + 404, RelationshipNotFoundError) def test_404_on_related_item_not_found(post, client): payload = {'data': [{'type': 'blog-comments', 'id': str(uuid4())}]} - client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_403_on_permission_denied(user, log, client): payload = {'data': {'type': 'users', 'id': str(user.id)}} - client.patch( - '/api/logs/{}/relationships/user/'.format(log.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.patch('/api/logs/{}/relationships/user/'.format(log.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_403_on_permission_denied_on_related(log, user, client): payload = {'data': {'type': 'logs', 'id': str(log.id)}} - client.patch( - '/api/users/{}/relationships/logs/'.format(user.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.patch('/api/users/{}/relationships/logs/'.format(user.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_409_on_to_one_with_incompatible_model(post, comment, client): payload = {'data': {'type': 'blog-comments', 'id': str(comment.id)}} - client.patch( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_to_many_with_incompatible_model(post, client): payload = {'data': [{'type': 'blog-posts', 'id': str(post.id)}]} - client.patch( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) -""" + client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) diff --git a/tests/test_relationship_post.py b/tests/test_relationship_post.py index 2c559fc..a771932 100644 --- a/tests/test_relationship_post.py +++ b/tests/test_relationship_post.py @@ -1,4 +1,4 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import ValidationError, ResourceNotFoundError, RelationshipNotFoundError @@ -23,18 +23,18 @@ def test_200_on_to_many(comment, post, client): def test_409_on_hash_instead_of_array_provided(comment, post, client): payload = {'data': {'type': 'blog-comments', 'id': str(comment.id)}} - client.post( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_incompatible_model(user, post, client): payload = {'data': [{'type': 'users', 'id': str(user.id)}]} - client.post( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_to_one_relationship(post, client): @@ -45,17 +45,14 @@ def test_409_on_to_one_relationship(post, client): def test_404_on_resource_not_found(client): - client.post( - '/api/blog-posts/{}/relationships/comments/'.format(uuid4()), - data='{}', - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.post('/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.post( - '/api/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}', - content_type='application/vnd.api+json').validate( - 404, RelationshipNotFoundError) -""" + client.post('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}', + content_type='application/vnd.api+json').validate( + 404, RelationshipNotFoundError) diff --git a/tests/test_resource_delete.py b/tests/test_resource_delete.py index ffc6f1b..a291c5e 100644 --- a/tests/test_resource_delete.py +++ b/tests/test_resource_delete.py @@ -1,8 +1,9 @@ -"""from uuid import uuid4 +from uuid import uuid4 from sqlalchemy_jsonapi.errors import ( PermissionDeniedError, ResourceNotFoundError, ResourceTypeNotFoundError) + # TODO: Bad query param @@ -25,4 +26,3 @@ def test_403_on_permission_denied(user, client): def test_404_on_resource_not_found(client): client.delete('/api/blog-comments/{}/'.format(uuid4())).validate( 404, ResourceNotFoundError) -""" diff --git a/tests/test_resource_get.py b/tests/test_resource_get.py index 3d680b3..30a2e49 100644 --- a/tests/test_resource_get.py +++ b/tests/test_resource_get.py @@ -1,6 +1,7 @@ -"""from sqlalchemy_jsonapi.errors import ResourceNotFoundError, PermissionDeniedError +from sqlalchemy_jsonapi.errors import ResourceNotFoundError, PermissionDeniedError from uuid import uuid4 + # TODO: Sparse Fieldsets # TODO: Related Includes # TODO: Bad query param @@ -73,4 +74,3 @@ def test_200_with_single_field_across_a_relationship(post, client): assert {'title', 'content'} == set(item['attributes'].keys()) assert len(item['attributes']) == 0 assert {'author'} == set(item['relationships'].keys()) -""" diff --git a/tests/test_resource_patch.py b/tests/test_resource_patch.py index c10aadf..d15a73a 100644 --- a/tests/test_resource_patch.py +++ b/tests/test_resource_patch.py @@ -1,9 +1,10 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import (BadRequestError, PermissionDeniedError, ResourceNotFoundError, ValidationError) + # TODO: Sparse Fieldsets # TODO: Related Includes # TODO: Bad query param @@ -38,17 +39,16 @@ def test_200(client, post, user): def test_400_missing_type(post, client): - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps({}), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps({}), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_404_resource_not_found(client): - client.patch( - '/api/blog-posts/{}/'.format(uuid4()), - content_type='application/vnd.api+json', - data='{}').validate(404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/'.format(uuid4()), + content_type='application/vnd.api+json', + data='{}').validate(404, ResourceNotFoundError) def test_404_related_resource_not_found(client, post): @@ -66,11 +66,10 @@ def test_404_related_resource_not_found(client, post): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_400_field_not_found(client, post, user): @@ -88,10 +87,10 @@ def test_400_field_not_found(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_409_type_mismatch_to_one(client, post, user): @@ -109,10 +108,10 @@ def test_409_type_mismatch_to_one(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_400_type_mismatch_to_many(client, post, user): @@ -130,10 +129,10 @@ def test_400_type_mismatch_to_many(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_409_validation_failed(client, post, user): @@ -154,10 +153,10 @@ def test_409_validation_failed(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_400_type_does_not_match_endpoint(client, post, user): @@ -178,16 +177,14 @@ def test_400_type_does_not_match_endpoint(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_403_permission_denied(user, client): - client.patch( - '/api/users/{}/'.format(user.id), - data='{}', - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) -""" + client.patch('/api/users/{}/'.format(user.id), + data='{}', + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 18a4506..38fe038 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,4 +1,4 @@ -"""from app import api +from app import api import uuid @@ -10,4 +10,3 @@ def test_include_different_types_same_id(session, comment): r = api.serializer.get_resource(session, {'include': 'post,author'}, 'blog-comments', comment.id) assert len(r.data['included']) == 2 -""" From 0068a4b922ce99569668e5b0a986d31b79815442 Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 2/9] 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 Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 3/9] Revert "Fix to constants docs. Last commit of night. Too tired. Need sleep." This reverts commit 7195452fcbc255bed399f7d03467b3afc2d9e5f9. --- docs/constants.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/constants.rst b/docs/constants.rst index 397b73b..4fa35a0 100644 --- a/docs/constants.rst +++ b/docs/constants.rst @@ -1,8 +1,8 @@ -========= -Constants -========= +====== +Errors +====== -.. currentmodule:: sqlalchemy_jsonapi.constants +.. currentmodule:: sqlalchemy_jsonapi.errors .. autoclass:: Method :members: From 8185af8a8edca9e045e51469715a8d27031f50c7 Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 4/9] Revert "Adding the constants to docs" This reverts commit 1c792ed2aebe4bc094f292cc2de2dae2ef245490. --- docs/constants.rst | 11 ----------- docs/index.rst | 1 - 2 files changed, 12 deletions(-) delete mode 100644 docs/constants.rst diff --git a/docs/constants.rst b/docs/constants.rst deleted file mode 100644 index 4fa35a0..0000000 --- a/docs/constants.rst +++ /dev/null @@ -1,11 +0,0 @@ -====== -Errors -====== - -.. currentmodule:: sqlalchemy_jsonapi.errors - -.. autoclass:: Method - :members: - -.. autoclass:: Endpoint - :members: diff --git a/docs/index.rst b/docs/index.rst index b6a6a2f..229000c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,7 +12,6 @@ Contents: serializer flask errors - constants Indices and tables From 267272bc62d07023e61d1afda6f68ac5e22d61e0 Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 5/9] Revert "Adding flask API docs" This reverts commit 7ff4015b2cc15ce2938c346aee20edbcd81133b2. --- docs/flask.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/flask.rst b/docs/flask.rst index 44c252e..4f2165b 100644 --- a/docs/flask.rst +++ b/docs/flask.rst @@ -2,7 +2,7 @@ Flask ===== -.. currentmodule:: sqlalchemy_jsonapi.flaskext +.. currentmodule:: sqlalchemy_jsonapi.flask To those who use Flask, setting up SQLAlchemy-JSONAPI can be extremely complex and frustrating. Let's look at an example:: From 5fb5f5c17b7731f01153a13643f29da96c1e62df Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 6/9] Revert "Fixed version file. It's 2 AM, so don't blame me." This reverts commit ae77a3ccd60f1f331a307c0ad6f8ddd5be4ded51. --- setup.py | 2 +- sqlalchemy_jsonapi/__init__.py | 2 +- sqlalchemy_jsonapi/serializer.py | 2 +- sqlalchemy_jsonapi/{_version.py => version.py} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename sqlalchemy_jsonapi/{_version.py => version.py} (100%) diff --git a/setup.py b/setup.py index 5579599..127c66b 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ requirements = ['SQLAlchemy', 'inflection'] -with open('sqlalchemy_jsonapi/_version.py', 'r') as f: +with open('sqlalchemy_jsonapi/__version__.py', 'r') as f: version = ast.parse(f.read()).body[0].value.s if sys.version_info[0] != 3 or sys.version_info[1] < 4: diff --git a/sqlalchemy_jsonapi/__init__.py b/sqlalchemy_jsonapi/__init__.py index 1e3996c..ad95029 100644 --- a/sqlalchemy_jsonapi/__init__.py +++ b/sqlalchemy_jsonapi/__init__.py @@ -3,7 +3,7 @@ AttributeActions, Permissions, RelationshipActions, attr_descriptor, permission_test, relationship_descriptor) -from ._version import __version__ +from .version import __version__ try: from .flaskext import FlaskJSONAPI diff --git a/sqlalchemy_jsonapi/serializer.py b/sqlalchemy_jsonapi/serializer.py index 1185c87..78dcaea 100644 --- a/sqlalchemy_jsonapi/serializer.py +++ b/sqlalchemy_jsonapi/serializer.py @@ -16,7 +16,7 @@ RelationshipNotFoundError, ResourceNotFoundError, ResourceTypeNotFoundError, ToManyExpectedError, ValidationError) -from ._version import __version__ +from .version import __version__ class AttributeActions(Enum): diff --git a/sqlalchemy_jsonapi/_version.py b/sqlalchemy_jsonapi/version.py similarity index 100% rename from sqlalchemy_jsonapi/_version.py rename to sqlalchemy_jsonapi/version.py From fd0be761f6d3bc7b33a0914c1aa3610b38d382fe Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 7/9] Revert "Fixing path for version file. Been working on MNL too much." This reverts commit 2a07cfb16057751d7a870b7f3a28e60f3a84253a. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 127c66b..a7b7806 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ requirements = ['SQLAlchemy', 'inflection'] -with open('sqlalchemy_jsonapi/__version__.py', 'r') as f: +with open('mynewleaf/__version__.py', 'r') as f: version = ast.parse(f.read()).body[0].value.s if sys.version_info[0] != 3 or sys.version_info[1] < 4: From 883a0517e95ab5c8e2c675a8e12072412381ad16 Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 8/9] Revert "A little bit of documentation" This reverts commit a4d2fd221b8e6689e889eebf9ba13e1aebb12fb6. --- docs/errors.rst | 46 +-------------------------------------------- docs/serializer.rst | 7 +------ 2 files changed, 2 insertions(+), 51 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 6f67af2..8571920 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -1,47 +1,3 @@ ====== Errors -====== - -.. currentmodule:: sqlalchemy_jsonapi.errors - -.. autoclass:: BaseError - :members: - -.. autoclass:: BadRequestError - :members: - -.. autoclass:: NotAnAttributeError - :members: - -.. autoclass:: NotSortableError - :members: - -.. autoclass:: PermissionDeniedError - :members: - -.. autoclass:: InvalidTypeForEndpointError - :members: - -.. autoclass:: MissingTypeError - :members: - -.. autoclass:: MissingContentTypeError - :members: - -.. autoclass:: ValidationError - :members: - -.. autoclass:: ResourceNotFoundError - :members: - -.. autoclass:: RelatedResourceNotFoundError - :members: - -.. autoclass:: RelationshipNotFoundError - :members: - -.. autoclass:: ToManyExpectedError - :members: - -.. autoclass:: ResourceTypeNotFoundError - :members: +====== \ No newline at end of file diff --git a/docs/serializer.rst b/docs/serializer.rst index 4f85a59..d0bdb09 100644 --- a/docs/serializer.rst +++ b/docs/serializer.rst @@ -1,8 +1,3 @@ ========== Serializer -========== - -.. currentmodule:: sqlalchemy_jsonapi.serializer - -.. autoclass:: JSONAPI - :members: +========== \ No newline at end of file From 3c59683f8a2dc7951d3194ebd24b1de37e8421a1 Mon Sep 17 00:00:00 2001 From: Deryck Hodge Date: Wed, 8 Feb 2017 14:35:11 -0800 Subject: [PATCH 9/9] Revert "BROKEN: Starting implementation of query params properly" This reverts commit 4dcff7d78f9393152f4ddd2c150ea211f9bb305b. --- CHANGES.md | 3 +- setup.py | 8 +- sqlalchemy_jsonapi/__init__.py | 3 +- sqlalchemy_jsonapi/constants.py | 3 +- sqlalchemy_jsonapi/flaskext.py | 10 +- sqlalchemy_jsonapi/serializer.py | 148 ++++++++---------- sqlalchemy_jsonapi/tests/app.py | 22 +-- sqlalchemy_jsonapi/tests/conftest.py | 24 +-- .../tests/test_collection_get.py | 44 ++---- .../tests/test_collection_post.py | 75 ++------- sqlalchemy_jsonapi/tests/test_related_get.py | 17 +- .../tests/test_relationship_delete.py | 22 ++- .../tests/test_relationship_get.py | 32 ++-- .../tests/test_relationship_patch.py | 52 +++--- .../tests/test_relationship_post.py | 25 ++- .../tests/test_resource_delete.py | 3 - sqlalchemy_jsonapi/tests/test_resource_get.py | 19 +-- .../tests/test_resource_patch.py | 20 +-- sqlalchemy_jsonapi/tests/test_serializer.py | 3 +- sqlalchemy_jsonapi/version.py | 1 - 20 files changed, 205 insertions(+), 329 deletions(-) delete mode 100644 sqlalchemy_jsonapi/version.py diff --git a/CHANGES.md b/CHANGES.md index ad62544..71cd9e0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,9 @@ # SQLAlchemy-JSONAPI Changelog -## 5.0.0 +## 4.0.9 *Unreleased* -* BREAKING: The query data is now expected for all endpoints * Fixed bug during testing in delete_relationship where returned resource was missing data key * Fixed bug during testing in patch_resource where field check was failing diff --git a/setup.py b/setup.py index a7b7806..4f72f56 100644 --- a/setup.py +++ b/setup.py @@ -14,18 +14,14 @@ from setuptools import setup import sys -import ast requirements = ['SQLAlchemy', 'inflection'] -with open('mynewleaf/__version__.py', 'r') as f: - version = ast.parse(f.read()).body[0].value.s - if sys.version_info[0] != 3 or sys.version_info[1] < 4: - requirements.append('enum34') + requirements.append('enum34') setup(name='SQLAlchemy-JSONAPI', - version=version, + version='4.0.9', url='http://github.com/coltonprovias/sqlalchemy-jsonapi', license='MIT', author='Colton J. Provias', diff --git a/sqlalchemy_jsonapi/__init__.py b/sqlalchemy_jsonapi/__init__.py index ad95029..9f9fd7f 100644 --- a/sqlalchemy_jsonapi/__init__.py +++ b/sqlalchemy_jsonapi/__init__.py @@ -3,9 +3,8 @@ AttributeActions, Permissions, RelationshipActions, attr_descriptor, permission_test, relationship_descriptor) -from .version import __version__ try: from .flaskext import FlaskJSONAPI except ImportError: - FlaskJSONAPI = None + FlaskJSONAPI = None \ No newline at end of file diff --git a/sqlalchemy_jsonapi/constants.py b/sqlalchemy_jsonapi/constants.py index b1501a9..c7b519a 100644 --- a/sqlalchemy_jsonapi/constants.py +++ b/sqlalchemy_jsonapi/constants.py @@ -5,6 +5,7 @@ MIT License """ + try: from enum import Enum except ImportError: @@ -26,4 +27,4 @@ class Endpoint(Enum): COLLECTION = '/' RESOURCE = '//' RELATED = '///' - RELATIONSHIP = '///relationships/' + RELATIONSHIP = '///relationships/' \ No newline at end of file diff --git a/sqlalchemy_jsonapi/flaskext.py b/sqlalchemy_jsonapi/flaskext.py index 1e8f6ce..2548d32 100644 --- a/sqlalchemy_jsonapi/flaskext.py +++ b/sqlalchemy_jsonapi/flaskext.py @@ -163,11 +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, - prefix='{}://{}{}'.format( - self.app.config['PREFERRED_URL_SCHEME'], - self.app.config['SERVER_NAME'], - route_prefix)) + 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 @@ -224,8 +220,8 @@ def new_view(**kwargs): try: attr = '{}_{}'.format(method.name, endpoint.name).lower() handler = getattr(self.serializer, attr) - handler_chain = list(self._handler_chains.get((kwargs[ - 'api_type'], method, endpoint), [])) + handler_chain = list(self._handler_chains.get(( + kwargs['api_type'], method, endpoint), [])) handler_chain.append(handler) chained_handler = self._call_next(handler_chain) response = chained_handler(*args) diff --git a/sqlalchemy_jsonapi/serializer.py b/sqlalchemy_jsonapi/serializer.py index 78dcaea..16dcef1 100644 --- a/sqlalchemy_jsonapi/serializer.py +++ b/sqlalchemy_jsonapi/serializer.py @@ -5,18 +5,19 @@ MIT License """ +from collections import MutableMapping from enum import Enum -from inflection import dasherize, tableize, underscore +from inflection import pluralize, dasherize, parameterize, tableize, underscore from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.interfaces import MANYTOONE from sqlalchemy.util.langhelpers import iterate_attributes +from pprint import pprint from .errors import (BadRequestError, InvalidTypeForEndpointError, MissingTypeError, NotSortableError, PermissionDeniedError, RelationshipNotFoundError, ResourceNotFoundError, ResourceTypeNotFoundError, ToManyExpectedError, ValidationError) -from .version import __version__ class AttributeActions(Enum): @@ -139,7 +140,7 @@ def __init__(self): self.status_code = 200 self.data = { 'jsonapi': {'version': '1.0'}, - 'meta': {'sqlalchemy_jsonapi_version': __version__} + 'meta': {'sqlalchemy_jsonapi_version': '4.0.9'} } @@ -226,8 +227,7 @@ def __init__(self, base, prefix=''): continue prepped_name = self._api_type_for_model(model) - api_type = getattr(model, '__jsonapi_type_override__', - prepped_name) + api_type = getattr(model, '__jsonapi_type_override__', prepped_name) model_keys = set(model.__mapper__.all_orm_descriptors.keys()) model_keys |= set(model.__mapper__.relationships.keys()) @@ -236,14 +236,8 @@ def __init__(self, base, prefix=''): model.__jsonapi_rel_desc__ = {} model.__jsonapi_permissions__ = {} model.__jsonapi_type__ = api_type - model.__jsonapi_map_to_py__ = { - dasherize(underscore(x)): x - for x in model_keys - } - model.__jsonapi_map_to_api__ = { - v: k - for k, v in model.__jsonapi_map_to_py__.items() - } + model.__jsonapi_map_to_py__ = {dasherize(underscore(x)): x for x in model_keys} + model.__jsonapi_map_to_api__ = {x: dasherize(underscore(x)) for x in model_keys} for prop_name, prop_value in iterate_attributes(model): @@ -298,8 +292,7 @@ def _fetch_model(self, api_type): def _lazy_relationship(self, api_type, obj_id, rel_key): return { - 'self': '{}/{}/{}/relationships/{}'.format(self.prefix, api_type, - obj_id, rel_key), + 'self': '{}/{}/{}/relationships/{}'.format(self.prefix, api_type, obj_id, rel_key), 'related': '{}/{}/{}/{}'.format(self.prefix, api_type, obj_id, rel_key) } @@ -366,15 +359,13 @@ 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])) + local_fields = list(map((lambda x: instance.__jsonapi_map_to_py__[x]), fields[api_type])) else: local_fields = orm_desc_keys for key, relationship in instance.__mapper__.relationships.items(): - attrs_to_ignore |= set( - [c.name for c in relationship.local_columns]) | {key} + attrs_to_ignore |= set([c.name for c in relationship.local_columns + ]) | {key} api_key = instance.__jsonapi_map_to_api__[key] @@ -393,29 +384,23 @@ def _render_full_resource(self, instance, include, fields): if api_key in include.keys(): related = desc(instance) if related is not None: - perm = get_permission_test(related, None, - Permissions.VIEW) - if key in local_fields and (related is None or - not perm(related)): + perm = get_permission_test(related, None, Permissions.VIEW) + if key in local_fields and (related is None or not perm(related)): to_ret['relationships'][api_key]['data'] = None continue if key in local_fields: - to_ret['relationships'][api_key][ - 'data'] = self._render_short_instance(related) + to_ret['relationships'][api_key]['data'] = self._render_short_instance(related) new_include = self._parse_include(include[api_key]) - built = self._render_full_resource(related, new_include, - fields) + built = self._render_full_resource(related, new_include, fields) included = built.pop('included') to_ret['included'].update(included) - to_ret['included'][(related.__jsonapi_type__, - related.id)] = built + to_ret['included'][(related.__jsonapi_type__, related.id)] = built else: if key in local_fields: to_ret['relationships'][api_key] = { - 'links': self._lazy_relationship(api_type, instance.id, - api_key), + 'links': self._lazy_relationship(api_type, instance.id, api_key), } if api_key not in include.keys(): @@ -433,23 +418,20 @@ def _render_full_resource(self, instance, include, fields): continue if key in local_fields: - to_ret['relationships'][api_key]['data'].append( - self._render_short_instance(item)) + to_ret['relationships'][api_key]['data'].append(self._render_short_instance(item)) new_include = self._parse_include(include[api_key]) built = self._render_full_resource(item, new_include, fields) included = built.pop('included') to_ret['included'].update(included) - to_ret['included'][(item.__jsonapi_type__, - item.id)] = built + to_ret['included'][(item.__jsonapi_type__, item.id)] = built for key in set(orm_desc_keys) - attrs_to_ignore: try: desc = get_attr_desc(instance, key, AttributeActions.GET) if key in local_fields: - to_ret['attributes'][instance.__jsonapi_map_to_api__[ - key]] = desc(instance) + to_ret['attributes'][instance.__jsonapi_map_to_api__[key]] = desc(instance) except PermissionDeniedError: continue @@ -744,8 +726,8 @@ def get_related(self, session, query, api_type, obj_id, rel_key): for item in related: try: - response.data['data'].append(self._render_full_resource( - item, {}, {})) + response.data['data'].append( + self._render_full_resource(item, {}, {})) except PermissionDeniedError: continue @@ -774,7 +756,7 @@ def get_relationship(self, session, query, api_type, obj_id, rel_key): RelationshipActions.GET)(resource) if relationship.direction == MANYTOONE: - if related is None: + if related == None: response.data['data'] = None else: try: @@ -786,8 +768,8 @@ def get_relationship(self, session, query, api_type, obj_id, rel_key): response.data['data'] = [] for item in related: try: - response.data['data'].append(self._render_short_instance( - item)) + response.data['data'].append( + self._render_short_instance(item)) except PermissionDeniedError: continue @@ -819,7 +801,7 @@ def patch_relationship(self, session, json_data, api_type, obj_id, try: if relationship.direction == MANYTOONE: if not isinstance(json_data['data'], dict)\ - and json_data['data'] is not None: + and json_data['data'] != None: raise ValidationError('Provided data must be a hash.') related = getattr(resource, relationship.key) @@ -829,7 +811,7 @@ def patch_relationship(self, session, json_data, api_type, obj_id, setter = get_rel_desc(resource, relationship.key, RelationshipActions.SET) - if json_data['data'] is None: + if json_data['data'] == None: setter(resource, None) else: to_relate = self._fetch_resource( @@ -906,9 +888,10 @@ def patch_resource(self, session, json_data, api_type, obj_id): - set(resource.__jsonapi_map_to_py__.keys()) if missing_keys: - raise BadRequestError('{} not relationships for {}.{}'.format( - ', '.join(list(missing_keys)), model.__jsonapi_type__, - resource.id)) + raise BadRequestError( + '{} not relationships for {}.{}'.format( + ', '.join(list(missing_keys)), + model.__jsonapi_type__, resource.id)) attrs_to_ignore = {'__mapper__', 'id'} @@ -926,20 +909,18 @@ def patch_resource(self, session, json_data, api_type, obj_id): session, json_data['data']['relationships'][api_key], model.__jsonapi_type__, resource.id, api_key) - data_keys = set(map(( - lambda x: resource.__jsonapi_map_to_py__.get(x, None)), - json_data['data']['attributes'].keys())) + data_keys = set(map((lambda x: resource.__jsonapi_map_to_py__.get(x, None)), json_data['data']['attributes'].keys())) model_keys = set(orm_desc_keys) - attrs_to_ignore if not data_keys <= model_keys: - raise BadRequestError('{} not attributes for {}.{}'.format( - ', '.join(list(data_keys - model_keys)), - model.__jsonapi_type__, resource.id)) + raise BadRequestError( + '{} not attributes for {}.{}'.format( + ', '.join(list(data_keys - model_keys)), + model.__jsonapi_type__, resource.id)) for key in data_keys & model_keys: setter = get_attr_desc(resource, key, AttributeActions.SET) - setter(resource, json_data['data']['attributes'][ - resource.__jsonapi_map_to_api__[key]]) + setter(resource, json_data['data']['attributes'][resource.__jsonapi_map_to_api__[key]]) session.commit() except IntegrityError as e: session.rollback() @@ -950,8 +931,8 @@ def patch_resource(self, session, json_data, api_type, obj_id): except TypeError as e: session.rollback() raise ValidationError('Incompatible data type') - return self.get_resource(session, {}, model.__jsonapi_type__, - resource.id) + return self.get_resource( + session, {}, model.__jsonapi_type__, resource.id) def post_collection(self, session, data, api_type): """ @@ -970,8 +951,8 @@ def post_collection(self, session, data, api_type): raise MissingTypeError() if data['data']['type'] != model.__jsonapi_type__: - raise InvalidTypeForEndpointError(model.__jsonapi_type__, - data['data']['type']) + raise InvalidTypeForEndpointError( + model.__jsonapi_type__, data['data']['type']) resource = model() check_permission(resource, None, Permissions.CREATE) @@ -979,14 +960,13 @@ def post_collection(self, session, data, api_type): data['data'].setdefault('relationships', {}) data['data'].setdefault('attributes', {}) - data_keys = set(map(( - lambda x: resource.__jsonapi_map_to_py__.get(x, None)), data[ - 'data'].get('relationships', {}).keys())) + data_keys = set(map((lambda x: resource.__jsonapi_map_to_py__.get(x, None)), data['data'].get('relationships', {}).keys())) model_keys = set(resource.__mapper__.relationships.keys()) if not data_keys <= model_keys: - raise BadRequestError('{} not relationships for {}'.format( - ', '.join(list(data_keys - model_keys)), - model.__jsonapi_type__)) + raise BadRequestError( + '{} not relationships for {}'.format( + ', '.join(list(data_keys - + model_keys)), model.__jsonapi_type__)) attrs_to_ignore = {'__mapper__', 'id'} @@ -1018,8 +998,8 @@ def post_collection(self, session, data, api_type): setters.append([setter, None]) else: if not isinstance(data_rel, dict): - raise BadRequestError('{} must be a hash'.format( - key)) + raise BadRequestError( + '{} must be a hash'.format(key)) if not {'type', 'id'} == set(data_rel.keys()): raise BadRequestError( '{} must have type and id keys'.format(key)) @@ -1038,8 +1018,8 @@ def post_collection(self, session, data, api_type): setter = get_rel_desc(resource, key, RelationshipActions.APPEND) if not isinstance(data_rel, list): - raise BadRequestError('{} must be an array'.format( - key)) + raise BadRequestError( + '{} must be an array'.format(key)) for item in data_rel: if not {'type', 'id'} in set(item.keys()): raise BadRequestError( @@ -1056,15 +1036,14 @@ def post_collection(self, session, data, api_type): Permissions.CREATE) setters.append([setter, to_relate]) - data_keys = set(map(( - lambda x: resource.__jsonapi_map_to_py__.get(x, None)), data[ - 'data'].get('attributes', {}).keys())) + data_keys = set(map((lambda x: resource.__jsonapi_map_to_py__.get(x, None)), data['data'].get('attributes', {}).keys())) model_keys = set(orm_desc_keys) - attrs_to_ignore if not data_keys <= model_keys: - raise BadRequestError('{} not attributes for {}'.format( - ', '.join(list(data_keys - model_keys)), - model.__jsonapi_type__)) + raise BadRequestError( + '{} not attributes for {}'.format( + ', '.join(list(data_keys - + model_keys)), model.__jsonapi_type__)) with session.no_autoflush: for setter, value in setters: @@ -1088,8 +1067,8 @@ def post_collection(self, session, data, api_type): session.rollback() raise ValidationError('Incompatible data type') session.refresh(resource) - response = self.get_resource(session, {}, model.__jsonapi_type__, - resource.id) + response = self.get_resource( + session, {}, model.__jsonapi_type__, resource.id) response.status_code = 201 return response @@ -1125,13 +1104,14 @@ def post_relationship(self, session, json_data, api_type, obj_id, rel_key): RelationshipActions.APPEND) if not isinstance(json_data['data'], list): - raise BadRequestError('{} must be an array'.format( - relationship.key)) + raise BadRequestError( + '{} must be an array'.format(relationship.key)) for item in json_data['data']: if {'type', 'id'} != set(item.keys()): - raise BadRequestError('{} must have type and id keys' - .format(relationship.key)) + raise BadRequestError( + '{} must have type and id keys' + .format(relationship.key)) to_relate = self._fetch_resource( session, item['type'], item['id'], Permissions.EDIT) @@ -1154,5 +1134,5 @@ def post_relationship(self, session, json_data, api_type, obj_id, rel_key): except KeyError: raise ValidationError('Incompatible type provided') - return self.get_relationship(session, {}, model.__jsonapi_type__, - resource.id, rel_key) + return self.get_relationship( + session, {}, model.__jsonapi_type__, resource.id, rel_key) diff --git a/sqlalchemy_jsonapi/tests/app.py b/sqlalchemy_jsonapi/tests/app.py index ab262ce..386fe75 100644 --- a/sqlalchemy_jsonapi/tests/app.py +++ b/sqlalchemy_jsonapi/tests/app.py @@ -12,9 +12,7 @@ from sqlalchemy import Boolean, Column, ForeignKey, Unicode, UnicodeText from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import backref, relationship, validates -from sqlalchemy_jsonapi import (INTERACTIVE_PERMISSIONS, Endpoint, - FlaskJSONAPI, Method, Permissions, - permission_test) +from sqlalchemy_jsonapi import FlaskJSONAPI, Permissions, permission_test, Method, Endpoint, INTERACTIVE_PERMISSIONS from sqlalchemy_utils import EmailType, PasswordType, Timestamp, UUIDType app = Flask(__name__) @@ -36,9 +34,9 @@ class User(Timestamp, db.Model): id = Column(UUIDType, default=uuid4, primary_key=True) username = Column(Unicode(30), unique=True, nullable=False) email = Column(EmailType, nullable=False) - password = Column( - PasswordType(schemes=['bcrypt']), - nullable=False) + password = Column(PasswordType(schemes=['bcrypt']), + nullable=False, + info={'allow_serialize': False}) is_admin = Column(Boolean, default=False) @hybrid_property @@ -106,7 +104,8 @@ class BlogPost(Timestamp, db.Model): author = relationship('User', lazy='joined', - backref=backref('posts', lazy='dynamic')) + backref=backref('posts', + lazy='dynamic')) @validates('title') def validate_title(self, key, title): @@ -137,7 +136,8 @@ class BlogComment(Timestamp, db.Model): post = relationship('BlogPost', lazy='joined', - backref=backref('comments', lazy='dynamic')) + backref=backref('comments', + lazy='dynamic')) author = relationship('User', lazy='joined', backref=backref('comments', @@ -152,10 +152,12 @@ class Log(Timestamp, db.Model): post = relationship('BlogPost', lazy='joined', - backref=backref('logs', lazy='dynamic')) + backref=backref('logs', + lazy='dynamic')) user = relationship('User', lazy='joined', - backref=backref('logs', lazy='dynamic')) + backref=backref('logs', + lazy='dynamic')) @permission_test(INTERACTIVE_PERMISSIONS) def block_interactive(cls): diff --git a/sqlalchemy_jsonapi/tests/conftest.py b/sqlalchemy_jsonapi/tests/conftest.py index 9e93e06..e028a30 100644 --- a/sqlalchemy_jsonapi/tests/conftest.py +++ b/sqlalchemy_jsonapi/tests/conftest.py @@ -61,8 +61,8 @@ def validate(self, status_code, error=None): if error: assert self.status_code == error.status_code assert self.json_data['errors'][0]['code'] == error.code - assert self.json_data['errors'][0][ - 'status'] == error.status_code + assert self.json_data['errors'][0]['status' + ] == error.status_code return self @@ -88,9 +88,9 @@ def user(session): @pytest.fixture def post(user, session): new_post = BlogPost(author=user, - title=fake.sentence(), - content=fake.paragraph(), - is_published=True) + title=fake.sentence(), + content=fake.paragraph(), + is_published=True) session.add(new_post) session.commit() return new_post @@ -99,9 +99,9 @@ def post(user, session): @pytest.fixture def unpublished_post(user, session): new_post = BlogPost(author=user, - title=fake.sentence(), - content=fake.paragraph(), - is_published=False) + title=fake.sentence(), + content=fake.paragraph(), + is_published=False) session.add(new_post) session.commit() return new_post @@ -111,12 +111,12 @@ def unpublished_post(user, session): def bunch_of_posts(user, session): for x in range(30): new_post = BlogPost(author=user, - title=fake.sentence(), - content=fake.paragraph(), - is_published=fake.boolean()) + title=fake.sentence(), + content=fake.paragraph(), + is_published=fake.boolean()) session.add(new_post) new_post.comments.append(BlogComment(author=user, - content=fake.paragraph())) + content=fake.paragraph())) session.commit() diff --git a/sqlalchemy_jsonapi/tests/test_collection_get.py b/sqlalchemy_jsonapi/tests/test_collection_get.py index a66a7e6..8442eb8 100644 --- a/sqlalchemy_jsonapi/tests/test_collection_get.py +++ b/sqlalchemy_jsonapi/tests/test_collection_get.py @@ -1,10 +1,5 @@ -from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError - - -# TODO: Ember-style filtering -# TODO: Simple filtering -# TODO: Complex filtering -# TODO: Bad query param +from sqlalchemy_jsonapi.errors import ( + BadRequestError, NotAnAttributeError, NotSortableError) def test_200_with_no_querystring(bunch_of_posts, client): @@ -21,43 +16,31 @@ def test_200_with_single_included_model(bunch_of_posts, client): def test_200_with_including_model_and_including_inbetween(bunch_of_posts, client): - response = client.get('/api/blog-comments/?include=post.author').validate( - 200) + 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 ['blog-posts', 'users'] def test_200_with_multiple_includes(bunch_of_posts, client): - response = client.get('/api/blog-posts/?include=comments,author').validate( - 200) + 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 ['blog-comments', 'users'] def test_200_with_single_field(bunch_of_posts, client): - response = client.get( - '/api/blog-posts/?fields[blog-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_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 len(item['relationships']) == 0 - def test_200_with_multiple_fields(bunch_of_posts, client): - response = client.get( - '/api/blog-posts/?fields[blog-posts]=title,content,is-published').validate( - 200) + 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', 'is-published'} == set(item[ - 'attributes'].keys()) + assert {'title', 'content', 'is-published'} == set(item['attributes'].keys()) assert len(item['relationships']) == 0 @@ -102,20 +85,19 @@ def test_409_when_given_a_missing_field_for_sorting(bunch_of_posts, client): 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) + 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/blog-posts/?page[offset]=5&page[limit]=5').validate(200) + 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/blog-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): diff --git a/sqlalchemy_jsonapi/tests/test_collection_post.py b/sqlalchemy_jsonapi/tests/test_collection_post.py index d01dab6..8dba8e2 100644 --- a/sqlalchemy_jsonapi/tests/test_collection_post.py +++ b/sqlalchemy_jsonapi/tests/test_collection_post.py @@ -8,7 +8,6 @@ fake = Faker() -# TODO: Bad query param def test_200_resource_creation(client): payload = { @@ -21,10 +20,10 @@ def test_200_resource_creation(client): } } } - response = client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(201) + response = client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 201) assert response.json_data['data']['type'] == 'users' user_id = response.json_data['data']['id'] response = client.get('/api/users/{}/'.format(user_id)).validate(200) @@ -49,68 +48,16 @@ def test_200_resource_creation_with_relationships(user, client): } } } - response = client.post( - '/api/blog-posts/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(201) + response = client.post('/api/blog-posts/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 201) assert response.json_data['data']['type'] == 'blog-posts' - assert len(response.json_data['included']) == 0 post_id = response.json_data['data']['id'] - response = client.get('/api/blog-posts/{}/'.format( - post_id)).validate(200) - -def test_200_resource_creation_with_relationships_and_include(user, client): - payload = { - 'data': { - 'type': 'blog-posts', - 'attributes': { - 'title': 'Some title', - 'content': 'Hello, World!', - 'is-published': True - }, - 'relationships': { - 'author': { - 'data': { - 'type': 'users', - 'id': str(user.id) - } - } - } - } - } - response = client.post( - '/api/blog-posts/?include=author', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(201) - assert response.json_data['data']['type'] == 'blog-posts' + 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) - assert len(response.json_data['included']) == 1 - for data in response.json_data['included']: - assert data['type'] == 'users' - post_id = response.json_data['data']['id'] - response = client.get('/api/blog-posts/{}/?include=author'.format( - post_id)).validate(200) - -def test_200_resource_creation_with_sparse_fieldset(client): - payload = { - 'data': { - 'type': 'users', - 'attributes': { - 'username': fake.user_name(), - 'email': 'user@example.com', - 'password': 'password' - } - } - } - response = client.post( - '/api/users/?fields[users]=username', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(201) - assert response.json_data['data']['type'] == 'users' - assert set(response.json_data['data']['attributes'].keys()) == set(['username']) - user_id = response.json_data['data']['id'] - response = client.get('/api/users/{}/'.format(user_id)).validate(200) + 'id' + ] == str(user.id) def test_403_when_access_is_denied(client): diff --git a/sqlalchemy_jsonapi/tests/test_related_get.py b/sqlalchemy_jsonapi/tests/test_related_get.py index 7345174..225d028 100644 --- a/sqlalchemy_jsonapi/tests/test_related_get.py +++ b/sqlalchemy_jsonapi/tests/test_related_get.py @@ -3,19 +3,10 @@ from sqlalchemy_jsonapi.errors import (RelationshipNotFoundError, ResourceNotFoundError) -# TODO: Sparse Fieldsets -# TODO: Related Includes -# TODO: Sorting -# TODO: Pagination -# TODO: Ember-style filtering -# TODO: Simple filtering -# TODO: Complex filtering -# TODO: Bad query param - def test_200_result_of_to_one(post, client): - response = client.get('/api/blog-posts/{}/author/'.format( - post.id)).validate(200) + response = client.get('/api/blog-posts/{}/author/'.format(post.id)).validate( + 200) assert response.json_data['data']['type'] == 'users' @@ -26,8 +17,8 @@ def test_200_collection_of_to_many(comment, client): def test_404_when_relationship_not_found(post, client): - client.get('/api/blog-posts/{}/last_comment/'.format(post.id)).validate( - 404, RelationshipNotFoundError) + client.get('/api/blog-posts/{}/last_comment/'.format( + post.id)).validate(404, RelationshipNotFoundError) def test_404_when_resource_not_found(client): diff --git a/sqlalchemy_jsonapi/tests/test_relationship_delete.py b/sqlalchemy_jsonapi/tests/test_relationship_delete.py index 04b07e1..c7ae5a8 100644 --- a/sqlalchemy_jsonapi/tests/test_relationship_delete.py +++ b/sqlalchemy_jsonapi/tests/test_relationship_delete.py @@ -6,18 +6,12 @@ ResourceNotFoundError, ToManyExpectedError, MissingContentTypeError, ValidationError) -# TODO: Sorting -# TODO: Pagination -# TODO: Ember-style filtering -# TODO: Simple filtering -# TODO: Complex filtering -# TODO: Bad query param - def test_200_on_deletion_from_to_many(comment, client): payload = {'data': [{'type': 'blog-comments', 'id': str(comment.id)}]} response = client.delete( - '/api/blog-posts/{}/relationships/comments/'.format(comment.post.id), + '/api/blog-posts/{}/relationships/comments/'.format( + comment.post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate(200) for item in response.json_data['data']: @@ -35,26 +29,30 @@ def test_404_on_resource_not_found(client): def test_404_on_relationship_not_found(post, client): - client.delete('/api/blog-posts/{}/relationships/comment/'.format(post.id), + client.delete('/api/blog-posts/{}/relationships/comment/'.format( + post.id), data='{}', content_type='application/vnd.api+json').validate( 404, RelationshipNotFoundError) def test_403_on_permission_denied(user, client): - client.delete('/api/users/{}/relationships/logs/'.format(user.id), + client.delete('/api/users/{}/relationships/logs/'.format( + user.id), data='{"data": []}', content_type='application/vnd.api+json').validate( 403, PermissionDeniedError) def test_409_on_to_one_provided(post, client): - client.delete('/api/blog-posts/{}/relationships/author/'.format(post.id), + 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/blog-posts/{}/relationships/comment/'.format(post.id), + client.delete('/api/blog-posts/{}/relationships/comment/'.format( + post.id), data='{}').validate(409, MissingContentTypeError) diff --git a/sqlalchemy_jsonapi/tests/test_relationship_get.py b/sqlalchemy_jsonapi/tests/test_relationship_get.py index ef48592..761784c 100644 --- a/sqlalchemy_jsonapi/tests/test_relationship_get.py +++ b/sqlalchemy_jsonapi/tests/test_relationship_get.py @@ -2,37 +2,35 @@ RelationshipNotFoundError, ResourceNotFoundError, PermissionDeniedError) from uuid import uuid4 -# TODO: Sorting -# TODO: Pagination -# TODO: Ember-style filtering -# TODO: Simple filtering -# TODO: Complex filtering -# TODO: Bad query param - def test_200_on_to_many(post, client): - response = client.get('/api/blog-posts/{}/relationships/comments/'.format( - post.id)).validate(200) + response = client.get( + '/api/blog-posts/{}/relationships/comments/'.format( + post.id)).validate(200) for item in response.json_data['data']: assert {'id', 'type'} == set(item.keys()) def test_200_on_to_one(post, client): - response = client.get('/api/blog-posts/{}/relationships/author/'.format( - post.id)).validate(200) + response = client.get( + '/api/blog-posts/{}/relationships/author/'.format( + post.id)).validate(200) assert response.json_data['data']['type'] == 'users' def test_404_on_resource_not_found(client): - client.get('/api/blog-posts/{}/relationships/comments/'.format(uuid4( - ))).validate(404, ResourceNotFoundError) + client.get( + '/api/blog-posts/{}/relationships/comments/'.format(uuid4())).validate( + 404, ResourceNotFoundError) def test_404_on_relationship_not_found(post, client): - client.get('/api/blog-posts/{}/relationships/comment/'.format( - post.id)).validate(404, RelationshipNotFoundError) + client.get( + '/api/blog-posts/{}/relationships/comment/'.format( + post.id)).validate(404, RelationshipNotFoundError) def test_403_on_permission_denied(unpublished_post, client): - client.get('/api/blog-posts/{}/relationships/comment/'.format( - unpublished_post.id)).validate(403, PermissionDeniedError) + client.get( + '/api/blog-posts/{}/relationships/comment/'.format( + unpublished_post.id)).validate(403, PermissionDeniedError) diff --git a/sqlalchemy_jsonapi/tests/test_relationship_patch.py b/sqlalchemy_jsonapi/tests/test_relationship_patch.py index 9c889ee..d1d8df5 100644 --- a/sqlalchemy_jsonapi/tests/test_relationship_patch.py +++ b/sqlalchemy_jsonapi/tests/test_relationship_patch.py @@ -5,13 +5,6 @@ RelationshipNotFoundError, ResourceNotFoundError, ValidationError) -# TODO: Sorting -# TODO: Pagination -# TODO: Ember-style filtering -# TODO: Simple filtering -# TODO: Complex filtering -# TODO: Bad query param - def test_200_on_to_one_set_to_resource(post, user, client): payload = {'data': {'type': 'users', 'id': str(user.id)}} @@ -33,26 +26,29 @@ def test_200_on_to_one_set_to_null(post, client): def test_200_on_to_many_set_to_resources(post, comment, client): payload = {'data': [{'type': 'blog-comments', 'id': str(comment.id)}]} - response = client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(200) + response = client.patch('/api/blog-posts/{}/relationships/comments/'.format( + post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 200) assert response.json_data['data'][0]['id'] == str(comment.id) assert len(response.json_data['data']) == 1 def test_200_on_to_many_set_to_empty(post, client): payload = {'data': []} - response = client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(200) + response = client.patch('/api/blog-posts/{}/relationships/comments/'.format( + post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 200) assert len(response.json_data['data']) == 0 def test_409_on_to_one_set_to_empty_list(post, client): payload = {'data': []} - client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + client.patch('/api/blog-posts/{}/relationships/author/'.format( + post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 409, ValidationError) @@ -60,21 +56,24 @@ def test_409_on_to_one_set_to_empty_list(post, client): def test_409_on_to_many_set_to_null(post, client): payload = {'data': None} - client.patch('/api/blog-posts/{}/relationships/comments/'.format(post.id), + client.patch('/api/blog-posts/{}/relationships/comments/'.format( + post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 409, ValidationError) def test_404_on_resource_not_found(client): - client.patch('/api/blog-posts/{}/relationships/comments/'.format(uuid4()), + client.patch('/api/blog-posts/{}/relationships/comments/'.format( + uuid4()), data='{}', content_type='application/vnd.api+json').validate( 404, ResourceNotFoundError) def test_404_on_relationship_not_found(client, post): - client.patch('/api/blog-posts/{}/relationships/comment/'.format(post.id), + client.patch('/api/blog-posts/{}/relationships/comment/'.format( + post.id), data='{}', content_type='application/vnd.api+json').validate( 404, RelationshipNotFoundError) @@ -82,7 +81,8 @@ def test_404_on_relationship_not_found(client, post): def test_404_on_related_item_not_found(post, client): payload = {'data': [{'type': 'blog-comments', 'id': str(uuid4())}]} - client.patch('/api/blog-posts/{}/relationships/comments/'.format(post.id), + client.patch('/api/blog-posts/{}/relationships/comments/'.format( + post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 404, ResourceNotFoundError) @@ -90,7 +90,8 @@ def test_404_on_related_item_not_found(post, client): def test_403_on_permission_denied(user, log, client): payload = {'data': {'type': 'users', 'id': str(user.id)}} - client.patch('/api/logs/{}/relationships/user/'.format(log.id), + client.patch('/api/logs/{}/relationships/user/'.format( + log.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 403, PermissionDeniedError) @@ -98,7 +99,8 @@ def test_403_on_permission_denied(user, log, client): def test_403_on_permission_denied_on_related(log, user, client): payload = {'data': {'type': 'logs', 'id': str(log.id)}} - client.patch('/api/users/{}/relationships/logs/'.format(user.id), + client.patch('/api/users/{}/relationships/logs/'.format( + user.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 403, PermissionDeniedError) @@ -106,7 +108,8 @@ def test_403_on_permission_denied_on_related(log, user, client): def test_409_on_to_one_with_incompatible_model(post, comment, client): payload = {'data': {'type': 'blog-comments', 'id': str(comment.id)}} - client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + client.patch('/api/blog-posts/{}/relationships/author/'.format( + post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 409, ValidationError) @@ -114,7 +117,8 @@ def test_409_on_to_one_with_incompatible_model(post, comment, client): def test_409_on_to_many_with_incompatible_model(post, client): payload = {'data': [{'type': 'blog-posts', 'id': str(post.id)}]} - client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + client.patch('/api/blog-posts/{}/relationships/author/'.format( + post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 409, ValidationError) diff --git a/sqlalchemy_jsonapi/tests/test_relationship_post.py b/sqlalchemy_jsonapi/tests/test_relationship_post.py index a771932..287fedf 100644 --- a/sqlalchemy_jsonapi/tests/test_relationship_post.py +++ b/sqlalchemy_jsonapi/tests/test_relationship_post.py @@ -3,27 +3,22 @@ from sqlalchemy_jsonapi.errors import ValidationError, ResourceNotFoundError, RelationshipNotFoundError -# TODO: Sorting -# TODO: Pagination -# TODO: Ember-style filtering -# TODO: Simple filtering -# TODO: Complex filtering -# TODO: Bad query param - def test_200_on_to_many(comment, post, client): payload = {'data': [{'type': 'blog-comments', 'id': str(comment.id)}]} - response = client.post( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(200) + response = client.post('/api/blog-posts/{}/relationships/comments/'.format( + post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 200) assert str(comment.id) in [str(x['id']) for x in response.json_data['data']] def test_409_on_hash_instead_of_array_provided(comment, post, client): payload = {'data': {'type': 'blog-comments', 'id': str(comment.id)}} - client.post('/api/blog-posts/{}/relationships/comments/'.format(post.id), + client.post('/api/blog-posts/{}/relationships/comments/'.format( + post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 409, ValidationError) @@ -31,7 +26,8 @@ def test_409_on_hash_instead_of_array_provided(comment, post, client): def test_409_on_incompatible_model(user, post, client): payload = {'data': [{'type': 'users', 'id': str(user.id)}]} - client.post('/api/blog-posts/{}/relationships/comments/'.format(post.id), + client.post('/api/blog-posts/{}/relationships/comments/'.format( + post.id), data=json.dumps(payload), content_type='application/vnd.api+json').validate( 409, ValidationError) @@ -52,7 +48,8 @@ def test_404_on_resource_not_found(client): def test_404_on_relationship_not_found(post, client): - client.post('/api/blog-posts/{}/relationships/comment/'.format(post.id), + client.post('/api/blog-posts/{}/relationships/comment/'.format( + post.id), data='{}', content_type='application/vnd.api+json').validate( 404, RelationshipNotFoundError) diff --git a/sqlalchemy_jsonapi/tests/test_resource_delete.py b/sqlalchemy_jsonapi/tests/test_resource_delete.py index a291c5e..a48d429 100644 --- a/sqlalchemy_jsonapi/tests/test_resource_delete.py +++ b/sqlalchemy_jsonapi/tests/test_resource_delete.py @@ -4,9 +4,6 @@ PermissionDeniedError, ResourceNotFoundError, ResourceTypeNotFoundError) -# TODO: Bad query param - - def test_200_on_success(comment, client): client.delete('/api/blog-comments/{}/'.format(comment.id)).validate(204) client.get('/api/blog-comments/{}/'.format(comment.id)).validate( diff --git a/sqlalchemy_jsonapi/tests/test_resource_get.py b/sqlalchemy_jsonapi/tests/test_resource_get.py index 30a2e49..4f81414 100644 --- a/sqlalchemy_jsonapi/tests/test_resource_get.py +++ b/sqlalchemy_jsonapi/tests/test_resource_get.py @@ -2,11 +2,6 @@ from uuid import uuid4 -# TODO: Sparse Fieldsets -# TODO: Related Includes -# TODO: Bad query param - - def test_200_without_querystring(post, client): response = client.get('/api/blog-posts/{}/'.format(post.id)).validate(200) assert response.json_data['data']['type'] == 'blog-posts' @@ -47,17 +42,16 @@ def test_200_with_multiple_includes(post, client): def test_200_with_single_field(post, client): - response = client.get( - '/api/blog-posts/{}/?fields[blog-posts]=title'.format( - post.id)).validate(200) + response = client.get('/api/blog-posts/{}/?fields[blog-posts]=title'.format( + post.id)).validate(200) assert {'title'} == set(response.json_data['data']['attributes'].keys()) assert len(response.json_data['data']['relationships']) == 0 def test_200_with_multiple_fields(post, client): - response = client.get( - '/api/blog-posts/{}/?fields[blog-posts]=title,content'.format( - post.id)).validate(200) + response = client.get('/api/blog-posts/{}/?fields[blog-posts]=title,content'.format( + post.id)).validate( + 200) assert {'title', 'content' } == set(response.json_data['data']['attributes'].keys()) assert len(response.json_data['data']['relationships']) == 0 @@ -66,7 +60,8 @@ def test_200_with_multiple_fields(post, client): def test_200_with_single_field_across_a_relationship(post, client): response = client.get( '/api/blog-posts/{}/?fields[blog-posts]=title,content&fields[blog-comments]=author&include=comments'.format( - post.id)).validate(200) + post.id)).validate( + 200) assert {'title', 'content' } == set(response.json_data['data']['attributes'].keys()) assert len(response.json_data['data']['relationships']) == 0 diff --git a/sqlalchemy_jsonapi/tests/test_resource_patch.py b/sqlalchemy_jsonapi/tests/test_resource_patch.py index 5ee3b40..e5cdac1 100644 --- a/sqlalchemy_jsonapi/tests/test_resource_patch.py +++ b/sqlalchemy_jsonapi/tests/test_resource_patch.py @@ -7,11 +7,6 @@ MissingTypeError) -# TODO: Sparse Fieldsets -# TODO: Related Includes -# TODO: Bad query param - - def test_200(client, post, user): payload = { 'data': { @@ -30,14 +25,14 @@ def test_200(client, post, user): } } } - response = client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(200) + response = client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 200) assert response.json_data['data']['id'] == str(post.id) assert response.json_data['data']['type'] == 'blog-posts' - assert response.json_data['data']['attributes'][ - 'title'] == 'I just lost the game' + assert response.json_data['data']['attributes']['title' + ] == 'I just lost the game' def test_400_missing_type(post, client): @@ -50,7 +45,8 @@ def test_400_missing_type(post, client): def test_404_resource_not_found(client): client.patch('/api/blog-posts/{}/'.format(uuid4()), content_type='application/vnd.api+json', - data='{}').validate(404, ResourceNotFoundError) + data='{}').validate( + 404, ResourceNotFoundError) def test_404_related_resource_not_found(client, post): diff --git a/sqlalchemy_jsonapi/tests/test_serializer.py b/sqlalchemy_jsonapi/tests/test_serializer.py index 38fe038..07060c9 100644 --- a/sqlalchemy_jsonapi/tests/test_serializer.py +++ b/sqlalchemy_jsonapi/tests/test_serializer.py @@ -7,6 +7,5 @@ def test_include_different_types_same_id(session, comment): comment.post.id = comment.author.id = comment.post_id = comment.author_id = new_id session.commit() - r = api.serializer.get_resource(session, {'include': 'post,author'}, - 'blog-comments', comment.id) + r = api.serializer.get_resource(session, {'include': 'post,author'}, 'blog-comments', comment.id) assert len(r.data['included']) == 2 diff --git a/sqlalchemy_jsonapi/version.py b/sqlalchemy_jsonapi/version.py deleted file mode 100644 index a0f6658..0000000 --- a/sqlalchemy_jsonapi/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '5.0.0'