Beispiel #1
0
def get(resource):
    """Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    last_updated = datetime.min

    req = parse_request()
    cursor = app.data.find(resource, req)
    for document in cursor:
        # flask-pymongo returns timezone-aware value, we strip it out
        # because std lib datetime doesn't provide that, and comparisions
        # between the two values would fail

        # TODO consider testing if the app.data is of type Mongo before
        # replacing the tzinfo. On the other hand this could be handy for
        # other drivers as well (think of it as a safety measure). A
        # 'pythonic' alternative would be to perform the comparision in a
        # try..catch statement.. performing the replace in case of an
        # exception. However that would mean getting the exception at each
        # execution under standard circumstances (the default driver being
        # Mongo).
        document[config.LAST_UPDATED] = \
            document[config.LAST_UPDATED].replace(tzinfo=None)

        if document[config.LAST_UPDATED] > last_updated:
            last_updated = document[config.LAST_UPDATED]

        # document metadata
        document['etag'] = document_etag(document)
        document['_links'] = {'self': document_link(resource,
                                                    document[config.ID_FIELD])}

        documents.append(document)

    if req.if_modified_since and len(documents) == 0:
        # the if-modified-since conditional request returned no documents, we
        # send back a 304 Not-Modified, which means that the client already
        # has the up-to-date representation of the resultset.
        status = 304
        last_modified = None
    else:
        status = 200
        last_modified = last_updated if last_updated > datetime.min else None
        response['_items'] = documents
        response['_links'] = _pagination_links(resource, req, cursor.count())

    etag = None
    return response, last_modified, etag, status
Beispiel #2
0
def getitem(resource, **lookup):
    """ Retrieves and returns a single document.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: the lookup query.
    """
    response = dict()

    req = parse_request()
    document = app.data.find_one(resource, **lookup)
    if document:
        # need to update the document field as well since the etag must
        # be computed on the same document representation that might have
        # been used in the collection 'get' method
        last_modified = document[config.LAST_UPDATED] = \
            document[config.LAST_UPDATED].replace(tzinfo=None)
        etag = document_etag(document)

        if req.if_none_match and etag == req.if_none_match:
            # request etag matches the current server representation of the
            # document, return a 304 Not-Modified.
            return response, last_modified, etag, 304

        if req.if_modified_since and last_modified <= req.if_modified_since:
            # request If-Modified-Since conditional request match. We test
            # this after the etag since Last-Modified dates have lower
            # resolution (1 second).
            return response, last_modified, etag, 304

        document['link'] = document_link(resource, document[config.ID_FIELD])
        response[resource] = document
        response['links'] = standard_links(resource)
        return response, last_modified, etag, 200

    abort(404)
Beispiel #3
0
def post(resource):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    if len(request.form) == 0:
        abort(400)

    response = dict()
    date_utc = datetime.utcnow()

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    for key, value in request.form.items():

        response_item = dict()
        issues = list()

        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc
                document[config.ID_FIELD] = app.data.insert(resource, document)

                response_item[config.ID_FIELD] = document[config.ID_FIELD]
                response_item[config.LAST_UPDATED] = \
                    document[config.LAST_UPDATED]
                response_item['_links'] = \
                    {'self': document_link(resource,
                                           response_item[config.ID_FIELD])}

            else:
                issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            issues.append(str(e))

        if len(issues):
            response_item['issues'] = issues
            response_item['status'] = config.STATUS_ERR
        else:
            response_item['status'] = config.STATUS_OK

        response[key] = response_item

    return response, None, None, 200
Beispiel #4
0
Datei: get.py Projekt: klyr/eve
def getitem(resource, **lookup):
    """ Retrieves and returns a single document.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: the lookup query.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.6
        ETag added to payload.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Links wrapped with
       ``_links``. Links are now properly JSON formatted.
    """
    response = {}

    req = parse_request(resource)
    document = app.data.find_one(resource, **lookup)
    if document:
        # need to update the document field as well since the etag must
        # be computed on the same document representation that might have
        # been used in the collection 'get' method
        last_modified = document[config.LAST_UPDATED] = _last_updated(document)
        document["etag"] = document_etag(document)

        if req.if_none_match and document["etag"] == req.if_none_match:
            # request etag matches the current server representation of the
            # document, return a 304 Not-Modified.
            return response, last_modified, document["etag"], 304

        if req.if_modified_since and last_modified <= req.if_modified_since:
            # request If-Modified-Since conditional request match. We test
            # this after the etag since Last-Modified dates have lower
            # resolution (1 second).
            return response, last_modified, document["etag"], 304

        response["_links"] = {
            "self": document_link(resource, document[config.ID_FIELD]),
            "collection": collection_link(resource),
            "parent": home_link(),
        }
        response.update(document)
        return response, last_modified, document["etag"], 200

    abort(404)
Beispiel #5
0
def get(resource):
    """Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    last_updated = _epoch()

    req = parse_request(resource)
    cursor = app.data.find(resource, req)
    for document in cursor:
        document[config.LAST_UPDATED] = _last_updated(document)
        document[config.DATE_CREATED] = _date_created(document)

        if document[config.LAST_UPDATED] > last_updated:
            last_updated = document[config.LAST_UPDATED]

        # document metadata
        document['etag'] = document_etag(document)
        document['_links'] = {
            'self': document_link(resource, document[config.ID_FIELD])
        }

        documents.append(document)

    if req.if_modified_since and len(documents) == 0:
        # the if-modified-since conditional request returned no documents, we
        # send back a 304 Not-Modified, which means that the client already
        # has the up-to-date representation of the resultset.
        status = 304
        last_modified = None
    else:
        status = 200
        last_modified = last_updated if last_updated > _epoch() else None
        response['_items'] = documents
        response['_links'] = _pagination_links(resource, req, cursor.count())

    etag = None
    return response, last_modified, etag, status
Beispiel #6
0
def get(resource):
    """Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    last_updated = _epoch()

    req = parse_request(resource)
    cursor = app.data.find(resource, req)
    for document in cursor:
        document[config.LAST_UPDATED] = _last_updated(document)
        document[config.DATE_CREATED] = _date_created(document)

        if document[config.LAST_UPDATED] > last_updated:
            last_updated = document[config.LAST_UPDATED]

        # document metadata
        document['etag'] = document_etag(document)
        document['_links'] = {'self': document_link(resource,
                                                    document[config.ID_FIELD])}

        documents.append(document)

    if req.if_modified_since and len(documents) == 0:
        # the if-modified-since conditional request returned no documents, we
        # send back a 304 Not-Modified, which means that the client already
        # has the up-to-date representation of the resultset.
        status = 304
        last_modified = None
    else:
        status = 200
        last_modified = last_updated if last_updated > _epoch() else None
        response['_items'] = documents
        response['_links'] = _pagination_links(resource, req, cursor.count())

    etag = None
    return response, last_modified, etag, status
Beispiel #7
0
def getitem(resource, **lookup):
    """ Retrieves and returns a single document.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: the lookup query.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.6
        ETag added to payload.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Links wrapped with
       ``_links``. Links are now properly JSON formatted.
    """
    response = {}

    req = parse_request(resource)
    document = app.data.find_one(resource, **lookup)
    if document:
        # need to update the document field as well since the etag must
        # be computed on the same document representation that might have
        # been used in the collection 'get' method
        last_modified = document[config.LAST_UPDATED] = _last_updated(document)
        document['etag'] = document_etag(document)

        if req.if_none_match and document['etag'] == req.if_none_match:
            # request etag matches the current server representation of the
            # document, return a 304 Not-Modified.
            return response, last_modified, document['etag'], 304

        if req.if_modified_since and last_modified <= req.if_modified_since:
            # request If-Modified-Since conditional request match. We test
            # this after the etag since Last-Modified dates have lower
            # resolution (1 second).
            return response, last_modified, document['etag'], 304

        response['_links'] = {
            'self': document_link(resource, document[config.ID_FIELD]),
            'collection': collection_link(resource),
            'parent': home_link()
        }
        response.update(document)
        return response, last_modified, document['etag'], 200

    abort(404)
Beispiel #8
0
def post(resource):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.
    """

    if len(request.form) == 0:
        abort(400)

    response = dict()
    date_utc = datetime.utcnow()

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    for key, value in request.form.items():

        response_item = dict()
        issues = list()

        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc
                document[config.ID_FIELD] = app.data.insert(resource, document)

                response_item[config.ID_FIELD] = document[config.ID_FIELD]
                response_item[config.LAST_UPDATED] = \
                    document[config.LAST_UPDATED]
                response_item['link'] = \
                    document_link(resource, response_item[config.ID_FIELD])
            else:
                issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            issues.append(str(e))

        if len(issues):
            response_item['issues'] = issues
            response_item['status'] = config.STATUS_ERR
        else:
            response_item['status'] = config.STATUS_OK

        response[key] = response_item

    return response, None, None, 200
Beispiel #9
0
def get(resource):
    """Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.
    """
    documents = list()
    response = dict()
    last_updated = datetime.min

    req = parse_request()
    cursor = app.data.find(resource, req)
    for document in cursor:
        # flask-pymongo returns timezone-aware value, we strip it out
        # because std lib datetime doesn't provide that, and comparisions
        # between the two values would fail

        # TODO consider testing if the app.data is of type Mongo before
        # replacing the tzinfo. On the other hand this could be handy for
        # other drivers as well (think of it as a safety measure). A
        # 'pythonic' alternative would be to perform the comparision in a
        # try..catch statement.. performing the replace in case of an
        # exception. However that would mean getting the exception at each
        # execution under standard circumstances (the default driver being
        # Mongo).
        document[config.LAST_UPDATED] = \
            document[config.LAST_UPDATED].replace(tzinfo=None)

        if document[config.LAST_UPDATED] > last_updated:
            last_updated = document[config.LAST_UPDATED]

        # document metadata
        document['etag'] = document_etag(document)
        document['link'] = document_link(resource, document[config.ID_FIELD])

        documents.append(document)

    if req.if_modified_since and len(documents) == 0:
        # the if-modified-since conditional request returned no documents, we
        # send back a 304 Not-Modified, which means that the client already
        # has the up-to-date representation of the resultset.
        status = 304
        last_modified = None
    else:
        status = 200
        last_modified = last_updated if last_updated > datetime.min else None
        response[resource] = documents
        response['links'] = _pagination_links(resource, req, cursor.count())

    etag = None
    return response, last_modified, etag, status
Beispiel #10
0
def getitem(resource, **lookup):
    """ Retrieves and returns a single document.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: the lookup query.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Links wrapped with
       ``_links``. Links are now properly JSON formatted.
    """
    response = {}

    req = parse_request()
    document = app.data.find_one(resource, **lookup)
    if document:
        # need to update the document field as well since the etag must
        # be computed on the same document representation that might have
        # been used in the collection 'get' method
        last_modified = document[config.LAST_UPDATED] = \
            document[config.LAST_UPDATED].replace(tzinfo=None)
        etag = document_etag(document)

        if req.if_none_match and etag == req.if_none_match:
            # request etag matches the current server representation of the
            # document, return a 304 Not-Modified.
            return response, last_modified, etag, 304

        if req.if_modified_since and last_modified <= req.if_modified_since:
            # request If-Modified-Since conditional request match. We test
            # this after the etag since Last-Modified dates have lower
            # resolution (1 second).
            return response, last_modified, etag, 304

        response['_links'] = {
            'self': document_link(resource, document[config.ID_FIELD]),
            'collection': collection_link(resource),
            'parent': home_link()
        }
        response.update(document)
        return response, last_modified, etag, 200

    abort(404)
Beispiel #11
0
def getitem(resource, **lookup):
    """
    :param resource: the name of the resource to which the document belongs.
    :param **lookup: the lookup query.

    .. versionchanged:: 0.3
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.

    .. versionchanged:: 0.1.1
       Support for Embeded Resource Serialization.

    .. versionchanged:: 0.1.0
       Support for optional HATEOAS.

    .. versionchanged: 0.0.8
       'on_getting_item' event is raised when a document has been read from the
       database and is about to be sent to the client.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.6
        ETag added to payload.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Links wrapped with
       ``_links``. Links are now properly JSON formatted.
    """
    response = {}

    req = parse_request(resource)
    document = app.data.find_one(resource, **lookup)
    if document:
        # need to update the document field as well since the etag must
        # be computed on the same document representation that might have
        # been used in the collection 'get' method
        last_modified = document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        etag = None
        if config.IF_MATCH:
            etag = document[config.ETAG] = document_etag(document)

            if req.if_none_match and document[config.ETAG] == \
                    req.if_none_match:
                # request etag matches the current server representation of the
                # document, return a 304 Not-Modified.
                return response, last_modified, etag, 304

        if req.if_modified_since and last_modified <= req.if_modified_since:
            # request If-Modified-Since conditional request match. We test
            # this after the etag since Last-Modified dates have lower
            # resolution (1 second).
            return response, last_modified, document.get(config.ETAG), 304

        _resolve_embedded_documents(resource, req, [document])
        _resolve_media_files(document, resource)

        if config.DOMAIN[resource]['hateoas']:
            response[config.LINKS] = {
                'self': document_link(resource, document[config.ID_FIELD]),
                'collection': {'title':
                               config.DOMAIN[resource]['resource_title'],
                               'href': _collection_link(resource, True)},
                'parent': home_link()
            }

        # notify registered callback functions. Please note that, should the
        # functions modify the document, last_modified and etag  won't be
        # updated to reflect the changes (they always reflect the documents
        # state on the database).
        item_title = config.DOMAIN[resource]['item_title'].lower()

        getattr(app, "on_fetch_item")(resource, document[config.ID_FIELD],
                                      document)
        getattr(app, "on_fetch_item_%s" %
                item_title)(document[config.ID_FIELD], document)

        response.update(document)
        return response, last_modified, etag, 200

    abort(404)
Beispiel #12
0
def get(resource, lookup):
    """ Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged:: 0.3
       Don't return 304 if resource is empty. Fixes #243.
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       When If-Modified-Since header is present, either no documents (304) or
       all documents (200) are sent per the HTTP spec. Original behavior can be
       achieved with:
           /resource?where={"updated":{"$gt":"if-modified-since-date"}}

    .. versionchanged:: 0.2
       Use the new ITEMS configuration setting.
       Raise 'on_pre_<method>' event.
       Let cursor add extra info to response.

    .. versionchanged:: 0.1.0
       Support for optional HATEOAS.
       Support for embeddable documents.

    .. versionchanged:: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_getting'
       renamed to 'on_fetch'.

    .. versionchanged:: 0.0.8
       'on_getting' and 'on_getting_<resource>' events are raised when
       documents have been read from the database and are about to be sent to
       the client.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    etag = None
    req = parse_request(resource)

    if req.if_modified_since:
        # client has made this request before, has it changed?
        preflight_req = copy.copy(req)
        preflight_req.max_results = 1

        cursor = app.data.find(resource, preflight_req, lookup)
        if cursor.count() == 0:
            # make sure the datasource is not empty (#243).
            if not app.data.is_empty(resource):
                # the if-modified-since conditional request returned no
                # documents, we send back a 304 Not-Modified, which means that
                # the client already has the up-to-date representation of the
                # resultset.
                status = 304
                last_modified = None
                return response, last_modified, etag, status

    # continue processing the full request
    last_update = epoch()
    req.if_modified_since = None
    cursor = app.data.find(resource, req, lookup)

    for document in cursor:
        document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        if document[config.LAST_UPDATED] > last_update:
            last_update = document[config.LAST_UPDATED]

        # document metadata
        if config.IF_MATCH:
            document[config.ETAG] = document_etag(document)

        if config.DOMAIN[resource]['hateoas']:
            document[config.LINKS] = {'self':
                                      document_link(resource,
                                                    document[config.ID_FIELD])}

        _resolve_media_files(document, resource)

        documents.append(document)

    _resolve_embedded_documents(resource, req, documents)

    status = 200
    last_modified = last_update if last_update > epoch() else None

    # notify registered callback functions. Please note that, should the
    # functions modify the documents, the last_modified and etag won't be
    # updated to reflect the changes (they always reflect the documents
    # state on the database.)

    getattr(app, "on_fetch_resource")(resource, documents)
    getattr(app, "on_fetch_resource_%s" % resource)(documents)

    if config.DOMAIN[resource]['hateoas']:
        response[config.ITEMS] = documents
        response[config.LINKS] = _pagination_links(resource, req,
                                                   cursor.count())
    else:
        response = documents

    # the 'extra' cursor field, if present, will be added to the response.
    # Can be used by Eve extensions to add extra, custom data to any
    # response.
    if hasattr(cursor, 'extra'):
        getattr(cursor, 'extra')(response)

    return response, last_modified, etag, status
Beispiel #13
0
def get(resource):
    """Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_getting'
       renamed to 'on_fetch'.

    .. versionchanged: 0.0.8
       'on_getting' and 'on_getting_<resource>' events are raised when
       documents have been read from the database and are about to be sent to
       the client.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    last_updated = _epoch()

    req = parse_request(resource)
    cursor = app.data.find(resource, req)
    for document in cursor:
        document[config.LAST_UPDATED] = _last_updated(document)
        document[config.DATE_CREATED] = _date_created(document)

        if document[config.LAST_UPDATED] > last_updated:
            last_updated = document[config.LAST_UPDATED]

        # document metadata
        document['etag'] = document_etag(document)
        document['_links'] = {
            'self': document_link(resource, document[config.ID_FIELD])
        }

        documents.append(document)

    if req.if_modified_since and len(documents) == 0:
        # the if-modified-since conditional request returned no documents, we
        # send back a 304 Not-Modified, which means that the client already
        # has the up-to-date representation of the resultset.
        status = 304
        last_modified = None
    else:
        status = 200
        last_modified = last_updated if last_updated > _epoch() else None

        # notify registered callback functions. Please note that, should the
        # functions modify the documents, the last_modified and etag won't be
        # updated to reflect the changes (they always reflect the documents
        # state on the database.)

        getattr(app, "on_fetch_resource")(resource, documents)
        getattr(app, "on_fetch_resource_%s" % resource)(documents)

        response['_items'] = documents
        response['_links'] = _pagination_links(resource, req, cursor.count())

    etag = None
    return response, last_modified, etag, status
Beispiel #14
0
def patch(resource, **lookup):
    """Perform a document patch/update. Updates are first validated against
    the resource schema. If validation passes, the document is updated and
    an OK status update is returned. If validation fails, a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.
    """
    if len(request.form) > 1 or len(request.form) == 0:
        # only one update-per-document supported
        abort(400)

    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    object_id = original[config.ID_FIELD]
    last_modified = None
    etag = None

    issues = list()

    key = request.form.keys()[0]
    value = request.form[key]

    response_item = dict()

    try:
        updates = parse(value, resource)
        validation = validator.validate_update(updates, object_id)
        if validation:
            updates[config.LAST_UPDATED] = datetime.utcnow()
            app.data.update(resource, object_id, updates)

            # TODO computing etag without reloading the document
            # would be ideal. However, for reasons that need fruther
            # investigation, an etag computed on original.update(updates)
            # won't provide the same result as the saved document.
            # this has probably comething to do with a) the different
            # precision between the BSON (milliseconds) python datetime and,
            # b), the string representation of the documents (being dicts)
            # not matching.
            #
            # TL;DR: find a way to compute a reliable etag without reloading
            updated = app.data.find_one(resource,
                                        **{config.ID_FIELD: object_id})
            updated[config.LAST_UPDATED] = \
                updated[config.LAST_UPDATED].replace(tzinfo=None)
            etag = document_etag(updated)

            response_item[config.ID_FIELD] = object_id
            last_modified = response_item[config.LAST_UPDATED] = \
                updated[config.LAST_UPDATED]

            # metadata
            response_item['etag'] = etag
            response_item['link'] = document_link(resource, object_id)
        else:
            issues.extend(validator.errors)
    except ValidationError, e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
Beispiel #15
0
def patch(resource, **lookup):
    """Perform a document patch/update. Updates are first validated against
    the resource schema. If validation passes, the document is updated and
    an OK status update is returned. If validation fails, a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.1.1
       Item-identifier wrapper stripped from both request and response payload.

    .. versionchanged:: 0.1.0
       Support for optional HATEOAS.
       Re-raises `exceptions.Unauthorized`, this could occur if the
       `auth_field` condition fails

    .. versionchanged:: 0.0.9
       More informative error messages.
       Support for Python 3.3.

    .. versionchanged:: 0.0.8
       Let ``werkzeug.exceptions.InternalServerError`` go through as they have
       probably been explicitly raised by the data driver.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       ETag is now computed without the need of an additional db lookup

    .. versionchanged:: 0.0.5
       Support for 'aplication/json' Content-Type.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """
    payload = payload_()
    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)

    object_id = original[config.ID_FIELD]
    last_modified = None
    etag = None

    issues = []
    response = {}

    try:
        updates = parse(payload, resource)
        validation = validator.validate_update(updates, object_id)
        if validation:
            # the mongo driver has a different precision than the python
            # datetime. since we don't want to reload the document once it has
            # been updated, and we still have to provide an updated etag,
            # we're going to update the local version of the 'original'
            # document, and we will use it for the etag computation.
            original.update(updates)
            # some datetime precision magic
            updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = \
                datetime.utcnow().replace(microsecond=0)
            etag = document_etag(original)

            app.data.update(resource, object_id, updates)
            response[config.ID_FIELD] = object_id
            last_modified = response[config.LAST_UPDATED] = \
                original[config.LAST_UPDATED]

            # metadata
            response['etag'] = etag
            if resource_def['hateoas']:
                response['_links'] = {
                    'self': document_link(resource, object_id)
                }
        else:
            issues.extend(validator.errors)
    except ValidationError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
    except (exceptions.InternalServerError, exceptions.Unauthorized) as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        abort(400,
              description=debug_error_message('An exception occurred: %s' % e))

    if len(issues):
        response['issues'] = issues
        response['status'] = config.STATUS_ERR
    else:
        response['status'] = config.STATUS_OK

    return response, last_modified, etag, 200
Beispiel #16
0
def post(resource):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``reqiores_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    response = {}
    date_utc = datetime.utcnow()

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    for key, value in payload().items():

        response_item = {}
        issues = []

        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                # if 'user-restricted resource access' is enabled and there's
                # an Auth request active, inject the username into the document
                username_field = \
                    app.config['DOMAIN'][resource]['auth_username_field']
                if username_field and request.authorization:
                    document[username_field] = request.authorization.username

                document[config.ID_FIELD] = app.data.insert(resource, document)

                response_item[config.ID_FIELD] = document[config.ID_FIELD]
                response_item[config.LAST_UPDATED] = \
                    document[config.LAST_UPDATED]
                response_item['_links'] = \
                    {'self': document_link(resource,
                                           response_item[config.ID_FIELD])}

            else:
                issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            issues.append(str(e))

        if len(issues):
            response_item['issues'] = issues
            response_item['status'] = config.STATUS_ERR
        else:
            response_item['status'] = config.STATUS_OK

        response[key] = response_item

    return response, None, None, 200
Beispiel #17
0
def patch(resource, **lookup):
    """Perform a document patch/update. Updates are first validated against
    the resource schema. If validation passes, the document is updated and
    an OK status update is returned. If validation fails, a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.0.9
       More informative error messages.
       Support for Python 3.3.

    .. versionchanged:: 0.0.8
       Let ``werkzeug.exceptions.InternalServerError`` go through as they have
       probably been explicitly raised by the data driver.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       ETag is now computed without the need of an additional db lookup

    .. versionchanged:: 0.0.5
       Support for 'aplication/json' Content-Type.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """
    payload = payload_()
    if len(payload) > 1:
        # only one update-per-document supported
        abort(400, description=debug_error_message(
            'Only one update-per-document supported'
        ))

    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    object_id = original[config.ID_FIELD]
    last_modified = None
    etag = None

    issues = []

    # the list is needed for Py33. Yes kind of sucks.
    key = list(payload.keys())[0]
    value = payload[key]

    response_item = {}

    try:
        updates = parse(value, resource)
        validation = validator.validate_update(updates, object_id)
        if validation:
            # the mongo driver has a different precision than the python
            # datetime. since we don't want to reload the document once it has
            # been updated, and we still have to provide an updated etag,
            # we're going to update the local version of the 'original'
            # document, and we will use it for the etag computation.
            original.update(updates)
            # some datetime precision magic
            updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = \
                datetime.utcnow().replace(microsecond=0)
            etag = document_etag(original)

            app.data.update(resource, object_id, updates)
            response_item[config.ID_FIELD] = object_id
            last_modified = response_item[config.LAST_UPDATED] = \
                original[config.LAST_UPDATED]

            # metadata
            response_item['etag'] = etag
            response_item['_links'] = {'self': document_link(resource,
                                                             object_id)}
        else:
            issues.extend(validator.errors)
    except ValidationError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
    except exceptions.InternalServerError as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        abort(400, description=debug_error_message(
            'An exception occurred: %s' % e
        ))

    if len(issues):
        response_item['issues'] = issues
        response_item['status'] = config.STATUS_ERR
    else:
        response_item['status'] = config.STATUS_OK

    response = {}
    response[key] = response_item
    return response, last_modified, etag, 200
Beispiel #18
0
def put(resource, **lookup):
    """ Perform a document replacement. Updates are first validated against
    the resource schema. If validation passes, the document is repalced and
    an OK status update is returned. If validation fails a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.3
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise pre_<method> event.
       explictly resolve default values instead of letting them be resolved
       by common.parse. This avoids a validation error when a read-only field
       also has a default value.

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.
       Item-identifier wrapper stripped from both request and response payload.

    .. versionadded:: 0.1.0
    """
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)

    payload = payload_()
    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    last_modified = None
    etag = None
    issues = {}
    object_id = original[config.ID_FIELD]

    response = {}

    try:
        document = parse(payload, resource)
        validation = validator.validate_replace(document, object_id)
        if validation:
            last_modified = datetime.utcnow().replace(microsecond=0)
            document[config.LAST_UPDATED] = last_modified
            document[config.DATE_CREATED] = original[config.DATE_CREATED]

            # ID_FIELD not in document means it is not being automatically
            # handled (it has been set to a field which exists in the resource
            # schema.
            if config.ID_FIELD not in document:
                document[config.ID_FIELD] = object_id

            resolve_user_restricted_access(document, resource)
            resolve_default_values(document, resource)
            resolve_media_files(document, resource, original)

            # notify callbacks
            getattr(app, "on_insert")(resource, [document])
            getattr(app, "on_insert_%s" % resource)([document])

            app.data.replace(resource, object_id, document)

            response[config.ID_FIELD] = document.get(config.ID_FIELD,
                                                     object_id)
            response[config.LAST_UPDATED] = last_modified

            # metadata
            if config.IF_MATCH:
                etag = response[config.ETAG] = document_etag(document)
            if resource_def['hateoas']:
                response[config.LINKS] = {
                    'self': document_link(resource, response[config.ID_FIELD])
                }
        else:
            issues = validator.errors
    except ValidationError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues['validator exception'] = str(e)
    except exceptions.InternalServerError as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        abort(400,
              description=debug_error_message('An exception occurred: %s' % e))

    if len(issues):
        response[config.ISSUES] = issues
        response[config.STATUS] = config.STATUS_ERR
    else:
        response[config.STATUS] = config.STATUS_OK

    return response, last_modified, etag, 200
Beispiel #19
0
def get(resource, lookup):
    """ Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged:: 0.3
       Don't return 304 if resource is empty. Fixes #243.
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       When If-Modified-Since header is present, either no documents (304) or
       all documents (200) are sent per the HTTP spec. Original behavior can be
       achieved with:
           /resource?where={"updated":{"$gt":"if-modified-since-date"}}

    .. versionchanged:: 0.2
       Use the new ITEMS configuration setting.
       Raise 'on_pre_<method>' event.
       Let cursor add extra info to response.

    .. versionchanged:: 0.1.0
       Support for optional HATEOAS.
       Support for embeddable documents.

    .. versionchanged:: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_getting'
       renamed to 'on_fetch'.

    .. versionchanged:: 0.0.8
       'on_getting' and 'on_getting_<resource>' events are raised when
       documents have been read from the database and are about to be sent to
       the client.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    etag = None
    req = parse_request(resource)

    if req.if_modified_since:
        # client has made this request before, has it changed?
        preflight_req = copy.copy(req)
        preflight_req.max_results = 1

        cursor = app.data.find(resource, preflight_req, lookup)
        if cursor.count() == 0:
            # make sure the datasource is not empty (#243).
            if not app.data.is_empty(resource):
                # the if-modified-since conditional request returned no
                # documents, we send back a 304 Not-Modified, which means that
                # the client already has the up-to-date representation of the
                # resultset.
                status = 304
                last_modified = None
                return response, last_modified, etag, status

    # continue processing the full request
    last_update = epoch()
    req.if_modified_since = None
    cursor = app.data.find(resource, req, lookup)

    for document in cursor:
        document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        if document[config.LAST_UPDATED] > last_update:
            last_update = document[config.LAST_UPDATED]

        # document metadata
        if config.IF_MATCH:
            document[config.ETAG] = document_etag(document)

        if config.DOMAIN[resource]['hateoas']:
            document[config.LINKS] = {
                'self': document_link(resource, document[config.ID_FIELD])
            }

        _resolve_media_files(document, resource)

        documents.append(document)

    _resolve_embedded_documents(resource, req, documents)

    status = 200
    last_modified = last_update if last_update > epoch() else None

    # notify registered callback functions. Please note that, should the
    # functions modify the documents, the last_modified and etag won't be
    # updated to reflect the changes (they always reflect the documents
    # state on the database.)

    getattr(app, "on_fetch_resource")(resource, documents)
    getattr(app, "on_fetch_resource_%s" % resource)(documents)

    if config.DOMAIN[resource]['hateoas']:
        response[config.ITEMS] = documents
        response[config.LINKS] = _pagination_links(resource, req,
                                                   cursor.count())
    else:
        response = documents

    # the 'extra' cursor field, if present, will be added to the response.
    # Can be used by Eve extensions to add extra, custom data to any
    # response.
    if hasattr(cursor, 'extra'):
        getattr(cursor, 'extra')(response)

    return response, last_modified, etag, status
Beispiel #20
0
def put(resource, **lookup):
    """Perform a document replacement. Updates are first validated against
    the resource schema. If validation passes, the document is repalced and
    an OK status update is returned. If validation fails a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.
       Item-identifier wrapper stripped from both request and response payload.

    .. versionadded:: 0.1.0
    """
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)

    payload = payload_()
    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    last_modified = None
    etag = None
    issues = []
    object_id = original[config.ID_FIELD]

    response = {}

    try:
        document = parse(payload, resource)
        validation = validator.validate_replace(document, object_id)
        if validation:
            last_modified = datetime.utcnow().replace(microsecond=0)
            document[config.ID_FIELD] = object_id
            document[config.LAST_UPDATED] = last_modified
            # TODO what do we need here: the original creation date or the
            # PUT date? Going for the former seems reasonable.
            document[config.DATE_CREATED] = original[config.DATE_CREATED]

            # if 'user-restricted resource access' is enabled and there's
            # an Auth request active, inject the username into the document
            auth_field = resource_def['auth_field']
            if app.auth and auth_field:
                request_auth_value = app.auth.request_auth_value
                if request_auth_value and request.authorization:
                    document[auth_field] = request_auth_value
            etag = document_etag(document)

            # notify callbacks
            getattr(app, "on_insert")(resource, [document])
            getattr(app, "on_insert_%s" % resource)([document])

            app.data.replace(resource, object_id, document)

            response[config.ID_FIELD] = object_id
            response[config.LAST_UPDATED] = last_modified

            # metadata
            response['etag'] = etag
            if resource_def['hateoas']:
                response['_links'] = {'self': document_link(resource,
                                                            object_id)}
        else:
            issues.extend(validator.errors)
    except ValidationError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
    except exceptions.InternalServerError as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        abort(400, description=debug_error_message(
            'An exception occurred: %s' % e
        ))

    if len(issues):
        response['issues'] = issues
        response['status'] = config.STATUS_ERR
    else:
        response['status'] = config.STATUS_OK

    return response, last_modified, etag, 200
Beispiel #21
0
def getitem(resource, **lookup):
    """
    :param resource: the name of the resource to which the document belongs.
    :param **lookup: the lookup query.

    .. versionchanged:: 0.3
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.

    .. versionchanged:: 0.1.1
       Support for Embeded Resource Serialization.

    .. versionchanged:: 0.1.0
       Support for optional HATEOAS.

    .. versionchanged: 0.0.8
       'on_getting_item' event is raised when a document has been read from the
       database and is about to be sent to the client.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.6
        ETag added to payload.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Links wrapped with
       ``_links``. Links are now properly JSON formatted.
    """
    response = {}

    req = parse_request(resource)
    document = app.data.find_one(resource, **lookup)
    if document:
        # need to update the document field as well since the etag must
        # be computed on the same document representation that might have
        # been used in the collection 'get' method
        last_modified = document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        etag = None
        if config.IF_MATCH:
            etag = document[config.ETAG] = document_etag(document)

            if req.if_none_match and document[config.ETAG] == \
                    req.if_none_match:
                # request etag matches the current server representation of the
                # document, return a 304 Not-Modified.
                return response, last_modified, etag, 304

        if req.if_modified_since and last_modified <= req.if_modified_since:
            # request If-Modified-Since conditional request match. We test
            # this after the etag since Last-Modified dates have lower
            # resolution (1 second).
            return response, last_modified, document.get(config.ETAG), 304

        _resolve_embedded_documents(resource, req, [document])
        _resolve_media_files(document, resource)

        if config.DOMAIN[resource]['hateoas']:
            response[config.LINKS] = {
                'self': document_link(resource, document[config.ID_FIELD]),
                'collection': {
                    'title': config.DOMAIN[resource]['resource_title'],
                    'href': _collection_link(resource, True)
                },
                'parent': home_link()
            }

        # notify registered callback functions. Please note that, should the
        # functions modify the document, last_modified and etag  won't be
        # updated to reflect the changes (they always reflect the documents
        # state on the database).
        item_title = config.DOMAIN[resource]['item_title'].lower()

        getattr(app, "on_fetch_item")(resource, document[config.ID_FIELD],
                                      document)
        getattr(app,
                "on_fetch_item_%s" % item_title)(document[config.ID_FIELD],
                                                 document)

        response.update(document)
        return response, last_modified, etag, 200

    abort(404)
Beispiel #22
0
def post(resource, payl=None):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.
    :param payl: alternative payload. When calling post() from your own code
                 you can provide an alternative payload This can be useful, for
                 example, when you have a callback function hooked to a certain
                 endpoint, and want to perform additional post() calls from
                 there.

                 Please be advised that in order to successfully use this
                 option, a request context must be available.

                 See https://github.com/nicolaiarocci/eve/issues/74 for a
                 discussion, and a typical use case.

    .. versionchanged:: 0.3
       Return 201 if at least one document has been successfully inserted.
       Fix #231 auth field not set if resource level authentication is set.
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise 'on_pre_<method>' event.
       Explictly resolve default values instead of letting them be resolved
       by common.parse. This avoids a validation error when a read-only field
       also has a default value.

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.

    .. versionchanged:: 0.1.0
       More robust handling of auth_field.
       Support for optional HATEOAS.

    .. versionchanged: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_posting'
       renamed to 'on_insert'.
       You can now pass a pre-defined custom payload to the funcion.

    .. versionchanged:: 0.0.9
       Storing self.app.auth.userid in auth_field when 'user-restricted
       resource access' is enabled.

    .. versionchanged: 0.0.7
       Support for Rate-Limiting.
       Support for 'extra_response_fields'.

       'on_posting' and 'on_posting_<resource>' events are raised before the
       documents are inserted into the database. This allows callback functions
       to arbitrarily edit/update the documents being stored.

    .. versionchanged:: 0.0.6
       Support for bulk inserts.

       Please note: validation constraints are checked against the database,
       and not between the payload documents themselves. This causes an
       interesting corner case: in the event of a multiple documents payload
       where two or more documents carry the same value for a field where the
       'unique' constraint is set, the payload will validate successfully, as
       there are no duplicates in the database (yet). If this is an issue, the
       client can always send the documents once at a time for insertion, or
       validate locally before submitting the payload to the API.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    date_utc = datetime.utcnow().replace(microsecond=0)
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)
    documents = []
    issues = []

    # validation, and additional fields
    if payl is None:
        payl = payload()

    if isinstance(payl, dict):
        payl = [payl]

    for value in payl:
        document = []
        doc_issues = {}
        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                # validation is successful
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                resolve_user_restricted_access(document, resource)
                resolve_default_values(document, resource)
                resolve_media_files(document, resource)
            else:
                # validation errors added to list of document issues
                doc_issues = validator.errors
        except ValidationError as e:
            doc_issues['validation exception'] = str(e)
        except Exception as e:
            # most likely a problem with the incoming payload, report back to
            # the client as if it was a validation issue
            doc_issues['exception'] = str(e)

        issues.append(doc_issues)

        if len(doc_issues) == 0:
            documents.append(document)

    if len(documents):
        # notify callbacks
        getattr(app, "on_insert")(resource, documents)
        getattr(app, "on_insert_%s" % resource)(documents)
        # bulk insert
        ids = app.data.insert(resource, documents)
        # request was received and accepted; at least one document passed
        # validation and was accepted for insertion.
        return_code = 201
    else:
        # request was received and accepted; no document passed validation
        # though.
        return_code = 200

    # build response payload
    response = []
    for doc_issues in issues:
        item = {}
        if len(doc_issues):
            item[config.STATUS] = config.STATUS_ERR
            item[config.ISSUES] = doc_issues
        else:
            item[config.STATUS] = config.STATUS_OK

            document = documents.pop(0)
            item[config.LAST_UPDATED] = document[config.LAST_UPDATED]

            # either return the custom ID_FIELD or the id returned by
            # data.insert().
            item[config.ID_FIELD] = document.get(config.ID_FIELD, ids.pop(0))

            if config.IF_MATCH:
                item[config.ETAG] = document_etag(document)

            if resource_def['hateoas']:
                item[config.LINKS] = \
                    {'self': document_link(resource, item[config.ID_FIELD])}

            # add any additional field that might be needed
            allowed_fields = [
                x for x in resource_def['extra_response_fields']
                if x in document.keys()
            ]
            for field in allowed_fields:
                item[field] = document[field]

        response.append(item)

    if len(response) == 1:
        response = response.pop(0)

    return response, None, None, return_code
Beispiel #23
0
def post(resource, payl=None):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.
    :param payl: alternative payload. When calling post() from your own code
                 you can provide an alternative payload This can be useful, for
                 example, when you have a callback function hooked to a certain
                 endpoint, and want to perform additional post() calls from
                 there.

                 Please be advised that in order to successfully use this
                 option, a request context must be available.

                 See https://github.com/nicolaiarocci/eve/issues/74 for a
                 discussion, and a typical use case.

    .. versionchanged:: 0.3
       Return 201 if at least one document has been successfully inserted.
       Fix #231 auth field not set if resource level authentication is set.
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise 'on_pre_<method>' event.
       Explictly resolve default values instead of letting them be resolved
       by common.parse. This avoids a validation error when a read-only field
       also has a default value.

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.

    .. versionchanged:: 0.1.0
       More robust handling of auth_field.
       Support for optional HATEOAS.

    .. versionchanged: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_posting'
       renamed to 'on_insert'.
       You can now pass a pre-defined custom payload to the funcion.

    .. versionchanged:: 0.0.9
       Storing self.app.auth.userid in auth_field when 'user-restricted
       resource access' is enabled.

    .. versionchanged: 0.0.7
       Support for Rate-Limiting.
       Support for 'extra_response_fields'.

       'on_posting' and 'on_posting_<resource>' events are raised before the
       documents are inserted into the database. This allows callback functions
       to arbitrarily edit/update the documents being stored.

    .. versionchanged:: 0.0.6
       Support for bulk inserts.

       Please note: validation constraints are checked against the database,
       and not between the payload documents themselves. This causes an
       interesting corner case: in the event of a multiple documents payload
       where two or more documents carry the same value for a field where the
       'unique' constraint is set, the payload will validate successfully, as
       there are no duplicates in the database (yet). If this is an issue, the
       client can always send the documents once at a time for insertion, or
       validate locally before submitting the payload to the API.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    date_utc = datetime.utcnow().replace(microsecond=0)
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)
    documents = []
    issues = []

    # validation, and additional fields
    if payl is None:
        payl = payload()

    if isinstance(payl, dict):
        payl = [payl]

    for value in payl:
        document = []
        doc_issues = {}
        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                # validation is successful
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                resolve_user_restricted_access(document, resource)
                resolve_default_values(document, resource)
                resolve_media_files(document, resource)
            else:
                # validation errors added to list of document issues
                doc_issues = validator.errors
        except ValidationError as e:
            doc_issues['validation exception'] = str(e)
        except Exception as e:
            # most likely a problem with the incoming payload, report back to
            # the client as if it was a validation issue
            doc_issues['exception'] = str(e)

        issues.append(doc_issues)

        if len(doc_issues) == 0:
            documents.append(document)

    if len(documents):
        # notify callbacks
        getattr(app, "on_insert")(resource, documents)
        getattr(app, "on_insert_%s" % resource)(documents)
        # bulk insert
        ids = app.data.insert(resource, documents)
        # request was received and accepted; at least one document passed
        # validation and was accepted for insertion.
        return_code = 201
    else:
        # request was received and accepted; no document passed validation
        # though.
        return_code = 200

    # build response payload
    response = []
    for doc_issues in issues:
        item = {}
        if len(doc_issues):
            item[config.STATUS] = config.STATUS_ERR
            item[config.ISSUES] = doc_issues
        else:
            item[config.STATUS] = config.STATUS_OK

            document = documents.pop(0)
            item[config.LAST_UPDATED] = document[config.LAST_UPDATED]

            # either return the custom ID_FIELD or the id returned by
            # data.insert().
            item[config.ID_FIELD] = document.get(config.ID_FIELD, ids.pop(0))

            if config.IF_MATCH:
                item[config.ETAG] = document_etag(document)

            if resource_def['hateoas']:
                item[config.LINKS] = \
                    {'self': document_link(resource, item[config.ID_FIELD])}

            # add any additional field that might be needed
            allowed_fields = [x for x in resource_def['extra_response_fields']
                              if x in document.keys()]
            for field in allowed_fields:
                item[field] = document[field]

        response.append(item)

    if len(response) == 1:
        response = response.pop(0)

    return response, None, None, return_code
Beispiel #24
0
def put(resource, **lookup):
    """ Perform a document replacement. Updates are first validated against
    the resource schema. If validation passes, the document is repalced and
    an OK status update is returned. If validation fails a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.3
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise pre_<method> event.
       explictly resolve default values instead of letting them be resolved
       by common.parse. This avoids a validation error when a read-only field
       also has a default value.

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.
       Item-identifier wrapper stripped from both request and response payload.

    .. versionadded:: 0.1.0
    """
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)

    payload = payload_()
    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    last_modified = None
    etag = None
    issues = {}
    object_id = original[config.ID_FIELD]

    response = {}

    try:
        document = parse(payload, resource)
        validation = validator.validate_replace(document, object_id)
        if validation:
            last_modified = datetime.utcnow().replace(microsecond=0)
            document[config.ID_FIELD] = object_id
            document[config.LAST_UPDATED] = last_modified
            # TODO what do we need here: the original creation date or the
            # PUT date? Going for the former seems reasonable.
            document[config.DATE_CREATED] = original[config.DATE_CREATED]

            resolve_user_restricted_access(document, resource)
            resolve_default_values(document, resource)
            resolve_media_files(document, resource, original)

            # notify callbacks
            getattr(app, "on_insert")(resource, [document])
            getattr(app, "on_insert_%s" % resource)([document])

            app.data.replace(resource, object_id, document)

            response[config.ID_FIELD] = object_id
            response[config.LAST_UPDATED] = last_modified

            # metadata
            if config.IF_MATCH:
                etag = response[config.ETAG] = document_etag(document)
            if resource_def['hateoas']:
                response[config.LINKS] = {'self': document_link(resource,
                                                                object_id)}
        else:
            issues = validator.errors
    except ValidationError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues['validator exception'] = str(e)
    except exceptions.InternalServerError as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        abort(400, description=debug_error_message(
            'An exception occurred: %s' % e
        ))

    if len(issues):
        response[config.ISSUES] = issues
        response[config.STATUS] = config.STATUS_ERR
    else:
        response[config.STATUS] = config.STATUS_OK

    return response, last_modified, etag, 200
Beispiel #25
0
def get(resource):
    """Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_getting'
       renamed to 'on_fetch'.

    .. versionchanged: 0.0.8
       'on_getting' and 'on_getting_<resource>' events are raised when
       documents have been read from the database and are about to be sent to
       the client.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    last_updated = _epoch()

    req = parse_request(resource)
    cursor = app.data.find(resource, req)
    for document in cursor:
        document[config.LAST_UPDATED] = _last_updated(document)
        document[config.DATE_CREATED] = _date_created(document)

        if document[config.LAST_UPDATED] > last_updated:
            last_updated = document[config.LAST_UPDATED]

        # document metadata
        document["etag"] = document_etag(document)
        document["_links"] = {"self": document_link(resource, document[config.ID_FIELD])}

        documents.append(document)

    if req.if_modified_since and len(documents) == 0:
        # the if-modified-since conditional request returned no documents, we
        # send back a 304 Not-Modified, which means that the client already
        # has the up-to-date representation of the resultset.
        status = 304
        last_modified = None
    else:
        status = 200
        last_modified = last_updated if last_updated > _epoch() else None

        # notify registered callback functions. Please note that, should the
        # functions modify the documents, the last_modified and etag won't be
        # updated to reflect the changes (they always reflect the documents
        # state on the database.)

        getattr(app, "on_fetch_resource")(resource, documents)
        getattr(app, "on_fetch_resource_%s" % resource)(documents)

        response["_items"] = documents
        response["_links"] = _pagination_links(resource, req, cursor.count())

    etag = None
    return response, last_modified, etag, status
Beispiel #26
0
def post(resource, payl=None):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.
    :param payl: alternative payload. When calling post() from your own code
                 you can provide an alternative payload This can be useful, for
                 example, when you have a callback function hooked to a certain
                 endpoint, and want to perform additional post() calls from
                 there.

                 Please be advised that in order to successfully use this
                 option, a request context must be available.

                 See https://github.com/nicolaiarocci/eve/issues/74 for a
                 discussion, and a typical use case.

    .. versionchanged:: 0.1.1
        auth.request_auth_value is now used to store the auth_field value.

    .. versionchanged:: 0.1.0
       More robust handling of auth_field.
       Support for optional HATEOAS.

    .. versionchanged: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_posting'
       renamed to 'on_insert'.
       You can now pass a pre-defined custom payload to the funcion.

    .. versionchanged:: 0.0.9
       Storing self.app.auth.userid in auth_field when 'user-restricted
       resource access' is enabled.

    .. versionchanged: 0.0.7
       Support for Rate-Limiting.
       Support for 'extra_response_fields'.

       'on_posting' and 'on_posting_<resource>' events are raised before the
       documents are inserted into the database. This allows callback functions
       to arbitrarily edit/update the documents being stored.

    .. versionchanged:: 0.0.6
       Support for bulk inserts.

       Please note: validation constraints are checked against the database,
       and not between the payload documents themselves. This causes an
       interesting corner case: in the event of a multiple documents payload
       where two or more documents carry the same value for a field where the
       'unique' constraint is set, the payload will validate successfully, as
       there are no duplicates in the database (yet). If this is an issue, the
       client can always send the documents once at a time for insertion, or
       validate locally before submitting the payload to the API.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    date_utc = datetime.utcnow().replace(microsecond=0)
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)
    documents = []
    issues = []

    # validation, and additional fields
    if payl is None:
        payl = payload()

    if isinstance(payl, dict):
        payl = [payl]

    for value in payl:
        document = []
        doc_issues = []
        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                # validation is successful
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                # if 'user-restricted resource access' is enabled
                # and there's an Auth request active,
                # inject the auth_field into the document
                auth_field = resource_def['auth_field']
                if app.auth and auth_field:
                    request_auth_value = app.auth.request_auth_value
                    if request_auth_value and request.authorization:
                        document[auth_field] = request_auth_value
            else:
                # validation errors added to list of document issues
                doc_issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            # most likely a problem with the incoming payload, report back to
            # the client as if it was a validation issue
            doc_issues.append(str(e))

        issues.append(doc_issues)

        if len(doc_issues) == 0:
            documents.append(document)

    if len(documents):
        # notify callbacks
        getattr(app, "on_insert")(resource, documents)
        getattr(app, "on_insert_%s" % resource)(documents)
        # bulk insert
        ids = app.data.insert(resource, documents)

    # build response payload
    response = []
    for doc_issues in issues:
        response_item = {}
        if len(doc_issues):
            response_item['status'] = config.STATUS_ERR
            response_item['issues'] = doc_issues
        else:
            response_item['status'] = config.STATUS_OK
            response_item[config.ID_FIELD] = ids.pop(0)
            document = documents.pop(0)
            response_item[config.LAST_UPDATED] = document[config.LAST_UPDATED]
            response_item['etag'] = document_etag(document)
            if resource_def['hateoas']:
                response_item['_links'] = \
                    {'self': document_link(resource,
                                           response_item[config.ID_FIELD])}

            # add any additional field that might be needed
            allowed_fields = [
                x for x in resource_def['extra_response_fields']
                if x in document.keys()
            ]
            for field in allowed_fields:
                response_item[field] = document[field]

        response.append(response_item)

    if len(response) == 1:
        response = response.pop(0)

    return response, None, None, 200
Beispiel #27
0
def patch(resource, **lookup):
    """Perform a document patch/update. Updates are first validated against
    the resource schema. If validation passes, the document is updated and
    an OK status update is returned. If validation fails, a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.
    """
    if len(request.form) > 1 or len(request.form) == 0:
        # only one update-per-document supported
        abort(400)

    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    object_id = original[config.ID_FIELD]
    last_modified = None
    etag = None

    issues = list()

    key = request.form.keys()[0]
    value = request.form[key]

    response_item = dict()

    try:
        updates = parse(value, resource)
        validation = validator.validate_update(updates, object_id)
        if validation:
            updates[config.LAST_UPDATED] = datetime.utcnow()
            app.data.update(resource, object_id, updates)

            # TODO computing etag without reloading the document
            # would be ideal. However, for reasons that need fruther
            # investigation, an etag computed on original.update(updates)
            # won't provide the same result as the saved document.
            # this has probably comething to do with a) the different
            # precision between the BSON (milliseconds) python datetime and,
            # b), the string representation of the documents (being dicts)
            # not matching.
            #
            # TL;DR: find a way to compute a reliable etag without reloading
            updated = app.data.find_one(resource,
                                        **{config.ID_FIELD: object_id})
            updated[config.LAST_UPDATED] = \
                updated[config.LAST_UPDATED].replace(tzinfo=None)
            etag = document_etag(updated)

            response_item[config.ID_FIELD] = object_id
            last_modified = response_item[config.LAST_UPDATED] = \
                updated[config.LAST_UPDATED]

            # metadata
            response_item['etag'] = etag
            response_item['link'] = document_link(resource, object_id)
        else:
            issues.extend(validator.errors)
    except ValidationError, e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
Beispiel #28
0
def get(resource, lookup):
    """ Retrieves the resource documents that match the current request.

    :param resource: the name of the resource.

    .. versionchanged:: 0.2
       Use the new ITEMS configuration setting.
       Raise 'on_pre_<method>' event.
       Let cursor add extra info to response.

    .. versionchanged:: 0.1.0
       Support for optional HATEOAS.
       Support for embeddable documents.

    .. versionchanged:: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_getting'
       renamed to 'on_fetch'.

    .. versionchanged:: 0.0.8
       'on_getting' and 'on_getting_<resource>' events are raised when
       documents have been read from the database and are about to be sent to
       the client.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Collection items wrapped
       with ``_items``. Links wrapped with ``_links``. Links are now properly
       JSON formatted.
    """

    documents = []
    response = {}
    last_update = epoch()

    req = parse_request(resource)
    cursor = app.data.find(resource, req, lookup)

    for document in cursor:
        document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        if document[config.LAST_UPDATED] > last_update:
            last_update = document[config.LAST_UPDATED]

        # document metadata
        document['etag'] = document_etag(document)
        if config.DOMAIN[resource]['hateoas']:
            document[config.LINKS] = {'self':
                                      document_link(resource,
                                                    document[config.ID_FIELD])}

        documents.append(document)

    _resolve_embedded_documents(resource, req, documents)

    if req.if_modified_since and len(documents) == 0:
        # the if-modified-since conditional request returned no documents, we
        # send back a 304 Not-Modified, which means that the client already
        # has the up-to-date representation of the resultset.
        status = 304
        last_modified = None
    else:
        status = 200
        last_modified = last_update if last_update > epoch() else None

        # notify registered callback functions. Please note that, should the
        # functions modify the documents, the last_modified and etag won't be
        # updated to reflect the changes (they always reflect the documents
        # state on the database.)

        getattr(app, "on_fetch_resource")(resource, documents)
        getattr(app, "on_fetch_resource_%s" % resource)(documents)

        if config.DOMAIN[resource]['hateoas']:
            response[config.ITEMS] = documents
            response[config.LINKS] = _pagination_links(resource, req,
                                                       cursor.count())
        else:
            response = documents

        # the 'extra' cursor field, if present, will be added to the response.
        # Can be used by Eve extensions to add extra, custom data to any
        # response.
        if hasattr(cursor, 'extra'):
            getattr(cursor, 'extra')(response)

    etag = None
    return response, last_modified, etag, status
Beispiel #29
0
def patch(resource, **lookup):
    """Perform a document patch/update. Updates are first validated against
    the resource schema. If validation passes, the document is updated and
    an OK status update is returned. If validation fails, a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.0.8
       Let ``werkzeug.exceptions.InternalServerError`` go through as they have
       probably been explicitly raised by the data driver.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       ETag is now computed without the need of an additional db lookup

    .. versionchanged:: 0.0.5
       Support for 'aplication/json' Content-Type.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """
    payload = payload_()
    if len(payload) > 1:
        # only one update-per-document supported
        abort(400)

    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    object_id = original[config.ID_FIELD]
    last_modified = None
    etag = None

    issues = []

    key = payload.keys()[0]
    value = payload[key]

    response_item = {}

    try:
        updates = parse(value, resource)
        validation = validator.validate_update(updates, object_id)
        if validation:
            # the mongo driver has a different precision than the python
            # datetime. since we don't want to reload the document once it has
            # been updated, and we still have to provide an updated etag,
            # we're going to update the local version of the 'original'
            # document, and we will use it for the etag computation.
            original.update(updates)
            # some datetime precision magic
            updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = \
                datetime.utcnow().replace(microsecond=0)
            etag = document_etag(original)

            app.data.update(resource, object_id, updates)
            response_item[config.ID_FIELD] = object_id
            last_modified = response_item[config.LAST_UPDATED] = \
                original[config.LAST_UPDATED]

            # metadata
            response_item['etag'] = etag
            response_item['_links'] = {
                'self': document_link(resource, object_id)
            }
        else:
            issues.extend(validator.errors)
    except ValidationError, e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
Beispiel #30
0
def post(resource):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.

    .. versionchanged:: 0.0.6
        Support for bulk inserts.
        Please note: validation constraints are checked against the database,
        and not between the payload documents themselves. This causes an
        interesting corner case: in the event of a multiple documents payload
        where two or more documents carry the same value for a field where the
        'unique' constraint is set, the payload will validate successfully, as
        there are no duplicates in the database (yet). If this is an issue, the
        client can always send the documents once at a time for insertion, or
        validate locally before submitting the payload to the API.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    date_utc = datetime.utcnow().replace(microsecond=0)

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)
    documents = []
    issues = []

    # validation, and additional fields
    payl = payload()
    for key, value in payl.items():
        document = []
        doc_issues = []
        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                # validation is successful
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                # if 'user-restricted resource access' is enabled and there's
                # an Auth request active, inject the username into the document
                username_field = \
                    app.config['DOMAIN'][resource]['auth_username_field']
                if username_field and request.authorization:
                    document[username_field] = request.authorization.username

            else:
                # validation errors added to list of document issues
                doc_issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            # most likely a problem with the incoming payload, report back to
            # the client as if it was a validation issue
            doc_issues.append(str(e))

        issues.append(doc_issues)

        if len(doc_issues) == 0:
            documents.append(document)

    # bulk insert
    if len(documents):
        ids = app.data.insert(resource, documents)

    # build response payload
    response = {}
    for key, doc_issues in zip(payl.keys(), issues):
        response_item = {}
        if len(doc_issues):
            response_item['status'] = config.STATUS_ERR
            response_item['issues'] = doc_issues
        else:
            response_item['status'] = config.STATUS_OK
            response_item[config.ID_FIELD] = ids.pop(0)
            document = documents.pop(0)
            response_item[config.LAST_UPDATED] = document[config.LAST_UPDATED]
            response_item['etag'] = document_etag(document)
            response_item['_links'] = \
                {'self': document_link(resource,
                                       response_item[config.ID_FIELD])}

        response[key] = response_item

    return response, None, None, 200
Beispiel #31
0
def post(resource):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.

    .. versionchanged:: 0.0.6
        Support for bulk inserts.
        Please note: validation constraints are checked against the database,
        and not between the payload documents themselves. This causes an
        interesting corner case: in the event of a multiple documents payload
        where two or more documents carry the same value for a field where the
        'unique' constraint is set, the payload will validate successfully, as
        there are no duplicates in the database (yet). If this is an issue, the
        client can always send the documents once at a time for insertion, or
        validate locally before submitting the payload to the API.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    date_utc = datetime.utcnow().replace(microsecond=0)

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)
    documents = []
    issues = []

    # validation, and additional fields
    payl = payload()
    for key, value in payl.items():
        document = []
        doc_issues = []
        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                # validation is successful
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                # if 'user-restricted resource access' is enabled and there's
                # an Auth request active, inject the username into the document
                username_field = \
                    app.config['DOMAIN'][resource]['auth_username_field']
                if username_field and request.authorization:
                    document[username_field] = request.authorization.username

            else:
                # validation errors added to list of document issues
                doc_issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            # most likely a problem with the incoming payload, report back to
            # the client as if it was a validation issue
            doc_issues.append(str(e))

        issues.append(doc_issues)

        if len(doc_issues) == 0:
            documents.append(document)

    # bulk insert
    if len(documents):
        ids = app.data.insert(resource, documents)

    # build response payload
    response = {}
    for key, doc_issues in zip(payl.keys(), issues):
        response_item = {}
        if len(doc_issues):
            response_item['status'] = config.STATUS_ERR
            response_item['issues'] = doc_issues
        else:
            response_item['status'] = config.STATUS_OK
            response_item[config.ID_FIELD] = ids.pop(0)
            document = documents.pop(0)
            response_item[config.LAST_UPDATED] = document[config.LAST_UPDATED]
            response_item['etag'] = document_etag(document)
            response_item['_links'] = \
                {'self': document_link(resource,
                                       response_item[config.ID_FIELD])}

        response[key] = response_item

    return response, None, None, 200
Beispiel #32
0
def patch(resource, **lookup):
    """ Perform a document patch/update. Updates are first validated against
    the resource schema. If validation passes, the document is updated and
    an OK status update is returned. If validation fails, a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.3
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise 'on_pre_<method>' event.

    .. versionchanged:: 0.1.1
       Item-identifier wrapper stripped from both request and response payload.

    .. versionchanged:: 0.1.0
       Support for optional HATEOAS.
       Re-raises `exceptions.Unauthorized`, this could occur if the
       `auth_field` condition fails

    .. versionchanged:: 0.0.9
       More informative error messages.
       Support for Python 3.3.

    .. versionchanged:: 0.0.8
       Let ``werkzeug.exceptions.InternalServerError`` go through as they have
       probably been explicitly raised by the data driver.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       ETag is now computed without the need of an additional db lookup

    .. versionchanged:: 0.0.5
       Support for 'aplication/json' Content-Type.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """
    payload = payload_()
    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)

    object_id = original[config.ID_FIELD]
    last_modified = None
    etag = None

    issues = {}
    response = {}

    try:
        updates = parse(payload, resource)
        validation = validator.validate_update(updates, object_id)
        if validation:
            resolve_media_files(updates, resource, original)

            # the mongo driver has a different precision than the python
            # datetime. since we don't want to reload the document once it has
            # been updated, and we still have to provide an updated etag,
            # we're going to update the local version of the 'original'
            # document, and we will use it for the etag computation.
            original.update(updates)

            # some datetime precision magic
            updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = \
                datetime.utcnow().replace(microsecond=0)

            app.data.update(resource, object_id, updates)

            response[config.ID_FIELD] = original[config.ID_FIELD]
            last_modified = response[config.LAST_UPDATED] = \
                original[config.LAST_UPDATED]

            # metadata
            if config.IF_MATCH:
                etag = response[config.ETAG] = document_etag(original)
            if resource_def['hateoas']:
                response[config.LINKS] = {
                    'self': document_link(resource, original[config.ID_FIELD])
                }

        else:
            issues = validator.errors
    except ValidationError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues['validator exception'] = str(e)
    except (exceptions.InternalServerError, exceptions.Unauthorized) as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        abort(400, description=debug_error_message(
            'An exception occurred: %s' % e
        ))

    if len(issues):
        response[config.ISSUES] = issues
        response[config.STATUS] = config.STATUS_ERR
    else:
        response[config.STATUS] = config.STATUS_OK

    return response, last_modified, etag, 200
Beispiel #33
0
Datei: patch.py Projekt: klyr/eve
def patch(resource, **lookup):
    """Perform a document patch/update. Updates are first validated against
    the resource schema. If validation passes, the document is updated and
    an OK status update is returned. If validation fails, a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
        ETag is now computed without the need of an additional db lookup

    .. versionchanged:: 0.0.5
        Support for 'aplication/json' Content-Type.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """
    payload = payload_()
    if len(payload) > 1:
        # only one update-per-document supported
        abort(400)

    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    schema = app.config["DOMAIN"][resource]["schema"]
    validator = app.validator(schema, resource)

    object_id = original[config.ID_FIELD]
    last_modified = None
    etag = None

    issues = []

    key = payload.keys()[0]
    value = payload[key]

    response_item = {}

    try:
        updates = parse(value, resource)
        validation = validator.validate_update(updates, object_id)
        if validation:
            # the mongo driver has a different precision than the python
            # datetime. since we don't want to reload the document once it has
            # been updated, and we still have to provide an updated etag,
            # we're going to update the local version of the 'original'
            # document, and we will use it for the etag computation.
            original.update(updates)
            # some datetime precision magic
            updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = datetime.utcnow().replace(microsecond=0)
            etag = document_etag(original)

            app.data.update(resource, object_id, updates)
            response_item[config.ID_FIELD] = object_id
            last_modified = response_item[config.LAST_UPDATED] = original[config.LAST_UPDATED]

            # metadata
            response_item["etag"] = etag
            response_item["_links"] = {"self": document_link(resource, object_id)}
        else:
            issues.extend(validator.errors)
    except ValidationError, e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
Beispiel #34
0
def getitem(resource, **lookup):
    """ Retrieves and returns a single document.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: the lookup query.

    .. versionchanged: 0.0.8
       'on_getting_item' event is raised when a document has been read from the
       database and is about to be sent to the client.

    .. versionchanged:: 0.0.7
       Support for Rate-Limiting.

    .. versionchanged:: 0.0.6
       Support for HEAD requests.

    .. versionchanged:: 0.0.6
        ETag added to payload.

    .. versionchanged:: 0.0.5
       Support for user-restricted access to resources.
       Support for LAST_UPDATED field missing from documents, because they were
       created outside the API context.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       Superflous ``response`` container removed. Links wrapped with
       ``_links``. Links are now properly JSON formatted.
    """
    response = {}

    req = parse_request(resource)
    document = app.data.find_one(resource, **lookup)
    if document:
        # need to update the document field as well since the etag must
        # be computed on the same document representation that might have
        # been used in the collection 'get' method
        last_modified = document[config.LAST_UPDATED] = _last_updated(document)
        document['etag'] = document_etag(document)

        if req.if_none_match and document['etag'] == req.if_none_match:
            # request etag matches the current server representation of the
            # document, return a 304 Not-Modified.
            return response, last_modified, document['etag'], 304

        if req.if_modified_since and last_modified <= req.if_modified_since:
            # request If-Modified-Since conditional request match. We test
            # this after the etag since Last-Modified dates have lower
            # resolution (1 second).
            return response, last_modified, document['etag'], 304

        response['_links'] = {
            'self': document_link(resource, document[config.ID_FIELD]),
            'collection': collection_link(resource),
            'parent': home_link()
        }

        # notify registered callback functions. Please note that, should the
        # functions modify the document, last_modified and etag  won't be
        # updated to reflect the changes (they always reflect the documents
        # state on the database).
        getattr(app, "on_getting_item")(resource, document[config.ID_FIELD],
                                        document)

        response.update(document)
        return response, last_modified, document['etag'], 200

    abort(404)
Beispiel #35
0
def post(resource):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``reqiores_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    response = {}
    date_utc = datetime.utcnow()

    schema = app.config['DOMAIN'][resource]['schema']
    validator = app.validator(schema, resource)

    for key, value in payload().items():

        response_item = {}
        issues = []

        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                # if 'user-restricted resource access' is enabled and there's
                # an Auth request active, inject the username into the document
                username_field = \
                    app.config['DOMAIN'][resource]['auth_username_field']
                if username_field and request.authorization:
                    document[username_field] = request.authorization.username

                document[config.ID_FIELD] = app.data.insert(resource, document)

                response_item[config.ID_FIELD] = document[config.ID_FIELD]
                response_item[config.LAST_UPDATED] = \
                    document[config.LAST_UPDATED]
                response_item['_links'] = \
                    {'self': document_link(resource,
                                           response_item[config.ID_FIELD])}

            else:
                issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            issues.append(str(e))

        if len(issues):
            response_item['issues'] = issues
            response_item['status'] = config.STATUS_ERR
        else:
            response_item['status'] = config.STATUS_OK

        response[key] = response_item

    return response, None, None, 200
Beispiel #36
0
def post(resource, payl=None):
    """ Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.
    :param payl: alternative payload. When calling post() from your own code
                 you can provide an alternative payload This can be useful, for
                 example, when you have a callback function hooked to a certain
                 endpoint, and want to perform additional post() calls from
                 there.

                 Please be advised that in order to successfully use this
                 option, a request context must be available.

                 See https://github.com/nicolaiarocci/eve/issues/74 for a
                 discussion, and a typical use case.

    .. versionchanged:: 0.1.0
       More robust handling of auth_field.
       Support for optional HATEOAS.

    .. versionchanged: 0.0.9
       Event hooks renamed to be more robuts and consistent: 'on_posting'
       renamed to 'on_insert'.
       You can now pass a pre-defined custom payload to the funcion.

    .. versionchanged:: 0.0.9
       Storing self.app.auth.userid in auth_field when 'user-restricted
       resource access' is enabled.

    .. versionchanged: 0.0.7
       Support for Rate-Limiting.
       Support for 'extra_response_fields'.

       'on_posting' and 'on_posting_<resource>' events are raised before the
       documents are inserted into the database. This allows callback functions
       to arbitrarily edit/update the documents being stored.

    .. versionchanged:: 0.0.6
       Support for bulk inserts.

       Please note: validation constraints are checked against the database,
       and not between the payload documents themselves. This causes an
       interesting corner case: in the event of a multiple documents payload
       where two or more documents carry the same value for a field where the
       'unique' constraint is set, the payload will validate successfully, as
       there are no duplicates in the database (yet). If this is an issue, the
       client can always send the documents once at a time for insertion, or
       validate locally before submitting the payload to the API.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    date_utc = datetime.utcnow().replace(microsecond=0)
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)
    documents = []
    issues = []

    # validation, and additional fields
    if payl is None:
        payl = payload()
    for key, value in payl.items():
        document = []
        doc_issues = []
        try:
            document = parse(value, resource)
            validation = validator.validate(document)
            if validation:
                # validation is successful
                document[config.LAST_UPDATED] = \
                    document[config.DATE_CREATED] = date_utc

                # if 'user-restricted resource access' is enabled
                # and there's an Auth request active,
                # inject the auth_field into the document
                auth_field = resource_def['auth_field']
                if auth_field:
                    auth_field_value = getattr(app.auth, auth_field)
                    if auth_field_value and request.authorization:
                        document[auth_field] = auth_field_value

            else:
                # validation errors added to list of document issues
                doc_issues.extend(validator.errors)
        except ValidationError as e:
            raise e
        except Exception as e:
            # most likely a problem with the incoming payload, report back to
            # the client as if it was a validation issue
            doc_issues.append(str(e))

        issues.append(doc_issues)

        if len(doc_issues) == 0:
            documents.append(document)

    if len(documents):
        # notify callbacks
        getattr(app, "on_insert")(resource, documents)
        getattr(app, "on_insert_%s" % resource)(documents)
        # bulk insert
        ids = app.data.insert(resource, documents)

    # build response payload
    response = {}
    for key, doc_issues in zip(payl.keys(), issues):
        response_item = {}
        if len(doc_issues):
            response_item['status'] = config.STATUS_ERR
            response_item['issues'] = doc_issues
        else:
            response_item['status'] = config.STATUS_OK
            response_item[config.ID_FIELD] = ids.pop(0)
            document = documents.pop(0)
            response_item[config.LAST_UPDATED] = document[config.LAST_UPDATED]
            response_item['etag'] = document_etag(document)
            if resource_def['hateoas']:
                response_item['_links'] = \
                    {'self': document_link(resource,
                                           response_item[config.ID_FIELD])}

            # add any additional field that might be needed
            allowed_fields = [x for x in resource_def['extra_response_fields']
                              if x in document.keys()]
            for field in allowed_fields:
                response_item[field] = document[field]

        response[key] = response_item

    return response, None, None, 200
Beispiel #37
0
def put(resource, **lookup):
    """Perform a document replacement. Updates are first validated against
    the resource schema. If validation passes, the document is repalced and
    an OK status update is returned. If validation fails a set of validation
    issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param **lookup: document lookup query.

    .. versionadded:: 0.1.0
    """
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = app.validator(schema, resource)

    payload = payload_()
    if len(payload) > 1:
        abort(400, description=debug_error_message(
            'Only one update-per-document supported'))

    original = get_document(resource, **lookup)
    if not original:
        # not found
        abort(404)

    last_modified = None
    etag = None
    issues = []
    object_id = original[config.ID_FIELD]

    # TODO the list is needed for Py33. Find a less ridiculous alternative?
    key = list(payload.keys())[0]
    document = payload[key]

    response_item = {}

    try:
        document = parse(document, resource)
        validation = validator.validate_replace(document, object_id)
        if validation:
            last_modified = datetime.utcnow().replace(microsecond=0)
            document[config.ID_FIELD] = object_id
            document[config.LAST_UPDATED] = last_modified
            # TODO what do we need here: the original creation date or the
            # PUT date? Going for the former seems reasonable.
            document[config.DATE_CREATED] = original[config.DATE_CREATED]

            # if 'user-restricted resource access' is enabled and there's
            # an Auth request active, inject the username into the document
            auth_field = resource_def['auth_field']
            if auth_field:
                userid = app.auth.user_id
                if userid and request.authorization:
                    document[auth_field] = userid
            etag = document_etag(document)

            app.data.replace(resource, object_id, document)

            response_item[config.ID_FIELD] = object_id
            response_item[config.LAST_UPDATED] = last_modified

            # metadata
            response_item['etag'] = etag
            if resource_def['hateoas']:
                response_item['_links'] = {'self': document_link(resource,
                                                                 object_id)}
        else:
            issues.extend(validator.errors)
    except ValidationError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues.append(str(e))
    except exceptions.InternalServerError as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        abort(400, description=debug_error_message(
            'An exception occurred: %s' % e
        ))

    if len(issues):
        response_item['issues'] = issues
        response_item['status'] = config.STATUS_ERR
    else:
        response_item['status'] = config.STATUS_OK

    response = {}
    response[key] = response_item
    return response, last_modified, etag, 200