PKùº{I¿Ýéÿµµ#tests/test_sqlalchemy_data_layer.py# -*- coding: utf-8 -*- from six.moves.urllib.parse import urlencode import pytest import json import datetime from sqlalchemy import create_engine, Column, Integer, DateTime, String from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base from flask import Blueprint from marshmallow_jsonapi.flask import Schema from marshmallow_jsonapi import fields from flask_rest_jsonapi import ResourceList, ResourceDetail, SqlalchemyDataLayer @pytest.fixture(scope="session") def base(): yield declarative_base() @pytest.fixture(scope="session") def item_cls(base): class Item(base): __tablename__ = 'item' id = Column(Integer, primary_key=True) title = Column(String) content = Column(String) created = Column(DateTime, default=datetime.datetime.utcnow) yield Item @pytest.fixture(scope="session") def engine(item_cls): engine = create_engine("sqlite:///:memory:") item_cls.metadata.create_all(engine) return engine @pytest.fixture(scope="session") def session(engine): Session = sessionmaker(bind=engine) return Session() @pytest.fixture(scope="session") def base_query(item_cls): def get_base_query(self, **view_kwargs): return self.session.query(item_cls) yield get_base_query @pytest.fixture(scope="session") def dummy_decorator(): def deco(f): def wrapper_f(*args, **kwargs): return f(*args, **kwargs) return wrapper_f yield deco @pytest.fixture(scope="session") def item_schema(): class ItemSchema(Schema): class Meta: type_ = 'item' self_view = 'rest_api.item_detail' self_view_kwargs = {'item_id': ''} self_view_many = 'rest_api.item_list' id = fields.Str(dump_only=True) title = fields.Str() content = fields.Str() created = fields.DateTime() yield ItemSchema @pytest.fixture(scope="session") def item_list_resource(session, item_cls, base_query, dummy_decorator, item_schema): class ItemList(ResourceList): class Meta: data_layer = {'cls': SqlalchemyDataLayer, 'kwargs': {'model': item_cls, 'session': session}, 'get_base_query': base_query} get_decorators = [dummy_decorator] post_decorators = [dummy_decorator] resource_type = 'item' schema = {'cls': item_schema} endpoint = {'alias': 'rest_api.item_list'} yield ItemList @pytest.fixture(scope="session") def item_detail_resource(session, item_cls, base_query, dummy_decorator, item_schema): class ItemDetail(ResourceDetail): class Meta: data_layer = {'cls': SqlalchemyDataLayer, 'kwargs': {'model': item_cls, 'session': session, 'id_field': 'id', 'url_param_name': 'item_id'}, 'get_base_query': base_query} get_decorators = [dummy_decorator] patch_decorators = [dummy_decorator] delete_decorators = [dummy_decorator] resource_type = 'item' schema = {'cls': item_schema} yield ItemDetail @pytest.fixture(scope="session") def item_list_resource_not_allowed(session, item_cls, base_query, dummy_decorator, item_schema): class ItemList(ResourceList): class Meta: data_layer = {'cls': SqlalchemyDataLayer, 'kwargs': {'model': item_cls, 'session': session}, 'get_base_query': base_query} get_decorators = [dummy_decorator] not_allowed_methods = ['POST'] resource_type = 'item' schema_cls = item_schema collection_endpoint = 'rest_api.item_list' yield ItemList @pytest.fixture(scope="session") def rest_api_blueprint(client): bp = Blueprint('rest_api', __name__) yield bp @pytest.fixture(scope="session") def register_routes(client, rest_api_blueprint, item_list_resource, item_detail_resource, item_list_resource_not_allowed): rest_api_blueprint.add_url_rule('/items', view_func=item_list_resource.as_view('item_list')) rest_api_blueprint.add_url_rule('/items/', view_func=item_detail_resource.as_view('item_detail')) rest_api_blueprint.add_url_rule('/items_not_allowed', view_func=item_list_resource_not_allowed.as_view('item_list_not_allowed')) client.application.register_blueprint(rest_api_blueprint) def test_get_list_resource(client, register_routes): querystring = urlencode({'page[number]': 3, 'page[size]': 1, 'fields[item]': 'title,content', 'sort': '-created,title', 'filter[item]': json.dumps([{'field': 'created', 'op': 'gt', 'value': '2016-11-10'}])}) response = client.get('/items' + '?' + querystring, content_type='application/vnd.api+json') assert response.status_code == 200 def test_post_list_resource(client, register_routes): response = client.post('/items', data=json.dumps({"data": {"type": "item", "attributes": {"title": "test"}}}), content_type='application/vnd.api+json') assert response.status_code == 201 def test_get_detail_resource(client, register_routes): response = client.get('/items/1', content_type='application/vnd.api+json') assert response.status_code == 200 def test_patch_patch_resource(client, register_routes): response = client.patch('/items/1', data=json.dumps({"data": {"type": "item", "id": 1, "attributes": {"title": "test2"}}}), content_type='application/vnd.api+json') assert response.status_code == 200 def test_delete_detail_resource(client, register_routes): response = client.delete('/items/1', content_type='application/vnd.api+json') assert response.status_code == 204 def test_post_list_resource_not_allowed(client, register_routes): response = client.post('/items_not_allowed', data=json.dumps({"data": {"type": "item", "attributes": {"title": "test"}}}), content_type='application/vnd.api+json') assert response.status_code == 405 def test_get_detail_resource_not_found(client, register_routes): response = client.get('/items/2', content_type='application/vnd.api+json') assert response.status_code == 404 def test_patch_patch_resource_error(client, register_routes): response = client.patch('/items/1', data=json.dumps({"data": {"type": "item", "attributes": {"title": "test2"}}}), content_type='application/vnd.api+json') assert response.status_code == 422 def test_wrong_content_type(client, register_routes): response = client.delete('/items/1') assert response.status_code == 415 def test_response_content_type(client, register_routes): response = client.delete('/items/1', content_type='application/vnd.api+json') assert response.headers['Content-Type'] == 'application/vnd.api+json' PK¤hrIêòQ  tests/conftest.py# -*- coding: utf-8 -*- import pytest from flask import Flask @pytest.fixture(scope="session") def app(): app = Flask(__name__) return app @pytest.yield_fixture(scope="session") def client(app): with app.test_client() as client: yield client PK-‡rItests/__init__.pyPK·zPJ@¨]€!flask_rest_jsonapi/querystring.py# -*- coding: utf-8 -*- import json from copy import copy class QueryStringManager(object): """Querystring parser according to jsonapi reference """ MANAGED_KEYS = ( 'filter', 'page', 'fields', 'sort', 'include' ) def __init__(self, querystring): """Initialization instance :param dict querystring: query string dict from request.args """ if not isinstance(querystring, dict): raise ValueError('QueryStringManager require a dict-like object query_string parameter') self.qs = querystring def _get_key_values(self, index, multiple_values=True): """Return a dict containing key / values items for a given key, used for items like filters, page, etc. :param str index: index to use for filtering :param bool multiple_values: indicate if each key can have more than one value :return dict: a dict of key / values items """ results = {} for key, value in self.qs.items(): if not key.startswith(index): continue try: key_start = key.index('[') + 1 key_end = key.index(']') except ValueError: continue item_key = key[key_start:key_end] if multiple_values: item_value = value.split(',') else: item_value = value results.update({item_key: item_value}) return results @property def querystring(self): """Return original querystring but containing only managed keys :return dict: dict of managed querystring parameter """ ret = {} for key, value in self.qs.items(): if key.startswith(self.MANAGED_KEYS): ret[key] = value return ret @property def filters(self): """Return filters from query string. :return list: a list of filter information Filters will be parsed based on jsonapi recommendations_ .. _recommendations: http://jsonapi.org/recommendations/#filtering Return value will be a dict containing all fields by resource, for example:: { "user": [{'field': 'username', 'op': 'eq', 'value': 'test'}], } """ filters = self._get_key_values('filter', multiple_values=False) for key, value in copy(filters).items(): try: filters[key] = json.loads(value) except ValueError: del filters[key] return filters @property def pagination(self): """Return all page parameters as a dict. :return dict: a dict of pagination information To allow multiples strategies, all parameters starting with `page` will be included. e.g:: { "number": '25', "size": '150', } Example with number strategy:: >>> query_string = {'page[number]': '25', 'page[size]': '10'} >>> parsed_query.pagination {'number': '25', 'size': '10'} """ # check values type result = self._get_key_values('page', multiple_values=False) for key, value in result.items(): try: int(value) except ValueError: raise Exception("Invalid value for %s attribut of page in querystring" % key) return result @property def fields(self): """Return fields wanted by client. :return dict: a dict of sparse fieldsets information Return value will be a dict containing all fields by resource, for example:: { "user": ['name', 'email'], } """ return self._get_key_values('fields') @property def sorting(self): """Return fields to sort by including sort name for SQLAlchemy and row sort parameter for other ORMs :return list: a list of sorting information Example of return value:: [ {'field': 'created_at', 'order': 'desc', 'raw': '-created_at'}, ] """ sort_fields = self.qs.get('sort') if not sort_fields: return [] sorting_results = [] for sort_field in sort_fields.split(','): field = sort_field.replace('-', '') order = 'desc' if sort_field.startswith('-') else 'asc' sorting_results.append({'field': field, 'order': order, 'raw': sort_field}) return sorting_results @property def include(self): """Return fields to include :return list: a list of include information """ include_param = self.qs.get('include') return include_param.split(',') if include_param else [] PK·zPJ[ðßñCCflask_rest_jsonapi/errors.py# -*- coding: utf-8 -*- def jsonapi_errors(jsonapi_errors): """Construct api error according to jsonapi 1.0 :param iterable jsonapi_errors: an iterable of jsonapi error :return dict: a dict of errors according to jsonapi 1.0 """ return {'errors': [jsonapi_error for jsonapi_error in jsonapi_errors]} PK·zPJŒHE< flask_rest_jsonapi/exceptions.py# -*- coding: utf-8 -*- class JsonApiException(Exception): title = 'Unknow error' status = 500 def __init__(self, source, detail, title=None, status=None): """Initialize a jsonapi exception :param str source: the source of the error :param str detail: the detail of the error """ self.source = source self.detail = detail if title is not None: self.title = title if status is not None: self.status = status def to_dict(self): return {'status': self.status, 'source': {'pointer': self.source}, 'title': self.title, 'detail': self.detail} class BadRequest(JsonApiException): title = "Bad request" status = 400 class InvalidField(BadRequest): title = "Invalid fields querystring parameter." class InvalidInclude(BadRequest): title = "Invalid include querystring parameter." class ObjectNotFound(JsonApiException): title = "Object not found" status = 404 class RelatedObjectNotFound(ObjectNotFound): title = "Related object not found" class RelationNotFound(JsonApiException): title = "Relation not found" class InvalidType(JsonApiException): title = "Invalid type" status = 409 PK·zPJ ôlC  flask_rest_jsonapi/api.py# -*- coding: utf-8 -*- from flask import Blueprint class Api(object): def __init__(self, app=None): self.app = None self.blueprint = None if app is not None: if isinstance(app, Blueprint): self.blueprint = app else: self.app = app self.resources = [] def init_app(self, app): """Update flask application with our api :param Application app: a flask application """ if self.blueprint is not None: app.register_blueprint(self.blueprint) else: self.app = app for resource in self.resources: self.route(**resource) def route(self, resource, endpoint, *urls, **kwargs): """Create an api endpoint. :param Resource resource: a resource class inherited from flask_rest_jsonapi.resource.Resource :param str endpoint: the endpoint name :param list urls: the urls of the endpoint :param dict kwargs: additional options of the route """ resource.endpoint = endpoint view_func = resource.as_view(endpoint) options = kwargs.get('url_rule_options') or dict() if self.app is not None: for url in urls: self.app.add_url_rule(url, view_func=view_func, **options) elif self.blueprint is not None: resource.endpoint = '.'.join([self.blueprint.name, resource.endpoint]) for url in urls: self.blueprint.add_url_rule(url, view_func=view_func, **options) else: self.resources.append({'resource': resource, 'endpoint': endpoint, 'urls': urls, 'options': options}) PK·zPJ]ÄE{9 9 flask_rest_jsonapi/schema.py# -*- coding: utf-8 -*- from marshmallow import class_registry from marshmallow.base import SchemaABC from marshmallow_jsonapi.fields import Relationship as MarshmallowJsonapiRelationship from flask_rest_jsonapi.exceptions import InvalidField, InvalidInclude def compute_schema(schema_cls, default_kwargs, qs, include): """Compute a schema around compound documents and sparse fieldsets :param Schema schema_cls: the schema class :param dict default_kwargs: the schema default kwargs :param QueryStringManager qs: qs :param list include: the relation field to include data from :return Schema schema: the schema computed """ # manage include_data parameter of the schema schema_kwargs = default_kwargs schema_kwargs['include_data'] = tuple() if include: for include_path in include: field = include_path.split('.')[0] if field not in schema_cls._declared_fields: raise InvalidInclude(field, "%s has no attribut %s" % (schema_cls.__name__, field)) elif not issubclass(schema_cls._declared_fields[field].__class__, MarshmallowJsonapiRelationship): raise InvalidInclude(field, "%s is not a relationship attribut of %s" % (field, schema_cls.__name__)) schema_kwargs['include_data'] += (field, ) # make sure id field is in only parameter unless marshamllow will raise an Exception if schema_kwargs.get('only') is not None and 'id' not in schema_kwargs['only']: schema_kwargs['only'] += ('id',) # create base schema instance schema = schema_cls(**schema_kwargs) # manage sparse fieldsets if schema.opts.type_ in qs.fields: # check that sparse fieldsets exists in the schema for field in qs.fields[schema.opts.type_]: if field not in schema.declared_fields: raise InvalidField(field, "%s has no attribut %s" % (schema.__class__.__name__, field)) tmp_only = set(schema.declared_fields.keys()) & set(qs.fields[schema.opts.type_]) if schema.only: tmp_only &= set(schema.only) schema.only = tuple(tmp_only) # make sure again that id field is in only parameter unless marshamllow will raise an Exception if schema.only is not None and 'id' not in schema.only: schema.only += ('id',) # manage compound documents if include: for include_path in include: field = include_path.split('.')[0] relation_field = schema.declared_fields[field] related_schema_cls = schema.declared_fields[field].__dict__['_Relationship__schema'] related_schema_kwargs = {} if isinstance(related_schema_cls, SchemaABC): related_schema_kwargs['many'] = related_schema_cls.many related_schema_cls = related_schema_cls.__class__ if isinstance(related_schema_cls, str): related_schema_cls = class_registry.get_class(related_schema_cls) if '.' in include_path: related_include = ['.'.join(include_path.split('.')[1:])] else: related_include = None related_schema = compute_schema(related_schema_cls, related_schema_kwargs, qs, related_include) relation_field.__dict__['_Relationship__schema'] = related_schema return schema PK·zPJ ý‹ —— flask_rest_jsonapi/pagination.py# -*- coding: utf-8 -*- from six.moves.urllib.parse import urlencode from math import ceil from copy import copy from flask_rest_jsonapi.constants import DEFAULT_PAGE_SIZE def add_pagination_links(data, object_count, querystring, base_url): """Add pagination links to result :param dict data: the result of the view :param int object_count: number of objects in result :param QueryStringManager querystring: the managed querystring fields and values :param str base_url: the base url for pagination """ links = {} all_qs_args = copy(querystring.querystring) links['self'] = links['first'] = links['last'] = base_url # compute self link if all_qs_args: links['self'] += '?' + urlencode(all_qs_args) try: del all_qs_args['page[number]'] except KeyError: pass # compute first link if all_qs_args: links['first'] += '?' + urlencode(all_qs_args) # compute last link page_size = int(querystring.pagination.get('size', 0)) or DEFAULT_PAGE_SIZE last_page = int(ceil(object_count / page_size)) if last_page > 0: all_qs_args.update({'page[number]': last_page}) links['last'] += '?' + urlencode(all_qs_args) # compute previous and next link current_page = int(querystring.pagination.get('number', 0)) or 1 if current_page > 1: all_qs_args.update({'page[number]': current_page - 1}) links['prev'] = '?'.join((base_url, urlencode(all_qs_args))) if current_page < last_page: all_qs_args.update({'page[number]': current_page + 1}) links['next'] = '?'.join((base_url, urlencode(all_qs_args))) data['links'] = links PK¤hrIöc¿“YYflask_rest_jsonapi/constants.py# -*- coding: utf-8 -*- # default number of items for pagination DEFAULT_PAGE_SIZE = 20 PKtQJ,j}â â flask_rest_jsonapi/decorators.py# -*- coding: utf-8 -*- from flask import abort, request def not_allowed_method(f): """A decorator to disallow method access :param callable f: the function to decorate :return callable: the wrapped function """ def wrapped_f(*args, **kwargs): abort(405) return wrapped_f def check_headers(f): """Check headers according to jsonapi reference :param callable f: the function to decorate :return callable: the wrapped function """ def wrapped_f(*args, **kwargs): if request.headers['Content-Type'] != 'application/vnd.api+json': abort(415) if request.headers.get('Accept') and request.headers['Accept'] != 'application/vnd.api+json': abort(406) return f(*args, **kwargs) return wrapped_f def add_headers(f): """Add headers according to jsonapi reference :param callable f: the function to decorate :return callable: the wrapped function """ def wrapped_f(*args, **kwargs): response = f(*args, **kwargs) response.headers['Content-Type'] = 'application/vnd.api+json' return response return wrapped_f def check_method_requirements(f): """Check methods requirements :param callable f: the function to decorate :return callable: the wrapped function """ def wrapped_f(self, *args, **kwargs): cls = type(self) cls_bases = [cls_.__name__ for cls_ in cls.__bases__] method_name = f.__name__ error_message = "You must provide %(error_field)s in %(cls)s to get access to the default %(method)s method" error_data = {'cls': cls.__name__, 'method': method_name} if not hasattr(self, 'data_layer'): error_data.update({'error_field': 'a data layer class'}) raise Exception(error_message % error_data) if not hasattr(self, 'schema'): error_data.update({'error_field': 'a schema class'}) raise Exception(error_message % error_data) if 'ResourceRelationship' in cls_bases: if not hasattr(self, 'related_type_'): error_data.update({'error_field': 'related_type_'}) raise Exception(error_message % error_data) if method_name == 'get': if not hasattr(self, 'related_endpoint'): error_data.update({'error_field': 'related_endpoint'}) raise Exception(error_message % error_data) return f(self, *args, **kwargs) return wrapped_f PK·zPJ³ðYïïflask_rest_jsonapi/__init__.py# -*- coding: utf-8 -*- from flask_rest_jsonapi.api import Api from flask_rest_jsonapi.resource import ResourceList, ResourceDetail, Relationship from flask_rest_jsonapi.data_layers.alchemy import SqlalchemyDataLayer from flask_rest_jsonapi.querystring import QueryStringManager from flask_rest_jsonapi.errors import jsonapi_errors __all__ = [ 'Api', 'ResourceList', 'ResourceDetail', 'Relationship', 'SqlalchemyDataLayer', 'QueryStringManager', 'jsonapi_errors' ] PKèzQJw!¦E¦Eflask_rest_jsonapi/resource.py# -*- coding: utf-8 -*- import inspect from six import with_metaclass import json from copy import copy from werkzeug.wrappers import Response from flask import request, url_for, make_response from flask.views import MethodViewType, MethodView from marshmallow_jsonapi.exceptions import IncorrectTypeError from marshmallow import ValidationError from flask_rest_jsonapi.errors import jsonapi_errors from flask_rest_jsonapi.querystring import QueryStringManager as QSManager from flask_rest_jsonapi.pagination import add_pagination_links from flask_rest_jsonapi.exceptions import ObjectNotFound, RelationNotFound, InvalidField, InvalidInclude, InvalidType, \ BadRequest, JsonApiException from flask_rest_jsonapi.decorators import not_allowed_method, check_headers, check_method_requirements, add_headers from flask_rest_jsonapi.schema import compute_schema from flask_rest_jsonapi.data_layers.base import BaseDataLayer from flask_rest_jsonapi.data_layers.alchemy import SqlalchemyDataLayer class ResourceMeta(MethodViewType): def __init__(cls, name, bases, nmspc): super(ResourceMeta, cls).__init__(name, bases, nmspc) meta = nmspc.get('Meta') # compute data_layer data_layer = None alternative_data_layer_cls = getattr(meta, 'data_layer', None) if alternative_data_layer_cls is not None and BaseDataLayer not in inspect.getmro(alternative_data_layer_cls): raise Exception("You must provide a data layer class inherited from BaseDataLayer in %s resource" % name) if nmspc.get('data_layer_kwargs') is not None: if not isinstance(nmspc['data_layer_kwargs'], dict): raise Exception("You must provide data_layer_kwargs as dictionary in %s resource" % name) else: data_layer_cls = getattr(meta, 'data_layer', SqlalchemyDataLayer) data_layer_kwargs = nmspc.get('data_layer_kwargs', dict()) data_layer = type('%sDataLayer' % name, (data_layer_cls, ), dict())(**data_layer_kwargs) data_layer.configure(meta) if data_layer is not None: data_layer.resource = cls cls.data_layer = data_layer # disable access to methods according to meta options if meta is not None: not_allowed_methods = getattr(meta, 'not_allowed_methods', []) for method in not_allowed_methods: if hasattr(cls, method.lower()): setattr(cls, method.lower(), not_allowed_method(getattr(cls, method.lower()))) # set meta information as opts of the resource class cls.opts = meta class ResourceListMeta(ResourceMeta): def __init__(cls, name, bases, nmspc): super(ResourceListMeta, cls).__init__(name, bases, nmspc) meta = nmspc.get('Meta') if meta is not None: get_decorators = getattr(meta, 'get_decorators', []) post_decorators = getattr(meta, 'post_decorators', []) for get_decorator in get_decorators: cls.get = get_decorator(cls.get) for post_decorator in post_decorators: cls.post = post_decorator(cls.post) class ResourceDetailMeta(ResourceMeta): def __init__(cls, name, bases, nmspc): super(ResourceDetailMeta, cls).__init__(name, bases, nmspc) meta = nmspc.get('Meta') if meta is not None: get_decorators = getattr(meta, 'get_decorators', []) patch_decorators = getattr(meta, 'patch_decorators', []) delete_decorators = getattr(meta, 'delete_decorators', []) for get_decorator in get_decorators: cls.get = get_decorator(cls.get) for patch_decorator in patch_decorators: cls.patch = patch_decorator(cls.patch) for delete_decorator in delete_decorators: cls.delete = delete_decorator(cls.delete) class ResourceRelationshipMeta(ResourceMeta): def __init__(cls, name, bases, nmspc): super(ResourceRelationshipMeta, cls).__init__(name, bases, nmspc) meta = nmspc.get('Meta') if meta is not None: get_decorators = getattr(meta, 'get_decorators', []) post_decorators = getattr(meta, 'post_decorators', []) patch_decorators = getattr(meta, 'patch_decorators', []) delete_decorators = getattr(meta, 'delete_decorators', []) for get_decorator in get_decorators: cls.get = get_decorator(cls.get) for post_decorator in post_decorators: cls.post = post_decorator(cls.post) for patch_decorator in patch_decorators: cls.patch = patch_decorator(cls.patch) for delete_decorator in delete_decorators: cls.delete = delete_decorator(cls.delete) class Resource(MethodView): decorators = (check_headers, add_headers) def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) if meth is None and request.method == 'HEAD': meth = getattr(self, 'get', None) assert meth is not None, 'Unimplemented method %r' % request.method try: resp = meth(*args, **kwargs) except JsonApiException as e: return make_response(json.dumps(jsonapi_errors([e.to_dict()])), e.status, dict()) except Exception as e: exc = JsonApiException('', str(e)) return make_response(json.dumps(jsonapi_errors([exc.to_dict()])), exc.status, dict()) if isinstance(resp, Response): return resp if not isinstance(resp, tuple): return make_response(json.dumps(resp)) try: data, status_code, headers = resp except ValueError: pass try: data, status_code = resp headers = {} except ValueError: pass return make_response(json.dumps(data), status_code, headers) class ResourceList(with_metaclass(ResourceListMeta, Resource)): @check_method_requirements def get(self, *args, **kwargs): """Retrieve a collection of objects """ qs = QSManager(request.args) object_count, objects = self.data_layer.get_collection(qs, **kwargs) schema_kwargs = getattr(self.opts, 'schema_get_kwargs', dict()) schema_kwargs.update({'many': True}) try: schema = compute_schema(self.schema, schema_kwargs, qs, qs.include) except (InvalidField, InvalidInclude) as e: return jsonapi_errors([e.to_dict()]), e.status result = schema.dump(objects) endpoint_kwargs = request.view_args if getattr(self.opts, 'endpoint_kwargs', None) is True else dict() add_pagination_links(result.data, object_count, qs, url_for(self.endpoint, **endpoint_kwargs)) return result.data @check_method_requirements def post(self, *args, **kwargs): """Create an object """ json_data = request.get_json() qs = QSManager(request.args) try: schema = compute_schema(self.schema, getattr(self.opts, 'schema_post_kwargs', dict()), qs, qs.include) except (InvalidField, InvalidInclude) as e: return jsonapi_errors([e.to_dict()]), e.status try: data, errors = schema.load(json_data) except IncorrectTypeError as e: errors = e.messages for error in errors['errors']: error['status'] = '409' error['title'] = "Incorrect type" return errors, 409 except ValidationError as e: errors = e.messages for message in errors['errors']: message['status'] = '422' message['title'] = "Validation error" return errors, 422 if errors: for error in errors['errors']: error['status'] = "422" error['title'] = "Validation error" return errors, 422 try: obj = self.data_layer.create_object(data, **kwargs) except JsonApiException as e: return jsonapi_errors([e.to_dict()]), e.status return schema.dump(obj).data, 201 class ResourceDetail(with_metaclass(ResourceDetailMeta, Resource)): @check_method_requirements def get(self, *args, **kwargs): """Get object details """ try: obj = self.data_layer.get_object(**kwargs) except ObjectNotFound as e: return jsonapi_errors([e.to_dict()]), e.status qs = QSManager(request.args) try: schema = compute_schema(self.schema, getattr(self.opts, 'schema_get_kwargs', dict()), qs, qs.include) except (InvalidField, InvalidInclude) as e: return jsonapi_errors([e.to_dict()]), e.status result = schema.dump(obj) return result.data @check_method_requirements def patch(self, *args, **kwargs): """Update an object """ json_data = request.get_json() qs = QSManager(request.args) schema_kwargs = getattr(self.opts, 'schema_patch_kwargs', dict()) schema_kwargs.update({'partial': True}) try: schema = compute_schema(self.schema, schema_kwargs, qs, qs.include) except (InvalidField, InvalidInclude) as e: return jsonapi_errors([e.to_dict()]), e.status try: data, errors = schema.load(json_data) except IncorrectTypeError as e: errors = e.messages for error in errors['errors']: error['status'] = '409' error['title'] = "Incorrect type" return errors, 409 except ValidationError as e: errors = e.messages for message in errors['errors']: message['status'] = '422' message['title'] = "Validation error" return errors, 422 if errors: for error in errors['errors']: error['status'] = "422" error['title'] = "Validation error" return errors, 422 try: if 'id' not in json_data['data']: raise BadRequest('/data/id', 'Missing id in "data" node') if json_data['data']['id'] != kwargs[self.data_layer.url_field]: raise BadRequest('/data/id', 'Value of id does not match the resource identifier in url') except BadRequest as e: return jsonapi_errors([e.to_dict()]), e.status try: obj = self.data_layer.get_object(**kwargs) except ObjectNotFound as e: return jsonapi_errors([e.to_dict()]), e.status try: self.data_layer.update_object(obj, data, **kwargs) except JsonApiException as e: return jsonapi_errors([e.to_dict()]), e.status result = schema.dump(obj) return result.data @check_method_requirements def delete(self, *args, **kwargs): """Delete an object """ try: obj = self.data_layer.get_object(**kwargs) except ObjectNotFound as e: return jsonapi_errors([e.to_dict()]), e.status try: self.data_layer.delete_object(obj, **kwargs) except JsonApiException as e: return jsonapi_errors([e.to_dict()]), e.status return '', 204 class Relationship(with_metaclass(ResourceRelationshipMeta, Resource)): @check_method_requirements def get(self, *args, **kwargs): """Get a relationship details """ related_id_field = getattr(self.opts, 'related_id_field', 'id') try: obj, data = self.data_layer.get_relation(self.related_type_, related_id_field, **kwargs) except (RelationNotFound, ObjectNotFound) as e: return jsonapi_errors([e.to_dict()]), e.status if hasattr(self.opts, 'related_endpoint_kwargs'): related_endpoint_kwargs = dict() for key, value in copy(self.opts.related_endpoint_kwargs).items(): tmp_obj = obj for field in value.split('.'): tmp_obj = getattr(tmp_obj, field) related_endpoint_kwargs[key] = tmp_obj else: related_endpoint_kwargs = kwargs result = {'links': {'self': url_for(self.endpoint, **kwargs), 'related': url_for(self.related_endpoint, **related_endpoint_kwargs)}, 'data': data} qs = QSManager(request.args) if qs.include: try: schema = compute_schema(self.schema, dict(), qs, qs.include) except (InvalidField, InvalidInclude) as e: return jsonapi_errors([e.to_dict()]), e.status serialized_obj = schema.dump(obj) result['included'] = serialized_obj.data['included'] return result @check_method_requirements def post(self, *args, **kwargs): """Add / create relationship(s) """ json_data = request.get_json() try: if 'data' not in json_data: raise BadRequest('/data', 'You must provide data with a "data" route node') if not isinstance(json_data.get('data'), list): raise BadRequest('/data', 'You must provide data as list') for obj in json_data['data']: if 'type' not in obj: raise BadRequest('/data/type', 'Missing type in "data" node') if 'id' not in obj: raise BadRequest('/data/id', 'Missing id in "data" node') if obj['type'] != self.related_type_: raise InvalidType('/data/type', 'The type provided does not match the resource type') except (BadRequest, InvalidType) as e: return jsonapi_errors([e.to_dict()]), e.status related_id_field = getattr(self.opts, 'related_id_field', 'id') try: self.data_layer.create_relation(json_data, related_id_field, **kwargs) except (RelationNotFound, ObjectNotFound) as e: return jsonapi_errors([e.to_dict()]), e.status return '' @check_method_requirements def patch(self, *args, **kwargs): """Update a relationship """ json_data = request.get_json() try: if 'data' not in json_data: raise BadRequest('/data', 'You must provide data with a "data" route node') if isinstance(json_data['data'], dict): if 'type' not in json_data['data']: raise BadRequest('/data/type', 'Missing type in "data" node') if 'id' not in json_data['data']: raise BadRequest('/data/id', 'Missing id in "data" node') if json_data['data']['type'] != self.related_type_: raise InvalidType('/data/type', 'The type field does not match the resource type') if isinstance(json_data['data'], list): for obj in json_data['data']: if 'type' not in obj: raise BadRequest('/data/type', 'Missing type in "data" node') if 'id' not in obj: raise BadRequest('/data/id', 'Missing id in "data" node') if obj['type'] != self.related_type_: raise InvalidType('/data/type', 'The type provided does not match the resource type') except (BadRequest, InvalidType) as e: return jsonapi_errors([e.to_dict()]), e.status related_id_field = getattr(self.opts, 'related_id_field', 'id') try: self.data_layer.update_relation(json_data, related_id_field, **kwargs) except (RelationNotFound, ObjectNotFound) as e: return jsonapi_errors([e.to_dict()]), e.status return '' @check_method_requirements def delete(self, *args, **kwargs): """Delete relationship(s) """ json_data = request.get_json() try: if 'data' not in json_data: raise BadRequest('/data', 'You must provide data with a "data" route node') if not isinstance(json_data.get('data'), list): raise BadRequest('/data', 'You must provide data as list') for obj in json_data['data']: if 'type' not in obj: raise BadRequest('/data/type', 'Missing type in "data" node') if 'id' not in obj: raise BadRequest('/data/id', 'Missing id in "data" node') if obj['type'] != self.related_type_: raise InvalidType('/data/type', 'The type provided does not match the resource type') except (BadRequest, InvalidType) as e: return jsonapi_errors([e.to_dict()]), e.status related_id_field = getattr(self.opts, 'related_id_field', 'id') try: self.data_layer.delete_relation(json_data, related_id_field, **kwargs) except RelationNotFound as e: return jsonapi_errors([e.to_dict()]), e.status return '' PKðlRJêR ¯Ë.Ë.)flask_rest_jsonapi/data_layers/alchemy.py# -*- coding: utf-8 -*- from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy.sql.expression import desc, asc, text from flask_rest_jsonapi.constants import DEFAULT_PAGE_SIZE from flask_rest_jsonapi.data_layers.base import BaseDataLayer from flask_rest_jsonapi.exceptions import ObjectNotFound, RelationNotFound, RelatedObjectNotFound class SqlalchemyDataLayer(BaseDataLayer): def __init__(self, *args, **kwargs): super(SqlalchemyDataLayer, self).__init__(*args, **kwargs) if not hasattr(self, 'session'): raise Exception("You must provide a session in data_layer_kwargs to use sqlalchemy data layer in %s" % self.resource.__name__) if not hasattr(self, 'model'): raise Exception("You must provide a model in data_layer_kwargs to use sqlalchemy data layer in %s" % self.resource.__name__) def create_object(self, data, **view_kwargs): """Create an object through sqlalchemy :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view :return DeclarativeMeta: an object from sqlalchemy """ self.before_create_object(data, **view_kwargs) obj = self.model(**data) self.session.add(obj) self.session.commit() return obj def get_object(self, **view_kwargs): """Retrieve an object through sqlalchemy :params dict view_kwargs: kwargs from the resource view :return DeclarativeMeta: an object from sqlalchemy """ id_field = getattr(self, 'id_field', 'id') try: filter_field = getattr(self.model, id_field) except Exception: raise Exception("Unable to find attribut: %s on model: %s" % (id_field, self.model.__name__)) url_field = getattr(self, 'url_field', 'id') filter_value = view_kwargs[url_field] try: obj = self.session.query(self.model).filter(filter_field == filter_value).one() except NoResultFound: raise ObjectNotFound('.'.join([self.model.__name__, id_field]), "Could not find %s.%s=%s object" % (self.model.__name__, id_field, filter_value)) return obj def get_collection(self, qs, **view_kwargs): """Retrieve a collection of objects through sqlalchemy :param QueryStringManager qs: a querystring manager to retrieve information from url :param dict view_kwargs: kwargs from the resource view :return tuple: the number of object and the list of objects """ query = self.query(**view_kwargs) if qs.filters: query = self.filter_query(query, qs.filters, self.model) if qs.sorting: query = self.sort_query(query, qs.sorting) object_count = query.count() query = self.paginate_query(query, qs.pagination) return object_count, query.all() def update_object(self, obj, data, **view_kwargs): """Update an object through sqlalchemy :param DeclarativeMeta obj: an object from sqlalchemy :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view """ self.before_update_object(obj, data, **view_kwargs) for field in data: if hasattr(obj, field): setattr(obj, field, data[field]) self.session.commit() def delete_object(self, obj, **view_kwargs): """Delete an object through sqlalchemy :param DeclarativeMeta item: an item from sqlalchemy :param dict view_kwargs: kwargs from the resource view """ self.before_delete_object(obj, **view_kwargs) self.session.delete(obj) self.session.commit() def create_relation(self, json_data, related_id_field, **view_kwargs): """Create a relation :param dict json_data: the request params :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ obj = self.get_object(**view_kwargs) if not hasattr(obj, self.relation_field): raise RelationNotFound related_model = getattr(obj.__class__, self.relation_field).property.mapper.class_ for obj_ in json_data['data']: related_object = self.get_related_object(related_model, related_id_field, obj_) getattr(obj, self.relation_field).append(related_object) self.session.commit() def get_relation(self, related_type_, related_id_field, **view_kwargs): """Get a relation :param str related_type_: the related resource type :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return tuple: the object and related object(s) """ obj = self.get_object(**view_kwargs) if not hasattr(obj, self.relation_field): raise RelationNotFound(self.relation_field, "%s as no attribut %s" % (self.model.__name__, self.relation_field)) related_objects = getattr(obj, self.relation_field) if related_objects is None: return obj, related_objects if isinstance(related_objects, InstrumentedList): return obj,\ [{'type': related_type_, 'id': getattr(obj_, related_id_field)} for obj_ in related_objects] else: return obj, {'type': related_type_, 'id': getattr(related_objects, related_id_field)} def update_relation(self, json_data, related_id_field, **view_kwargs): """Update a relation :param dict json_data: the request params :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ obj = self.get_object(**view_kwargs) if not hasattr(obj, self.relation_field): raise RelationNotFound related_model = getattr(obj.__class__, self.relation_field).property.mapper.class_ if not isinstance(json_data['data'], list): related_object = None if json_data['data'] is not None: related_object = self.get_related_object(related_model, related_id_field, json_data['data']) setattr(obj, self.relation_field, related_object) else: related_objects = [] for obj_ in json_data['data']: related_object = self.get_related_object(related_model, related_id_field, obj_) setattr(obj, self.relation_field, related_objects) self.session.commit() def delete_relation(self, json_data, related_id_field, **view_kwargs): """Delete a relation :param dict json_data: the request params :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ obj = self.get_object(**view_kwargs) if not hasattr(obj, self.relation_field): raise RelationNotFound related_model = getattr(obj.__class__, self.relation_field).property.mapper.class_ for obj_ in json_data['data']: related_object = self.get_related_object(related_model, related_id_field, obj_) getattr(obj, self.relation_field).remove(related_object) self.session.commit() def get_related_object(self, related_model, related_id_field, obj): """Get a related object :param Model related_model: an sqlalchemy model :param str related_id_field: the identifier field of the related model :param DeclarativeMeta obj: the sqlalchemy object to retrieve related objects from :return DeclarativeMeta: a related object """ try: related_object = self.session.query(related_model)\ .filter(getattr(related_model, related_id_field) == obj['id'])\ .one() except NoResultFound: raise RelatedObjectNotFound('%s.%s' % (related_model.__name__, related_id_field), "Could not find %s.%s=%s object" % (related_model.__name__, related_id_field, obj['id'])) return related_object def filter_query(self, query, filter_info, model): """Filter query according to jsonapi 1.0 :param Query query: sqlalchemy query to sort :param list filter_info: filter information :param DeclarativeMeta model: an sqlalchemy model :return Query: the sorted query """ for obj in filter_info[model.__name__.lower()]: try: column = getattr(model, obj['field']) except AttributeError: continue if obj['op'] == 'in': filt = column.in_(obj['value'].split(',')) else: try: attr = next(iter(filter(lambda e: hasattr(column, e % obj['op']), ['%s', '%s_', '__%s__']))) % obj['op'] except IndexError: continue if obj['value'] == 'null': obj['value'] = None filt = getattr(column, attr)(obj['value']) query = query.filter(filt) return query def sort_query(self, query, sort_info): """Sort query according to jsonapi 1.0 :param Query query: sqlalchemy query to sort :param list sort_info: sort information :return Query: the sorted query """ expressions = {'asc': asc, 'desc': desc} order_objects = [] for sort_opt in sort_info: field = text(sort_opt['field']) order = expressions.get(sort_opt['order']) order_objects.append(order(field)) return query.order_by(*order_objects) def paginate_query(self, query, paginate_info): """Paginate query according to jsonapi 1.0 :param Query query: sqlalchemy queryset :param dict paginate_info: pagination information :return Query: the paginated query """ if int(paginate_info.get('size', 1)) == 0: return query page_size = int(paginate_info.get('size', 0)) or DEFAULT_PAGE_SIZE query = query.limit(page_size) if paginate_info.get('number'): query = query.offset((int(paginate_info['number']) - 1) * page_size) return query def query(self, **view_kwargs): """Construct the base query to retrieve wanted data :param dict view_kwargs: kwargs from the resource view """ return self.session.query(self.model) def before_create_object(self, data, **view_kwargs): """Provide additional data before object creation :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view """ pass def before_update_object(self, obj, data, **view_kwargs): """Make checks or provide additional data before update object :param obj: an object from data layer :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view """ pass def before_delete_object(self, obj, **view_kwargs): """Make checks before delete object :param obj: an object from data layer :param dict view_kwargs: kwargs from the resource view """ pass PKXVAJA0 :TT'flask_rest_jsonapi/data_layers/mongo.py# -*- coding: utf-8 -*- from flask_rest_jsonapi.constants import DEFAULT_PAGE_SIZE from flask_rest_jsonapi.data_layers.base import BaseDataLayer from flask_rest_jsonapi.exceptions import ObjectNotFound from pymongo import ASCENDING, DESCENDING class MongoDataLayer(BaseDataLayer): def __init__(self, **kwargs): super(MongoDataLayer, self).__init__(**kwargs) if not hasattr(self, 'mongo') or self.mongo is None: raise Exception('You must provide a mongo connection') if not hasattr(self, 'collection') or self.collection is None: raise Exception('You must provide a collection to query') if not hasattr(self, 'model') or self.model is None: raise Exception('You must provide a proper model class !') def get_item(self, **view_kwargs): """Retrieve a single item from mongodb. :params dict view_kwargs: kwargs from the resource view :return dict: a mongo document """ query = self.get_single_item_query(**view_kwargs) result = self.get_collection().find_one(query) if result is None: raise ObjectNotFound(self.collection, view_kwargs.get(self.url_param_name)) return result def get_items(self, qs, **view_kwargs): query = self.get_base_query(**view_kwargs) if qs.filters: query = self.filter_query(query, qs.filters, self.model) query = self.get_collection().find(query) if qs.sorting: query = self.sort_query(query, qs.sorting) item_count = query.count() query = self.paginate_query(query, qs.pagination) return item_count, list(query) def create_and_save_item(self, data, **view_kwargs): """Create and save a mongo document. :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view :return object: A publimodels object """ self.before_create_instance(data, **view_kwargs) item = self.model(**data) self.get_collection().save(item) return item def update_and_save_item(self, item, data, **view_kwargs): """Update an instance of an item and store changes :param item: a doucment from mongodb :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view """ self.before_update_instance(item, data) for field in data: if hasattr(item, field): setattr(item, field, data[field]) id_query = self.get_single_item_query(**view_kwargs) self.get_collection().update(id_query, item) def get_collection(self): collection = getattr(self.mongo.db, self.collection, None) if collection is None: raise Exception( 'Collection %s does not exist' % self.collection ) return collection def get_single_item_query(self, **view_kwargs): return {self.id_field: view_kwargs.get(self.url_param_name)} def filter_query(self, query, filter_info, model): """Filter query according to jsonapi rfc :param dict: mongo query dict :param list filter_info: filter information :return dict: a new mongo query dict """ for item in filter_info.items()[model.__name__.lower()]: op = {'$%s' % item['op']: item['value']} query[item['field']] = op return query def paginate_query(self, query, paginate_info): """Paginate query according to jsonapi rfc :param pymongo.cursor.Cursor query: pymongo cursor :param dict paginate_info: pagination information :return pymongo.cursor.Cursor: the paginated query """ page_size = int(paginate_info.get('size', 0)) or DEFAULT_PAGE_SIZE if paginate_info.get('number'): offset = (int(paginate_info['number']) - 1) * page_size else: offset = 0 return query[offset:offset+page_size] def sort_query(self, query, sort_info): """Sort query according to jsonapi rfc :param pymongo.cursor.Cursor query: pymongo cursor :param list sort_info: sort information :return pymongo.cursor.Cursor: the paginated query """ expressions = {'asc': ASCENDING, 'desc': DESCENDING} for sort_opt in sort_info: field = sort_opt['field'] order = expressions.get(sort_opt['order']) query = query.sort(field, order) return query def before_create_instance(self, data, **view_kwargs): """Hook called at object creation. :param dict data: data validated by marshmallow :param dict view_kwargs: kwargs from the resource view """ pass def before_update_instance(self, item, data): """Hook called at object update. :param item: a document from sqlalchemy :param dict data: the data validated by marshmallow """ pass def get_base_query(self, **view_kwargs): """Construct the base query to retrieve wanted data. This would be created through metaclass. :param dict view_kwargs: Kwargs from the resource view """ raise NotImplemented @classmethod def configure(cls, data_layer): """Plug get_base_query to the instance class. :param dict data_layer: information from Meta class used to configure the data layer """ if data_layer.get('get_base_query') is not None and callable(data_layer['get_base_query']): cls.get_base_query = data_layer['get_base_query'] PKðlRJÐÈëjd d &flask_rest_jsonapi/data_layers/base.py# -*- coding: utf-8 -*- class BaseDataLayer(object): def __init__(self, *args, **kwargs): """Intialize an data layer instance with kwargs :param dict kwargs: information about data layer instance """ for key, value in kwargs.items(): setattr(self, key, value) def create_object(self, *args, **kwargs): """Create an instance of an object and store it through the data layer """ raise NotImplemented def get_object(self, *args, **kwargs): """Get an object through the data layer """ raise NotImplemented def get_collection(self, *args, **kwargs): """Get a collection of objects through the data layer """ raise NotImplemented def update_object(self, *args, **kwargs): """Update an instance of an object and store changes through the data layer """ raise NotImplemented def delete_object(self, *args, **kwargs): """Delete an item through the data layer """ raise NotImplemented def before_create_object(self, data, **view_kwargs): """Provide additional data before instance creation :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view """ raise NotImplemented def before_update_object(self, obj, data, **view_kwargs): """Make checks or provide additional data before update instance :param obj: an object from data layer :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view """ raise NotImplemented def before_delete_object(self, obj, **view_kwargs): """Make checks before delete instance :param obj: an object from data layer :param dict view_kwargs: kwargs from the resource view """ raise NotImplemented @classmethod def configure(cls, meta): """Rewrite default implemantation of methods or attributs :param class meta: information from Meta class used to configure the data layer instance """ for obj in ('query', 'before_create_object', 'before_update_object', 'before_delete_object'): if hasattr(meta, obj) and callable(getattr(meta, obj)): setattr(cls, obj, getattr(meta, obj)) PK·zPJ*flask_rest_jsonapi/data_layers/__init__.pyPKmRJ^-Ò 2Flask_Rest_JSONAPI-0.6.2.dist-info/DESCRIPTION.rstUNKNOWN PKmRJ¥c#6ÞÞ0Flask_Rest_JSONAPI-0.6.2.dist-info/metadata.json{"classifiers": ["Framework :: Flask", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "License :: OSI Approved :: MIT License"], "extensions": {"python.details": {"contacts": [{"email": "pf@milibris.net", "name": "miLibris API Team", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/miLibris/flask-rest-jsonapi"}}}, "extras": ["docs", "mongo_data_layer", "tests"], "generator": "bdist_wheel (0.26.0)", "keywords": ["web", "api", "jsonapi", "flask"], "license": "MIT", "metadata_version": "2.0", "name": "Flask-Rest-JSONAPI", "platform": "any", "run_requires": [{"requires": ["Flask", "marshmallow-jsonapi", "six", "sqlalchemy"]}, {"extra": "mongo_data_layer", "requires": ["pymongo"]}, {"extra": "tests", "requires": ["pytest"]}, {"extra": "docs", "requires": ["sphinx"]}], "summary": "Flask extension to create web api according to jsonapi specification with Flask, Marshmallow and data provider of your choice (SQLAlchemy, MongoDB, ...)", "test_requires": [{"requires": ["pytest"]}], "version": "0.6.2"}PKmRJ `ˆ§0Flask_Rest_JSONAPI-0.6.2.dist-info/top_level.txtflask_rest_jsonapi PKmRJìndªnn(Flask_Rest_JSONAPI-0.6.2.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PKmRJˆi™Ø%%+Flask_Rest_JSONAPI-0.6.2.dist-info/METADATAMetadata-Version: 2.0 Name: Flask-Rest-JSONAPI Version: 0.6.2 Summary: Flask extension to create web api according to jsonapi specification with Flask, Marshmallow and data provider of your choice (SQLAlchemy, MongoDB, ...) Home-page: https://github.com/miLibris/flask-rest-jsonapi Author: miLibris API Team Author-email: pf@milibris.net License: MIT Keywords: web api jsonapi flask Platform: any Classifier: Framework :: Flask Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: License :: OSI Approved :: MIT License Requires-Dist: Flask Requires-Dist: marshmallow-jsonapi Requires-Dist: six Requires-Dist: sqlalchemy Provides-Extra: docs Requires-Dist: sphinx; extra == 'docs' Provides-Extra: mongo_data_layer Requires-Dist: pymongo; extra == 'mongo_data_layer' Provides-Extra: tests Requires-Dist: pytest; extra == 'tests' UNKNOWN PKmRJzòz)Flask_Rest_JSONAPI-0.6.2.dist-info/RECORDFlask_Rest_JSONAPI-0.6.2.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 Flask_Rest_JSONAPI-0.6.2.dist-info/METADATA,sha256=PNOf36wk02RuXtjjiZWBQ8amXHdBn2WKILJLxKlAztY,1061 Flask_Rest_JSONAPI-0.6.2.dist-info/RECORD,, Flask_Rest_JSONAPI-0.6.2.dist-info/WHEEL,sha256=GrqQvamwgBV4nLoJe0vhYRSWzWsx7xjlt74FT0SWYfE,110 Flask_Rest_JSONAPI-0.6.2.dist-info/metadata.json,sha256=tgcKEnLtsToCMJMEK5GDaUf0_rQ4mVzoCOY2sG3LbkM,1246 Flask_Rest_JSONAPI-0.6.2.dist-info/top_level.txt,sha256=f5kOu1Fk6pReGO-JpRnp4Heu0crwHvMsCibjbZPy9cU,19 flask_rest_jsonapi/__init__.py,sha256=6DFuJMC_bUNPuDDyKTH6KgS6DsmN6z4P9jJli4iDuk8,495 flask_rest_jsonapi/api.py,sha256=aRLRAe78SZLVbEaY7x8ssSu4Ro81zN71gr46DnWbkrg,1824 flask_rest_jsonapi/constants.py,sha256=0VzVEqogYHnKe7dzDTcda9lRRdLJN86ZD7A2b3UNJDU,89 flask_rest_jsonapi/decorators.py,sha256=hPXuiSBPnynOt-4ckTkO0PXcPwdUAVWBXHt1d4-4vdY,2530 flask_rest_jsonapi/errors.py,sha256=duKdG-Uym1G5rcD1YqcLn4lBpBGMOnuM7C9wMxBmnK4,323 flask_rest_jsonapi/exceptions.py,sha256=QKC16BQzPcRwXhpaqKHc2imvvbc0WhKVwpN6336rQlo,1295 flask_rest_jsonapi/pagination.py,sha256=6QuBGBfPNpkmjmUYLxOjipTFqCjn7Z0hPjEx59krQbo,1687 flask_rest_jsonapi/querystring.py,sha256=71vXJbikXC_-6PzMOOBIIdPGp3SdzEbrCA0IKS64Qdw,4893 flask_rest_jsonapi/resource.py,sha256=4FMcbx8xktKoGsRnl_pNFWsohUF5BYMUUdUxCkH732Y,17830 flask_rest_jsonapi/schema.py,sha256=0HZyAUbv0_6cCI7jiUJHT2zelEthNdt2wwWadvMlqyE,3385 flask_rest_jsonapi/data_layers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 flask_rest_jsonapi/data_layers/alchemy.py,sha256=s_TGbN9cYnHZXXcEbIxcMnoUHlC-NaJ0RG7ESfeItws,11979 flask_rest_jsonapi/data_layers/base.py,sha256=EWnIlEri1e0_4jXUZH04Jk-LnPMjWd0KOzgFuHhpgIA,2404 flask_rest_jsonapi/data_layers/mongo.py,sha256=RQu9MgL6jJNJRvno9Hv76LaoCpIP8I15IQZ3FpWE_k0,5716 tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 tests/conftest.py,sha256=9ND3_GL4Fce3i2TfCEm7ozcJexBtpf9_M89piHYe1zM,268 tests/test_sqlalchemy_data_layer.py,sha256=2piNGVbU-qaSmngoRxZdr9LBWewhYOLjY7VahUm0rbs,7349 PKùº{I¿Ýéÿµµ#tests/test_sqlalchemy_data_layer.pyPK¤hrIêòQ  ötests/conftest.pyPK-‡rI1tests/__init__.pyPK·zPJ@¨]€!`flask_rest_jsonapi/querystring.pyPK·zPJ[ðßñCC¼1flask_rest_jsonapi/errors.pyPK·zPJŒHE< 93flask_rest_jsonapi/exceptions.pyPK·zPJ ôlC  †8flask_rest_jsonapi/api.pyPK·zPJ]ÄE{9 9 Ý?flask_rest_jsonapi/schema.pyPK·zPJ ý‹ —— PMflask_rest_jsonapi/pagination.pyPK¤hrIöc¿“YY%Tflask_rest_jsonapi/constants.pyPKtQJ,j}â â »Tflask_rest_jsonapi/decorators.pyPK·zPJ³ðYïïÛ^flask_rest_jsonapi/__init__.pyPKèzQJw!¦E¦Eaflask_rest_jsonapi/resource.pyPKðlRJêR ¯Ë.Ë.)è¦flask_rest_jsonapi/data_layers/alchemy.pyPKXVAJA0 :TT'úÕflask_rest_jsonapi/data_layers/mongo.pyPKðlRJÐÈëjd d &“ìflask_rest_jsonapi/data_layers/base.pyPK·zPJ*;öflask_rest_jsonapi/data_layers/__init__.pyPKmRJ^-Ò 2ƒöFlask_Rest_JSONAPI-0.6.2.dist-info/DESCRIPTION.rstPKmRJ¥c#6ÞÞ0ÝöFlask_Rest_JSONAPI-0.6.2.dist-info/metadata.jsonPKmRJ `ˆ§0 üFlask_Rest_JSONAPI-0.6.2.dist-info/top_level.txtPKmRJìndªnn(jüFlask_Rest_JSONAPI-0.6.2.dist-info/WHEELPKmRJˆi™Ø%%+ýFlask_Rest_JSONAPI-0.6.2.dist-info/METADATAPKmRJzòz)ŒFlask_Rest_JSONAPI-0.6.2.dist-info/RECORDPKBå