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' PKhrIQ  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}tJUvv!flask_rest_jsonapi/querystring.py# -*- coding: utf-8 -*- import json from flask_rest_jsonapi.exceptions import BadRequest, InvalidFilters, InvalidSort from flask_rest_jsonapi.schema import get_model_field, get_relationships class QueryStringManager(object): """Querystring parser according to jsonapi reference """ MANAGED_KEYS = ( 'filter', 'page', 'fields', 'sort', 'include' ) def __init__(self, querystring, schema): """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 self.schema = schema def _get_key_values(self, name): """Return a dict containing key / values items for a given key, used for items like filters, page, etc. :param str name: name of the querystring parameter :return dict: a dict of key / values items """ results = {} for key, value in self.qs.items(): try: if not key.startswith(name): continue key_start = key.index('[') + 1 key_end = key.index(']') item_key = key[key_start:key_end] if ',' in value: item_value = value.split(',') else: item_value = value results.update({item_key: item_value}) except Exception: raise BadRequest({'parameter': key}, "Parse error") return results @property def querystring(self): """Return original querystring but containing only managed keys :return dict: dict of managed querystring parameter """ return {key: value for (key, value) in self.qs.items() if key.startswith(self.MANAGED_KEYS)} @property def filters(self): """Return filters from query string. :return list: filter information """ filters = self.qs.get('filter') if filters is not None: try: filters = json.loads(filters) except (ValueError, TypeError): raise InvalidFilters("Parse error") 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') for key, value in result.items(): if key not in ('number', 'size'): raise BadRequest({'parameter': 'page'}, "{} is not a valid parameter of pagination".format(key)) try: int(value) except ValueError: raise BadRequest({'parameter': 'page[{}]'.format(key)}, "Parse error") 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'], } """ result = self._get_key_values('fields') for key, value in result.items(): if not isinstance(value, list): result[key] = [value] return result @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'}, ] """ if self.qs.get('sort'): sorting_results = [] for sort_field in self.qs['sort'].split(','): field = sort_field.replace('-', '') if field not in self.schema._declared_fields: raise InvalidSort("{} has no attribute {}".format(self.schema.__name__, field)) if field in get_relationships(self.schema).values(): raise InvalidSort("You can't sort on {} because it is a relationship field".format(field)) field = get_model_field(self.schema, field) order = 'desc' if sort_field.startswith('-') else 'asc' sorting_results.append({'field': field, 'order': order}) return sorting_results return [] @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 [] PKJmJ:nnflask_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], 'jsonapi': {'version': '1.0'}} PKmWJRƐ__ 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 dict 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': self.source, 'title': self.title, 'detail': self.detail} class BadRequest(JsonApiException): title = "Bad request" status = 400 class InvalidField(BadRequest): title = "Invalid fields querystring parameter." def __init__(self, detail): self.source = {'parameter': 'fields'} self.detail = detail class InvalidInclude(BadRequest): title = "Invalid include querystring parameter." def __init__(self, detail): self.source = {'parameter': 'include'} self.detail = detail class InvalidFilters(BadRequest): title = "Invalid filters querystring parameter." def __init__(self, detail): self.source = {'parameter': 'filters'} self.detail = detail class InvalidSort(BadRequest): title = "Invalid sort querystring parameter." def __init__(self, detail): self.source = {'parameter': 'sort'} self.detail = detail 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 PKx{JIEEEflask_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=None): """Update flask application with our api :param Application app: a flask application """ if self.app is None: self.app = app if self.blueprint is not None: self.app.register_blueprint(self.blueprint) else: for resource in self.resources: self.route(**resource) def route(self, resource, view, *urls, **kwargs): """Create an api view. :param Resource resource: a resource class inherited from flask_rest_jsonapi.resource.Resource :param str view: the view name :param list urls: the urls of the view :param dict kwargs: additional options of the route """ resource.view = view view_func = resource.as_view(view) url_rule_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, **url_rule_options) elif self.blueprint is not None: resource.view = '.'.join([self.blueprint.name, resource.view]) for url in urls: self.blueprint.add_url_rule(url, view_func=view_func, **url_rule_options) else: self.resources.append({'resource': resource, 'view': view, 'urls': urls, 'url_rule_options': url_rule_options}) PK}tJ~flask_rest_jsonapi/schema.py# -*- coding: utf-8 -*- from marshmallow import class_registry from marshmallow.base import SchemaABC from marshmallow_jsonapi.fields import Relationship 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("{} has no attribute {}".format(schema_cls.__name__, field)) elif not isinstance(schema_cls._declared_fields[field], Relationship): raise InvalidInclude("{} is not a relationship attribute of {}".format(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("{} has no attribute {}".format(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 def get_model_field(schema, field): """Get the model field of a schema field :param Schema schema: a marshmallow schema :param str field: the name of the schema field :return str: the name of the field in the model """ if schema._declared_fields[field].attribute is not None: return schema._declared_fields[field].attribute return field def get_relationships(schema): """Return relationship mapping from schema to model :param Schema schema: a marshmallow schema :param list: list of dict with schema field and model field """ return {get_model_field(schema, key): key for (key, value) in schema._declared_fields.items() if isinstance(value, Relationship)} PK}tJ7^DD 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'] = base_url # compute self link if all_qs_args: links['self'] += '?' + urlencode(all_qs_args) if querystring.pagination.get('size') != '0' and object_count > 1: # 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 > 1: links['first'] = links['last'] = base_url all_qs_args.pop('page[number]', None) # compute first link if all_qs_args: links['first'] += '?' + urlencode(all_qs_args) 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 PKhrIcYYflask_rest_jsonapi/constants.py# -*- coding: utf-8 -*- # default number of items for pagination DEFAULT_PAGE_SIZE = 20 PKx{J>y. . flask_rest_jsonapi/decorators.py# -*- coding: utf-8 -*- import json from flask import request, make_response from flask_rest_jsonapi.errors import jsonapi_errors 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.method in ('POST', 'PATCH'): if request.headers['Content-Type'] != 'application/vnd.api+json': error = json.dumps(jsonapi_errors([{'source': '', 'detail': "Content-Type header must be application/vnd.api+json", 'title': 'InvalidRequestHeader', 'status': 415}])) return make_response(error, 415, {'Content-Type': 'application/vnd.api+json'}) if request.headers.get('Accept') and request.headers['Accept'] != 'application/vnd.api+json': error = json.dumps(jsonapi_errors([{'source': '', 'detail': "Accept header must be application/vnd.api+json", 'title': 'InvalidRequestHeader', 'status': 406}])) return make_response(error, 406, {'Content-Type': 'application/vnd.api+json'}) return f(*args, **kwargs) 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) error_message = "You must provide {error_field} in {cls} to get access to the default {method} method" error_data = {'cls': cls.__name__, 'method': request.method} if not hasattr(self, '_data_layer'): error_data.update({'error_field': 'a data layer class'}) raise Exception(error_message.format(**error_data)) if request.method != 'DELETE': if not hasattr(self, 'schema'): error_data.update({'error_field': 'a schema class'}) raise Exception(error_message.format(**error_data)) return f(self, *args, **kwargs) return wrapped_f PKVcJO\MMflask_rest_jsonapi/__init__.py# -*- coding: utf-8 -*- from flask_rest_jsonapi.api import Api from flask_rest_jsonapi.resource import ResourceList, ResourceDetail, ResourceRelationship from flask_rest_jsonapi.exceptions import JsonApiException __all__ = [ 'Api', 'ResourceList', 'ResourceDetail', 'ResourceRelationship', 'JsonApiException' ] PKhvJZ/J/Jflask_rest_jsonapi/resource.py# -*- coding: utf-8 -*- import inspect import json from copy import copy from werkzeug.wrappers import Response from flask import request, url_for, make_response, current_app from flask.views import 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 InvalidType, BadRequest, JsonApiException, RelationNotFound from flask_rest_jsonapi.decorators import check_headers, check_method_requirements from flask_rest_jsonapi.schema import compute_schema, get_relationships, get_model_field from flask_rest_jsonapi.data_layers.base import BaseDataLayer from flask_rest_jsonapi.data_layers.alchemy import SqlalchemyDataLayer class Resource(MethodView): def __new__(cls): if hasattr(cls, 'data_layer'): if not isinstance(cls.data_layer, dict): raise Exception("You must provide a data layer information as dict in {}".format(cls.__name__)) if cls.data_layer.get('class') is not None and BaseDataLayer not in inspect.getmro(cls.data_layer['class']): raise Exception("You must provide a data layer class inherited from BaseDataLayer in {}" .format(cls.__name__)) data_layer_cls = cls.data_layer.get('class', SqlalchemyDataLayer) data_layer_kwargs = copy(cls.data_layer) cls._data_layer = data_layer_cls(data_layer_kwargs) cls._data_layer.resource = cls for method in getattr(cls, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')): if hasattr(cls, method.lower()): setattr(cls, method.lower(), check_headers(getattr(cls, method.lower()))) for method in ('get', 'post', 'patch', 'delete'): if hasattr(cls, '{}_decorators'.format(method)) and hasattr(cls, method): for decorator in getattr(cls, '{}_decorators'.format(method)): setattr(cls, method, decorator(getattr(cls, method))) return super(Resource, cls).__new__(cls) def dispatch_request(self, *args, **kwargs): method = getattr(self, request.method.lower(), None) if method is None and request.method == 'HEAD': method = getattr(self, 'get', None) assert method is not None, 'Unimplemented method {}'.format(request.method) headers = {'Content-Type': 'application/vnd.api+json'} try: response = method(*args, **kwargs) except JsonApiException as e: return make_response(json.dumps(jsonapi_errors([e.to_dict()])), e.status, headers) except Exception as e: if current_app.config['DEBUG'] is True: raise e exc = JsonApiException('', str(e)) return make_response(json.dumps(jsonapi_errors([exc.to_dict()])), exc.status, headers) if isinstance(response, Response): response.headers.add('Content-Type', 'application/vnd.api+json') return response if not isinstance(response, tuple): if isinstance(response, dict): response.update({'jsonapi': {'version': '1.0'}}) return make_response(json.dumps(response), 200, headers) try: data, status_code, headers = response headers.update({'Content-Type': 'application/vnd.api+json'}) except ValueError: pass try: data, status_code = response except ValueError: pass if isinstance(data, dict): data.update({'jsonapi': {'version': '1.0'}}) return make_response(json.dumps(data), status_code, headers) class ResourceList(Resource): @check_method_requirements def get(self, *args, **kwargs): """Retrieve a collection of objects """ self.before_get(args, kwargs) qs = QSManager(request.args, self.schema) object_count, objects = self._data_layer.get_collection(qs, kwargs) schema_kwargs = getattr(self, 'get_schema_kwargs', dict()) schema_kwargs.update({'many': True}) schema = compute_schema(self.schema, schema_kwargs, qs, qs.include) result = schema.dump(objects).data view_kwargs = request.view_args if getattr(self, 'view_kwargs', None) is True else dict() add_pagination_links(result, object_count, qs, url_for(self.view, **view_kwargs)) result.update({'meta': {'count': object_count}}) self.after_get(result) return result @check_method_requirements def post(self, *args, **kwargs): """Create an object """ self.before_post(args, kwargs) json_data = request.get_json() qs = QSManager(request.args, self.schema) schema = compute_schema(self.schema, getattr(self, 'post_schema_kwargs', dict()), qs, qs.include) 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 obj = self._data_layer.create_object(data, kwargs) result = schema.dump(obj).data self.after_post(result) return result, 201, {'Location': result['data']['links']['self']} def before_get(self, args, kwargs): pass def after_get(self, result): pass def before_post(self, args, kwargs): pass def after_post(self, result): pass class ResourceDetail(Resource): @check_method_requirements def get(self, *args, **kwargs): """Get object details """ self.before_get(args, kwargs) obj = self._data_layer.get_object(kwargs) qs = QSManager(request.args, self.schema) schema = compute_schema(self.schema, getattr(self, 'get_schema_kwargs', dict()), qs, qs.include) result = schema.dump(obj).data self.after_get(result) return result @check_method_requirements def patch(self, *args, **kwargs): """Update an object """ self.before_patch(args, kwargs) json_data = request.get_json() qs = QSManager(request.args, self.schema) schema_kwargs = getattr(self, 'patch_schema_kwargs', dict()) schema_kwargs.update({'partial': True}) schema = compute_schema(self.schema, schema_kwargs, qs, qs.include) 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 if 'id' not in json_data['data']: raise BadRequest('/data/id', 'Missing id in "data" node') if json_data['data']['id'] != str(kwargs[self.data_layer.get('url_field', 'id')]): raise BadRequest('/data/id', 'Value of id does not match the resource identifier in url') obj = self._data_layer.get_object(kwargs) self._data_layer.update_object(obj, data, kwargs) result = schema.dump(obj).data self.after_patch(result) return result @check_method_requirements def delete(self, *args, **kwargs): """Delete an object """ self.before_delete(args, kwargs) obj = self._data_layer.get_object(kwargs) self._data_layer.delete_object(obj, kwargs) result = {'meta': 'Object successful deleted'} self.after_delete(result) return result def before_get(self, args, kwargs): pass def after_get(self, result): pass def before_patch(self, args, kwargs): pass def after_patch(self, result): pass def before_delete(self, args, kwargs): pass def after_delete(self, result): pass class ResourceRelationship(Resource): @check_method_requirements def get(self, *args, **kwargs): """Get a relationship details """ self.before_get(args, kwargs) relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data() related_view = self.schema._declared_fields[relationship_field].related_view related_view_kwargs = self.schema._declared_fields[relationship_field].related_view_kwargs obj, data = self._data_layer.get_relationship(model_relationship_field, related_type_, related_id_field, kwargs) for key, value in copy(related_view_kwargs).items(): if isinstance(value, str) and value.startswith('<') and value.endswith('>'): tmp_obj = obj for field in value[1:-1].split('.'): tmp_obj = getattr(tmp_obj, field) related_view_kwargs[key] = tmp_obj result = {'links': {'self': request.path, 'related': url_for(related_view, **related_view_kwargs)}, 'data': data} qs = QSManager(request.args, self.schema) if qs.include: schema = compute_schema(self.schema, dict(), qs, qs.include) serialized_obj = schema.dump(obj) result['included'] = serialized_obj.data.get('included', dict()) self.after_get(result) return result @check_method_requirements def post(self, *args, **kwargs): """Add / create relationship(s) """ self.before_post(args, kwargs) json_data = request.get_json() relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data() 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'] != 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'] != related_type_: raise InvalidType('/data/type', 'The type provided does not match the resource type') obj_, updated = self._data_layer.create_relationship(json_data, model_relationship_field, related_id_field, kwargs) qs = QSManager(request.args, self.schema) includes = qs.include if relationship_field not in qs.include: includes.append(relationship_field) schema = compute_schema(self.schema, dict(), qs, includes) status_code = 200 if updated is True else 204 result = schema.dump(obj_).data if result.get('links', {}).get('self') is not None: result['links']['self'] = request.path self.after_post(result) return result, status_code @check_method_requirements def patch(self, *args, **kwargs): """Update a relationship """ self.before_patch(args, kwargs) json_data = request.get_json() relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data() 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'] != 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'] != related_type_: raise InvalidType('/data/type', 'The type provided does not match the resource type') obj_, updated = self._data_layer.update_relationship(json_data, model_relationship_field, related_id_field, kwargs) qs = QSManager(request.args, self.schema) includes = qs.include if relationship_field not in qs.include: includes.append(relationship_field) schema = compute_schema(self.schema, dict(), qs, includes) status_code = 200 if updated is True else 204 result = schema.dump(obj_).data if result.get('links', {}).get('self') is not None: result['links']['self'] = request.path self.after_patch(result) return result, status_code @check_method_requirements def delete(self, *args, **kwargs): """Delete relationship(s) """ self.before_delete(args, kwargs) json_data = request.get_json() relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data() 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'] != 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'] != related_type_: raise InvalidType('/data/type', 'The type provided does not match the resource type') obj_, updated = self._data_layer.delete_relationship(json_data, model_relationship_field, related_id_field, kwargs) qs = QSManager(request.args, self.schema) includes = qs.include if relationship_field not in qs.include: includes.append(relationship_field) schema = compute_schema(self.schema, dict(), qs, includes) status_code = 200 if updated is True else 204 result = schema.dump(obj_).data if result.get('links', {}).get('self') is not None: result['links']['self'] = request.path self.after_delete(result) return result, status_code def _get_relationship_data(self): """Get useful data for relationship management """ relationship_field = request.path.split('/')[-1] if relationship_field not in get_relationships(self.schema).values(): raise RelationNotFound('', "{} has no attribute {}".format(self.schema.__name__, relationship_field)) related_type_ = self.schema._declared_fields[relationship_field].type_ related_id_field = self.schema._declared_fields[relationship_field].id_field model_relationship_field = get_model_field(self.schema, relationship_field) return relationship_field, model_relationship_field, related_type_, related_id_field def before_get(self, args, kwargs): pass def after_get(self, result): pass def before_post(self, args, kwargs): pass def after_post(self, result): pass def before_patch(self, args, kwargs): pass def after_patch(self, result): pass def before_delete(self, args, kwargs): pass def after_delete(self, result): pass PKhvJ78`8`)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 sqlalchemy.inspection import inspect from flask_rest_jsonapi.constants import DEFAULT_PAGE_SIZE from flask_rest_jsonapi.data_layers.base import BaseDataLayer from flask_rest_jsonapi.exceptions import RelationNotFound, RelatedObjectNotFound, JsonApiException,\ InvalidSort from flask_rest_jsonapi.data_layers.filtering.alchemy import create_filters from flask_rest_jsonapi.schema import get_relationships class SqlalchemyDataLayer(BaseDataLayer): def __init__(self, kwargs): super(SqlalchemyDataLayer, self).__init__(kwargs) if not hasattr(self, 'session'): raise Exception("You must provide a session in data_layer_kwargs to use sqlalchemy data layer in {}" .format(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 {}" .format(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) relationship_fields = get_relationships(self.resource.schema) obj = self.model(**{key: value for (key, value) in data.items() if key not in relationship_fields}) self.apply_relationships(data, obj) self.session.add(obj) try: self.session.commit() except Exception as e: self.session.rollback() raise JsonApiException({'pointer': '/data'}, "Object creation error: " + str(e)) self.after_create_object(obj, data, view_kwargs) 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 """ self.before_get_object(view_kwargs) id_field = getattr(self, 'id_field', inspect(self.model).primary_key[0].name) try: filter_field = getattr(self.model, id_field) except Exception: raise Exception("{} has no attribute {}".format(self.model.__name__, id_field)) 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: obj = None self.after_get_object(obj, view_kwargs) 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 """ self.before_get_collection(qs, view_kwargs) 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) collection = query.all() self.after_get_collection(collection, qs, view_kwargs) return object_count, collection 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 :return boolean: True if object have changed else False """ self.before_update_object(obj, data, view_kwargs) relationship_fields = get_relationships(self.resource.schema) for key, value in data.items(): if hasattr(obj, key) and key not in relationship_fields: setattr(obj, key, value) self.apply_relationships(data, obj) try: self.session.commit() except Exception as e: self.session.rollback() raise JsonApiException({'pointer': '/data'}, "Update object error: " + str(e)) self.after_update_object(obj, data, view_kwargs) 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) try: self.session.commit() except Exception as e: self.session.rollback() raise JsonApiException('', "Delete object error: " + str(e)) self.after_delete_object(obj, view_kwargs) def create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Create a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ self.before_create_relationship(json_data, relationship_field, related_id_field, view_kwargs) obj = self.get_object(view_kwargs) if not hasattr(obj, relationship_field): raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field)) related_model = getattr(obj.__class__, relationship_field).property.mapper.class_ updated = False if isinstance(json_data['data'], list): obj_ids = {str(getattr(obj__, related_id_field)) for obj__ in getattr(obj, relationship_field)} for obj_ in json_data['data']: if obj_['id'] not in obj_ids: getattr(obj, relationship_field).append(self.get_related_object(related_model, related_id_field, obj_)) updated = True else: related_object = None if json_data['data'] is not None: related_object = self.get_related_object(related_model, related_id_field, json_data['data']) obj_id = getattr(getattr(obj, relationship_field), related_id_field, None) new_obj_id = getattr(related_object, related_id_field, None) if obj_id != new_obj_id: setattr(obj, relationship_field, related_object) updated = True try: self.session.commit() except Exception as e: self.session.rollback() raise JsonApiException('', "Create relationship error: " + str(e)) self.after_create_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs) return obj, updated def get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs): """Get a relationship :param str relationship_field: the model attribute used for relationship :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) """ self.before_get_relationship(relationship_field, related_type_, related_id_field, view_kwargs) obj = self.get_object(view_kwargs) if not hasattr(obj, relationship_field): raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field)) related_objects = getattr(obj, relationship_field) if related_objects is None: return obj, related_objects self.after_get_relationship(obj, related_objects, relationship_field, related_type_, related_id_field, view_kwargs) 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_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Update a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ self.before_update_relationship(json_data, relationship_field, related_id_field, view_kwargs) obj = self.get_object(view_kwargs) if not hasattr(obj, relationship_field): raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field)) related_model = getattr(obj.__class__, relationship_field).property.mapper.class_ updated = False if isinstance(json_data['data'], list): related_objects = [] for obj_ in json_data['data']: related_objects.append(self.get_related_object(related_model, related_id_field, obj_)) obj_ids = {getattr(obj__, related_id_field) for obj__ in getattr(obj, relationship_field)} new_obj_ids = {getattr(related_object, related_id_field) for related_object in related_objects} if obj_ids != new_obj_ids: setattr(obj, relationship_field, related_objects) updated = True else: related_object = None if json_data['data'] is not None: related_object = self.get_related_object(related_model, related_id_field, json_data['data']) obj_id = getattr(getattr(obj, relationship_field), related_id_field, None) new_obj_id = getattr(related_object, related_id_field, None) if obj_id != new_obj_id: setattr(obj, relationship_field, related_object) updated = True try: self.session.commit() except Exception as e: self.session.rollback() raise JsonApiException('', "Update relationship error: " + str(e)) self.after_update_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs) return obj, updated def delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Delete a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ self.before_delete_relationship(json_data, relationship_field, related_id_field, view_kwargs) obj = self.get_object(view_kwargs) if not hasattr(obj, relationship_field): raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field)) related_model = getattr(obj.__class__, relationship_field).property.mapper.class_ updated = False if isinstance(json_data['data'], list): obj_ids = {str(getattr(obj__, related_id_field)) for obj__ in getattr(obj, relationship_field)} for obj_ in json_data['data']: if obj_['id'] in obj_ids: getattr(obj, relationship_field).remove(self.get_related_object(related_model, related_id_field, obj_)) updated = True else: setattr(obj, relationship_field, None) updated = True try: self.session.commit() except Exception as e: self.session.rollback() raise JsonApiException('', "Delete relationship error: " + str(e)) self.after_delete_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs) return obj, updated 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('', "Could not find {}.{}={} object".format(related_model.__name__, related_id_field, obj['id'])) return related_object def apply_relationships(self, data, obj): """Apply relationship provided by data to obj :param dict data: data provided by the client :param DeclarativeMeta obj: the sqlalchemy object to plug relationships to :return boolean: True if relationship have changed else False """ relationship_fields = get_relationships(self.resource.schema) for key, value in data.items(): if key in relationship_fields: related_model = getattr(obj.__class__, key).property.mapper.class_ related_id_field = self.resource.schema._declared_fields[relationship_fields[key]].id_field if isinstance(value, list): related_objects = [] for identifier in value: related_object = self.get_related_object(related_model, related_id_field, {'id': identifier}) related_objects.append(related_object) setattr(obj, key, related_objects) else: related_object = None if value is not None: related_object = self.get_related_object(related_model, related_id_field, {'id': value}) setattr(obj, key, 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 filter_info: filter information :type filter_info: dict or None :param DeclarativeMeta model: an sqlalchemy model :return Query: the sorted query """ if filter_info: filters = create_filters(model, filter_info, self.resource) query = query.filter(*filters) 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: if not hasattr(self.model, sort_opt['field']): raise InvalidSort("{} has no attribute {}".format(self.model.__name__, sort_opt['field'])) field = text(sort_opt['field']) order = expressions[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 after_create_object(self, obj, data, view_kwargs): """Provide additional data after object creation :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_get_object(self, view_kwargs): """Make work before to retrieve an object :param dict view_kwargs: kwargs from the resource view """ pass def after_get_object(self, obj, view_kwargs): """Make work after to retrieve an object :param obj: an object from data layer :param dict view_kwargs: kwargs from the resource view """ pass def before_get_collection(self, qs, view_kwargs): """Make work before to retrieve a collection of objects :param QueryStringManager qs: a querystring manager to retrieve information from url :param dict view_kwargs: kwargs from the resource view """ pass def after_get_collection(self, collection, qs, view_kwargs): """Make work after to retrieve a collection of objects :param iterable collection: the collection of objects :param QueryStringManager qs: a querystring manager to retrieve information from url :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 after_update_object(self, obj, data, view_kwargs): """Make work after 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 def after_delete_object(self, obj, view_kwargs): """Make work after delete object :param obj: an object from data layer :param dict view_kwargs: kwargs from the resource view """ pass def before_create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Make work before to create a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ pass def after_create_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs): """Make work after to create a relationship :param obj: an object from data layer :param bool updated: True if object was updated else False :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ pass def before_get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs): """Make work before to get information about a relationship :param str relationship_field: the model attribute used for relationship :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) """ pass def after_get_relationship(self, obj, related_objects, relationship_field, related_type_, related_id_field, view_kwargs): """Make work after to get information about a relationship :param obj: an object from data layer :param iterable related_objects: related objects of the object :param str relationship_field: the model attribute used for relationship :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) """ pass def before_update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Make work before to update a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ pass def after_update_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs): """Make work after to update a relationship :param obj: an object from data layer :param bool updated: True if object was updated else False :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ pass def before_delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Make work before to delete a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ pass def after_delete_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs): """Make work after to delete a relationship :param obj: an object from data layer :param bool updated: True if object was updated else False :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :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'] PKhvJo 44&flask_rest_jsonapi/data_layers/base.py# -*- coding: utf-8 -*- import types class BaseDataLayer(object): ADDITIONAL_METHODS = ('query', 'before_create_object', 'after_create_object', 'before_get_object', 'after_get_object', 'before_get_collection', 'after_get_collection', 'before_update_object', 'after_update_object', 'before_delete_object', 'after_delete_object', 'before_create_relationship' 'after_create_relationship', 'before_get_relationship', 'after_get_relationship', 'before_update_relationship', 'after_update_relationship', 'before_delete_relationship', 'after_delete_relationship') def __init__(self, kwargs): """Intialize an data layer instance with kwargs :param dict kwargs: information about data layer instance """ if kwargs.get('methods') is not None: self.bound_additional_methods(kwargs['methods']) kwargs.pop('methods') kwargs.pop('class', None) for key, value in kwargs.items(): setattr(self, key, value) def create_object(self, data, view_kwargs): """Create an object :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view :return DeclarativeMeta: an object """ raise NotImplementedError def get_object(self, view_kwargs): """Retrieve an object :params dict view_kwargs: kwargs from the resource view :return DeclarativeMeta: an object """ raise NotImplementedError def get_collection(self, qs, view_kwargs): """Retrieve a collection of objects :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 """ raise NotImplementedError def update_object(self, obj, data, view_kwargs): """Update an object :param DeclarativeMeta obj: an object :param dict data: the data validated by marshmallow :param dict view_kwargs: kwargs from the resource view :return boolean: True if object have changed else False """ raise NotImplementedError def delete_object(self, obj, view_kwargs): """Delete an item through the data layer :param DeclarativeMeta obj: an object :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Create a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ raise NotImplementedError def get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs): """Get information about a relationship :param str relationship_field: the model attribute used for relationship :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) """ raise NotImplementedError def update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Update a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ raise NotImplementedError def delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Delete a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def query(self, view_kwargs): """Construct the base query to retrieve wanted data :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError 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 """ raise NotImplementedError def after_create_object(self, obj, data, view_kwargs): """Provide additional data after object creation :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 NotImplementedError def before_get_object(self, view_kwargs): """Make work before to retrieve an object :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def after_get_object(self, obj, view_kwargs): """Make work after to retrieve an object :param obj: an object from data layer :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def before_get_collection(self, qs, view_kwargs): """Make work before to retrieve a collection of objects :param QueryStringManager qs: a querystring manager to retrieve information from url :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def after_get_collection(self, collection, qs, view_kwargs): """Make work after to retrieve a collection of objects :param iterable collection: the collection of objects :param QueryStringManager qs: a querystring manager to retrieve information from url :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError 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 """ raise NotImplementedError def after_update_object(self, obj, data, view_kwargs): """Make work after 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 """ raise NotImplementedError 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 """ raise NotImplementedError def after_delete_object(self, obj, view_kwargs): """Make work after delete object :param obj: an object from data layer :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def before_create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Make work before to create a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ raise NotImplementedError def after_create_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs): """Make work after to create a relationship :param obj: an object from data layer :param bool updated: True if object was updated else False :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ raise NotImplementedError def before_get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs): """Make work before to get information about a relationship :param str relationship_field: the model attribute used for relationship :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) """ raise NotImplementedError def after_get_relationship(self, obj, related_objects, relationship_field, related_type_, related_id_field, view_kwargs): """Make work after to get information about a relationship :param obj: an object from data layer :param iterable related_objects: related objects of the object :param str relationship_field: the model attribute used for relationship :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) """ raise NotImplementedError def before_update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Make work before to update a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ raise NotImplementedError def after_update_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs): """Make work after to update a relationship :param obj: an object from data layer :param bool updated: True if object was updated else False :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view :return boolean: True if relationship have changed else False """ raise NotImplementedError def before_delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs): """Make work before to delete a relationship :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def after_delete_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs): """Make work after to delete a relationship :param obj: an object from data layer :param bool updated: True if object was updated else False :param dict json_data: the request params :param str relationship_field: the model attribute used for relationship :param str related_id_field: the identifier field of the related model :param dict view_kwargs: kwargs from the resource view """ raise NotImplementedError def bound_additional_methods(self, methods): """Bound additional methods to current instance :param class meta: information from Meta class used to configure the data layer instance """ for key, value in methods.items(): if key in self.ADDITIONAL_METHODS: setattr(self, key, types.MethodType(value, self)) PKzPJ*flask_rest_jsonapi/data_layers/__init__.pyPKhvJD3flask_rest_jsonapi/data_layers/filtering/alchemy.py# -*- coding: utf-8 -*- from sqlalchemy import and_, or_, not_ from flask_rest_jsonapi.exceptions import InvalidFilters from flask_rest_jsonapi.schema import get_relationships, get_model_field def create_filters(model, filter_info, resource): """Apply filters from filters information to base query :param DeclarativeMeta model: the model of the node :param dict filter_info: current node filter information :param Resource resource: the resource """ filters = [] for filter_ in filter_info: filters.append(Node(model, filter_, resource, resource.schema).resolve()) return filters class Node(object): def __init__(self, model, filter_, resource, schema): self.model = model self.filter_ = filter_ self.resource = resource self.schema = schema def resolve(self): if 'or' not in self.filter_ and 'and' not in self.filter_ and 'not' not in self.filter_: if self.val is None and self.field is None: raise InvalidFilters("Can't find value or field in a filter") value = self.value if isinstance(self.val, dict): value = Node(self.related_model, self.val, self.resource, self.related_schema).resolve() if '__' in self.filter_.get('name', ''): value = {self.filter_['name'].split('__')[1]: value} if isinstance(value, dict): return getattr(self.column, self.operator)(**value) else: return getattr(self.column, self.operator)(value) if 'or' in self.filter_: return or_(Node(self.model, filt, self.resource, self.schema).resolve() for filt in self.filter_['or']) if 'and' in self.filter_: return and_(Node(self.model, filt, self.resource, self.schema).resolve() for filt in self.filter_['and']) if 'not' in self.filter_: return not_(Node(self.model, self.filter_['not'], self.resource, self.schema).resolve()) @property def name(self): """Return the name of the node or raise a BadRequest exception :return str: the name of the field to filter on """ name = self.filter_.get('name') if name is None: raise InvalidFilters("Can't find name of a filter") if '__' in name: name = name.split('__')[0] if name not in self.schema._declared_fields: raise InvalidFilters("{} has no attribute {}".format(self.schema.__name__, name)) return name @property def op(self): """Return the operator of the node :return str: the operator to use in the filter """ try: return self.filter_['op'] except KeyError: raise InvalidFilters("Can't find op of a filter") @property def val(self): """Return the val of the node :return: the value to filter with """ return self.filter_.get('val') @property def field(self): """Return the field of the node :return: the field to pick up value from to filter with """ return self.filter_.get('field') @property def column(self): """Get the column object :param DeclarativeMeta model: the model :param str field: the field :return InstrumentedAttribute: the column to filter on """ field = self.name model_field = get_model_field(self.schema, field) try: return getattr(self.model, model_field) except AttributeError: raise InvalidFilters("{} has no attribute {} in a filter".format(self.model.__name__, model_field)) @property def operator(self): """Get the function operator from his name :return callable: a callable to make operation on a column """ operators = (self.op, self.op + '_', '__' + self.op + '__') for op in operators: if hasattr(self.column, op): return op raise InvalidFilters("{} has no operator {} in a filter".format(self.column.key, self.op)) @property def value(self): """Get the value to filter on :return: the value to filter on """ if self.field is not None: try: return getattr(self.model, self.field) except AttributeError: raise InvalidFilters("{} has no attribute {} in a filter".format(self.model.__name__, self.field)) return self.val @property def related_model(self): """Get the related model of a relationship field :return DeclarativeMeta: the related model """ relationship_field = self.name if relationship_field not in get_relationships(self.schema).values(): raise InvalidFilters("{} has no relationship attribute {}".format(self.schema.__name__, relationship_field)) relationship_model_field = get_model_field(self.schema, relationship_field) return getattr(self.model, relationship_model_field).property.mapper.class_ @property def related_schema(self): """Get the related schema of a relationship field :return Schema: the related schema """ relationship_field = self.name if relationship_field not in get_relationships(self.schema).values(): raise InvalidFilters("{} has no relationship attribute {}".format(self.schema.__name__, relationship_field)) return self.schema._declared_fields[relationship_field].schema.__class__ PKmWJ4flask_rest_jsonapi/data_layers/filtering/__init__.pyPK9y{J^- 3Flask_REST_JSONAPI-0.11.5.dist-info/DESCRIPTION.rstUNKNOWN PK9y{J?1Flask_REST_JSONAPI-0.11.5.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", "tests"], "generator": "bdist_wheel (0.26.0)", "keywords": ["web", "api", "rest", "jsonapi", "flask", "sqlalchemy", "marshmallow"], "license": "MIT", "metadata_version": "2.0", "name": "Flask-REST-JSONAPI", "platform": "any", "run_requires": [{"requires": ["Flask", "marshmallow (==2.13.1)", "marshmallow-jsonapi", "six", "sqlalchemy"]}, {"extra": "tests", "requires": ["pytest"]}, {"extra": "docs", "requires": ["sphinx"]}], "summary": "Flask extension to create REST web api according to JSONAPI 1.0 specification with Flask, Marshmallow and data provider of your choice (SQLAlchemy, MongoDB, ...)", "test_requires": [{"requires": ["pytest"]}], "version": "0.11.5"}PK9y{J `1Flask_REST_JSONAPI-0.11.5.dist-info/top_level.txtflask_rest_jsonapi PK9y{Jndnn)Flask_REST_JSONAPI-0.11.5.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PK9y{Jf,,Flask_REST_JSONAPI-0.11.5.dist-info/METADATAMetadata-Version: 2.0 Name: Flask-REST-JSONAPI Version: 0.11.5 Summary: Flask extension to create REST web api according to JSONAPI 1.0 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 rest jsonapi flask sqlalchemy marshmallow 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 (==2.13.1) Requires-Dist: marshmallow-jsonapi Requires-Dist: six Requires-Dist: sqlalchemy Provides-Extra: docs Requires-Dist: sphinx; extra == 'docs' Provides-Extra: tests Requires-Dist: pytest; extra == 'tests' UNKNOWN PK9y{JhDd*Flask_REST_JSONAPI-0.11.5.dist-info/RECORDFlask_REST_JSONAPI-0.11.5.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 Flask_REST_JSONAPI-0.11.5.dist-info/METADATA,sha256=E86IyH-aIrgf3sLrzCT463zmMJazf62rzB7TD-8MiEY,1052 Flask_REST_JSONAPI-0.11.5.dist-info/RECORD,, Flask_REST_JSONAPI-0.11.5.dist-info/WHEEL,sha256=GrqQvamwgBV4nLoJe0vhYRSWzWsx7xjlt74FT0SWYfE,110 Flask_REST_JSONAPI-0.11.5.dist-info/metadata.json,sha256=RkOQ230WS9R0SHS8lLHRC57mIaon9gVZDfjDJ73Yejs,1243 Flask_REST_JSONAPI-0.11.5.dist-info/top_level.txt,sha256=f5kOu1Fk6pReGO-JpRnp4Heu0crwHvMsCibjbZPy9cU,19 flask_rest_jsonapi/__init__.py,sha256=lSh8xndo3tdU0GlcDiB4eOyForRnUG6T2dgag_RtMsE,333 flask_rest_jsonapi/api.py,sha256=KbiZIW1PFcOIxE2au5wwFlMghnS0DmA4IrEwU1i7uOs,1861 flask_rest_jsonapi/constants.py,sha256=0VzVEqogYHnKe7dzDTcda9lRRdLJN86ZD7A2b3UNJDU,89 flask_rest_jsonapi/decorators.py,sha256=7kLZ4qALrAhBV-PYZ2CdI7y3QgcW2RTHjEPnPVuG_98,2350 flask_rest_jsonapi/errors.py,sha256=xBg2tHBJV13y8APh_yRxh2xm5ixLOjnCw0S0Schfo6Q,366 flask_rest_jsonapi/exceptions.py,sha256=JsKFqy3JHJHuvrbNBsbrRSmlSUd1MKIb7HfTaj6jI70,1887 flask_rest_jsonapi/pagination.py,sha256=UTj5J__IUD4YkCNGCxgN9lCHcdiNJu1H3pnhmju44pI,1860 flask_rest_jsonapi/querystring.py,sha256=CRSHDjXqdKbNLufkM_4fgK6hYfPugp9IlK70sfmasFY,5238 flask_rest_jsonapi/resource.py,sha256=g3hCUmpJlCo_lXZ42hZW_3CsdqpQSoJr5IwcK7n6j8A,18991 flask_rest_jsonapi/schema.py,sha256=DroqB0u_D-8NrJoUviBdVRKa9KJNJ3mioN-fl-KENLc,4048 flask_rest_jsonapi/data_layers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 flask_rest_jsonapi/data_layers/alchemy.py,sha256=xcEU_xOfP5lQDigh1kbfsT4E4dl5W05Lw_Veve2vWGM,24632 flask_rest_jsonapi/data_layers/base.py,sha256=mnSqOYIM4ZI4AalTX7BxjtgCcPelNO-Ae98slp61q58,13331 flask_rest_jsonapi/data_layers/mongo.py,sha256=RQu9MgL6jJNJRvno9Hv76LaoCpIP8I15IQZ3FpWE_k0,5716 flask_rest_jsonapi/data_layers/filtering/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 flask_rest_jsonapi/data_layers/filtering/alchemy.py,sha256=55G_VG_NvDKRdEdo5kkrPsZ6tGXwsfsYyDsR2T5bWVY,5620 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.pyPKhrIQ  tests/conftest.pyPK-rI1tests/__init__.pyPK}tJUvv!`flask_rest_jsonapi/querystring.pyPKJmJ:nn3flask_rest_jsonapi/errors.pyPKmWJRƐ__ 4flask_rest_jsonapi/exceptions.pyPKx{JIEEEZ<flask_rest_jsonapi/api.pyPK}tJ~Cflask_rest_jsonapi/schema.pyPK}tJ7^DD Sflask_rest_jsonapi/pagination.pyPKhrIcYYb[flask_rest_jsonapi/constants.pyPKx{J>y. . [flask_rest_jsonapi/decorators.pyPKVcJO\MMdeflask_rest_jsonapi/__init__.pyPKhvJZ/J/Jfflask_rest_jsonapi/resource.pyPKhvJ78`8`)Xflask_rest_jsonapi/data_layers/alchemy.pyPKXVAJA0 :TT'flask_rest_jsonapi/data_layers/mongo.pyPKhvJo 44&p(flask_rest_jsonapi/data_layers/base.pyPKzPJ*\flask_rest_jsonapi/data_layers/__init__.pyPKhvJD3]flask_rest_jsonapi/data_layers/filtering/alchemy.pyPKmWJ4Tsflask_rest_jsonapi/data_layers/filtering/__init__.pyPK9y{J^- 3sFlask_REST_JSONAPI-0.11.5.dist-info/DESCRIPTION.rstPK9y{J?1tFlask_REST_JSONAPI-0.11.5.dist-info/metadata.jsonPK9y{J `1+yFlask_REST_JSONAPI-0.11.5.dist-info/top_level.txtPK9y{Jndnn)yFlask_REST_JSONAPI-0.11.5.dist-info/WHEELPK9y{Jf,,BzFlask_REST_JSONAPI-0.11.5.dist-info/METADATAPK9y{JhDd*~Flask_REST_JSONAPI-0.11.5.dist-info/RECORDPK