<commit_before>import itertools

import flask
from flask.views import MethodView
from marshmallow import fields, ValidationError
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Load
from sqlalchemy.orm.exc import NoResultFound
from werkzeug.exceptions import NotFound

from . import meta
from .authentication import NoOpAuthentication
from .authorization import NoOpAuthorization
from .compat import MA2, schema_dump, schema_load
from .decorators import request_cached_property
from .exceptions import ApiError
from .spec import ApiViewDeclaration, ModelViewDeclaration
from .utils import iter_validation_errors, settable_property

# -----------------------------------------------------------------------------


class ApiView(MethodView):
    schema = None
    id_fields = ('id',)
    args_schema = None

    authentication = NoOpAuthentication()
    authorization = NoOpAuthorization()

    spec_declaration = ApiViewDeclaration()

    def dispatch_request(self, *args, **kwargs):
        self.authentication.authenticate_request()
        self.authorization.authorize_request()

        return super(ApiView, self).dispatch_request(*args, **kwargs)

    def serialize(self, item, **kwargs):
        return schema_dump(self.serializer, item, **kwargs)

    @settable_property
    def serializer(self):
        return self.schema

    def make_items_response(self, items, *args):
        data_out = self.serialize(items, many=True)
        return self.make_response(data_out, *args, items=items)

    def make_item_response(self, item, *args):
        data_out = self.serialize(item)
        self.set_item_response_meta(item)
        return self.make_response(data_out, *args, item=item)

    def set_item_response_meta(self, item):
        pass

    def make_response(self, data, *args, **kwargs):
        body = self.render_response_body(data, meta.get_response_meta())
        return self.make_raw_response(body, *args, **kwargs)

    def render_response_body(self, data, response_meta):
        body = {'data': data}
        if response_meta is not None:
            body['meta'] = response_meta

        return flask.jsonify(body)

    def make_raw_response(self, *args, **kwargs):
        response = flask.make_response(*args)
        for key, value in kwargs.items():
            setattr(response, key, value)
        return response

    def make_empty_response(self, **kwargs):
        return self.make_raw_response('', 204, **kwargs)

    def make_created_response(self, item):
        response = self.make_item_response(item, 201)
        location = self.get_location(item)
        if location is not None:
            response.headers['Location'] = location
        return response

    def make_updated_response(self, item, return_content=False):
        if return_content:
            return self.make_item_response(item)

        return self.make_empty_response(item=item)

    def make_deleted_response(self, item):
        return self.make_empty_response(item=item)

    def get_location(self, item):
        id_dict = {
            id_field: getattr(item, id_field) for id_field in self.id_fields
        }
        return flask.url_for(flask.request.endpoint, _method='GET', **id_dict)

    def get_request_data(self, **kwargs):
        data_raw = self.parse_request_data()
        return self.deserialize(data_raw, **kwargs)

    def parse_request_data(self):
        try:
            data_raw = flask.request.get_json()['data']
        except TypeError:
            raise ApiError(400, {'code': 'invalid_body'})
        except KeyError:
            raise ApiError(400, {'code': 'invalid_data.missing'})

        return data_raw

    def deserialize(self, data_raw, expected_id=None, **kwargs):
        try:
            data = schema_load(self.deserializer, data_raw, **kwargs)
        except ValidationError as e:
            raise ApiError(422, *(
                self.format_validation_error(error)
                for error in iter_validation_errors(e.messages)
            ))

        self.validate_request_id(data, expected_id)
        return data

    @settable_property
    def deserializer(self):
        return self.schema

    def format_validation_error(self, error):
        message, path = error
        pointer = '/data/{}'.format(
            '/'.join(str(field_key) for field_key in path),
        )

        return {
            'code': 'invalid_data',
            'detail': message,
            'source': {'pointer': pointer},
        }

    def validate_request_id(self, data, expected_id):
        if expected_id is None:
            return

        if expected_id is False:
            for id_field in self.id_fields:
                if id_field in data:
                    raise ApiError(403, {'code': 'invalid_id.forbidden'})
            return

        try:
            id = self.get_data_id(data)
        except KeyError:
            raise ApiError(422, {'code': 'invalid_id.missing'})

        if id != expected_id:
            raise ApiError(409, {'code': 'invalid_id.mismatch'})

    def get_data_id(self, data):
        if len(self.id_fields) == 1:
            return data[self.id_fields[0]]

        return tuple(data[id_field] for id_field in self.id_fields)

    @request_cached_property
    def request_args(self):
        """Use args_schema to parse request query arguments."""
        args = flask.request.args
        data_raw = {}

        for field_name, field in self.args_schema.fields.items():
            if field_name in args:
                args_key = field_name
            elif MA2 and field.load_from and field.load_from in args:
                args_key = field.load_from
            elif not MA2 and field.data_key and field.data_key in args:
                args_key = field.data_key
                field_name = field.data_key
            else:
                continue

            value = args.getlist(args_key)
            if not self.is_list_field(field) and len(value) == 1:
                value = value[0]

            data_raw[field_name] = value

        return self.deserialize_args(data_raw)

    def is_list_field(self, field):
        return isinstance(field, fields.List)

    def deserialize_args(self, data_raw, **kwargs):
        try:
            data = schema_load(self.args_schema, data_raw, **kwargs)
        except ValidationError as e:
            raise ApiError(422, *(<commit_msg>refactor: Simplify computing key for request_args (#253)<commit_after>import itertools

import flask
from flask.views import MethodView
from marshmallow import fields, ValidationError
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Load
from sqlalchemy.orm.exc import NoResultFound
from werkzeug.exceptions import NotFound

from . import meta
from .authentication import NoOpAuthentication
from .authorization import NoOpAuthorization
from .compat import MA2, schema_dump, schema_load
from .decorators import request_cached_property
from .exceptions import ApiError
from .spec import ApiViewDeclaration, ModelViewDeclaration
from .utils import iter_validation_errors, settable_property

# -----------------------------------------------------------------------------


class ApiView(MethodView):
    schema = None
    id_fields = ('id',)
    args_schema = None

    authentication = NoOpAuthentication()
    authorization = NoOpAuthorization()

    spec_declaration = ApiViewDeclaration()

    def dispatch_request(self, *args, **kwargs):
        self.authentication.authenticate_request()
        self.authorization.authorize_request()

        return super(ApiView, self).dispatch_request(*args, **kwargs)

    def serialize(self, item, **kwargs):
        return schema_dump(self.serializer, item, **kwargs)

    @settable_property
    def serializer(self):
        return self.schema

    def make_items_response(self, items, *args):
        data_out = self.serialize(items, many=True)
        return self.make_response(data_out, *args, items=items)

    def make_item_response(self, item, *args):
        data_out = self.serialize(item)
        self.set_item_response_meta(item)
        return self.make_response(data_out, *args, item=item)

    def set_item_response_meta(self, item):
        pass

    def make_response(self, data, *args, **kwargs):
        body = self.render_response_body(data, meta.get_response_meta())
        return self.make_raw_response(body, *args, **kwargs)

    def render_response_body(self, data, response_meta):
        body = {'data': data}
        if response_meta is not None:
            body['meta'] = response_meta

        return flask.jsonify(body)

    def make_raw_response(self, *args, **kwargs):
        response = flask.make_response(*args)
        for key, value in kwargs.items():
            setattr(response, key, value)
        return response

    def make_empty_response(self, **kwargs):
        return self.make_raw_response('', 204, **kwargs)

    def make_created_response(self, item):
        response = self.make_item_response(item, 201)
        location = self.get_location(item)
        if location is not None:
            response.headers['Location'] = location
        return response

    def make_updated_response(self, item, return_content=False):
        if return_content:
            return self.make_item_response(item)

        return self.make_empty_response(item=item)

    def make_deleted_response(self, item):
        return self.make_empty_response(item=item)

    def get_location(self, item):
        id_dict = {
            id_field: getattr(item, id_field) for id_field in self.id_fields
        }
        return flask.url_for(flask.request.endpoint, _method='GET', **id_dict)

    def get_request_data(self, **kwargs):
        data_raw = self.parse_request_data()
        return self.deserialize(data_raw, **kwargs)

    def parse_request_data(self):
        try:
            data_raw = flask.request.get_json()['data']
        except TypeError:
            raise ApiError(400, {'code': 'invalid_body'})
        except KeyError:
            raise ApiError(400, {'code': 'invalid_data.missing'})

        return data_raw

    def deserialize(self, data_raw, expected_id=None, **kwargs):
        try:
            data = schema_load(self.deserializer, data_raw, **kwargs)
        except ValidationError as e:
            raise ApiError(422, *(
                self.format_validation_error(error)
                for error in iter_validation_errors(e.messages)
            ))

        self.validate_request_id(data, expected_id)
        return data

    @settable_property
    def deserializer(self):
        return self.schema

    def format_validation_error(self, error):
        message, path = error
        pointer = '/data/{}'.format(
            '/'.join(str(field_key) for field_key in path),
        )

        return {
            'code': 'invalid_data',
            'detail': message,
            'source': {'pointer': pointer},
        }

    def validate_request_id(self, data, expected_id):
        if expected_id is None:
            return

        if expected_id is False:
            for id_field in self.id_fields:
                if id_field in data:
                    raise ApiError(403, {'code': 'invalid_id.forbidden'})
            return

        try:
            id = self.get_data_id(data)
        except KeyError:
            raise ApiError(422, {'code': 'invalid_id.missing'})

        if id != expected_id:
            raise ApiError(409, {'code': 'invalid_id.mismatch'})

    def get_data_id(self, data):
        if len(self.id_fields) == 1:
            return data[self.id_fields[0]]

        return tuple(data[id_field] for id_field in self.id_fields)

    @request_cached_property
    def request_args(self):
        """Use args_schema to parse request query arguments."""
        args = flask.request.args
        data_raw = {}

        for field_name, field in self.args_schema.fields.items():
            alternate_field_name = field.load_from if MA2 else field.data_key

            if alternate_field_name and alternate_field_name in args:
                field_name = alternate_field_name
            elif field_name not in args:
                # getlist will return an empty list instead of raising a
                # KeyError for args that aren't present.
                continue

            value = args.getlist(field_name)
            if not self.is_list_field(field) and len(value) == 1:
                value = value[0]

            data_raw[field_name] = value

        return self.deserialize_args(data_raw)

    def is_list_field(self, field):
        return isinstance(field, fields.List)

    def deserialize_args(self, data_raw, **kwargs):
        try:
            data = schema_load(self.args_schema, data_raw, **kwargs)
        except ValidationError as e:
            raise ApiError(422, *(
{'fraction_non_alphanumeric': 0.10184392748774605, 'fraction_numerical': 0.005446199330895511, 'mean_word_length': 2.490089600868857, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1414171', 'n_tokens_mistral': 3717, 'n_tokens_neox': 3577, 'n_words': 897}