Пример #1
0
    def test_document_etag_ignore_fields(self):
        test = {"key1": "value1", "key2": "value2"}
        ignore_fields = ["key2"]
        test_without_ignore = {"key1": "value1"}
        challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8")
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields)
            )

        # not required fields can not be present
        test = {"key1": "value1", "key2": "value2"}
        ignore_fields = ["key3"]
        test_without_ignore = {"key1": "value1", "key2": "value2"}
        challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8")
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields)
            )

        # ignore fiels nested using doting notation
        test = {"key1": "value1", "dict": {"key2": "value2", "key3": "value3"}}
        ignore_fields = ["dict.key2"]
        test_without_ignore = {"key1": "value1", "dict": {"key3": "value3"}}
        challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8")
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields)
            )
Пример #2
0
    def test_document_etag_ignore_fields(self):
        test = {"key1": "value1", "key2": "value2"}
        ignore_fields = ["key2"]
        test_without_ignore = {"key1": "value1"}
        challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8")
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(),
                document_etag(test, ignore_fields))

        # not required fields can not be present
        test = {"key1": "value1", "key2": "value2"}
        ignore_fields = ["key3"]
        test_without_ignore = {"key1": "value1", "key2": "value2"}
        challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8")
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(),
                document_etag(test, ignore_fields))

        # ignore fiels nested using doting notation
        test = {"key1": "value1", "dict": {"key2": "value2", "key3": "value3"}}
        ignore_fields = ["dict.key2"]
        test_without_ignore = {"key1": "value1", "dict": {"key3": "value3"}}
        challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8")
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(),
                document_etag(test, ignore_fields))
Пример #3
0
    def test_document_etag_ignore_fields(self):
        test = {'key1': 'value1', 'key2': 'value2'}
        ignore_fields = ["key2"]
        test_without_ignore = {'key1': 'value1'}
        challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8')
        with self.app.test_request_context():
            self.assertEqual(hashlib.sha1(challenge).hexdigest(),
                             document_etag(test, ignore_fields))

        # not required fields can not be present
        test = {'key1': 'value1', 'key2': 'value2'}
        ignore_fields = ["key3"]
        test_without_ignore = {'key1': 'value1', 'key2': 'value2'}
        challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8')
        with self.app.test_request_context():
            self.assertEqual(hashlib.sha1(challenge).hexdigest(),
                             document_etag(test, ignore_fields))

        # ignore fiels nested using doting notation
        test = {'key1': 'value1', 'dict': {'key2': 'value2', 'key3': 'value3'}}
        ignore_fields = ['dict.key2']
        test_without_ignore = {'key1': 'value1', 'dict': {'key3': 'value3'}}
        challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8')
        with self.app.test_request_context():
            self.assertEqual(hashlib.sha1(challenge).hexdigest(),
                             document_etag(test, ignore_fields))
Пример #4
0
    def test_document_etag_ignore_fields(self):
        test = {'key1': 'value1', 'key2': 'value2'}
        ignore_fields = ["key2"]
        test_without_ignore = {'key1': 'value1'}
        challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8')
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(),
                document_etag(test, ignore_fields))

        # not required fields can not be present
        test = {'key1': 'value1', 'key2': 'value2'}
        ignore_fields = ["key3"]
        test_without_ignore = {'key1': 'value1', 'key2': 'value2'}
        challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8')
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(),
                document_etag(test, ignore_fields))

        # ignore fiels nested using doting notation
        test = {'key1': 'value1', 'dict': {'key2': 'value2', 'key3': 'value3'}}
        ignore_fields = ['dict.key2']
        test_without_ignore = {'key1': 'value1', 'dict': {'key3': 'value3'}}
        challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8')
        with self.app.test_request_context():
            self.assertEqual(
                hashlib.sha1(challenge).hexdigest(),
                document_etag(test, ignore_fields))
Пример #5
0
    def __publish_package_items(self, package, last_updated):
        """
        Publishes items of a package recursively

        :return: True if all the items of a package have been published successfully. False otherwise.
        """

        items = [ref.get('residRef') for group in package.get('groups', [])
                 for ref in group.get('refs', []) if 'residRef' in ref]

        if items:
            for guid in items:
                doc = super().find_one(req=None, _id=guid)
                original = copy(doc)
                try:
                    if doc['type'] == 'composite':
                        self.__publish_package_items(doc)

                    resolve_document_version(document=doc, resource=ARCHIVE, method='PATCH', latest_doc=doc)
                    doc[config.CONTENT_STATE] = 'published'
                    doc[config.LAST_UPDATED] = last_updated
                    doc[config.ETAG] = document_etag(doc)
                    self.backend.update(self.datasource, guid, {config.CONTENT_STATE: doc[config.CONTENT_STATE],
                                                                config.ETAG: doc[config.ETAG],
                                                                config.VERSION: doc[config.VERSION],
                                                                config.LAST_UPDATED: doc[config.LAST_UPDATED]},
                                        original)
                    insert_into_versions(doc=doc)
                except KeyError:
                    raise SuperdeskApiError.badRequestError("A non-existent content id is requested to publish")
Пример #6
0
    def insert(self, resource, doc_or_docs):
        """Called when performing POST request"""
        datasource, filter_, _, _ = self._datasource_ex(resource)
        try:
            if not isinstance(doc_or_docs, list):
                doc_or_docs = [doc_or_docs]

            ids = []
            for doc in doc_or_docs:
                model = self._doc_to_model(resource, doc)
                model.save(write_concern=self._wc(resource))
                ids.append(model.id)
                doc.update(dict(model.to_mongo()))
                doc[config.ID_FIELD] = model.id
                # Recompute ETag since MongoEngine can modify the data via
                # save hooks.
                clean_doc(doc)
                doc['_etag'] = document_etag(doc)
            return ids
        except pymongo.errors.OperationFailure as e:
            # most likely a 'w' (write_concern) setting which needs an
            # existing ReplicaSet which doesn't exist. Please note that the
            # update will actually succeed (a new ETag will be needed).
            abort(500, description=debug_error_message(
                'pymongo.errors.OperationFailure: %s' % e
            ))
        except Exception as exc:
            self._handle_exception(exc)
Пример #7
0
Файл: get.py Проект: marchon/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.
    """
    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)
Пример #8
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
Пример #9
0
    def insert(self, resource, doc_or_docs):
        """Called when performing POST request"""
        datasource, filter_, _, _ = self._datasource_ex(resource)
        try:
            if not isinstance(doc_or_docs, list):
                doc_or_docs = [doc_or_docs]

            ids = []
            for doc in doc_or_docs:
                model = self._doc_to_model(resource, doc)
                model.save(write_concern=self._wc(resource))
                ids.append(model.id)
                doc.update(dict(model.to_mongo()))
                doc[config.ID_FIELD] = model.id
                # Recompute ETag since MongoEngine can modify the data via
                # save hooks.
                clean_doc(doc)
                doc['_etag'] = document_etag(doc)
            return ids
        except pymongo.errors.OperationFailure as e:
            # most likely a 'w' (write_concern) setting which needs an
            # existing ReplicaSet which doesn't exist. Please note that the
            # update will actually succeed (a new ETag will be needed).
            abort(500,
                  description=debug_error_message(
                      'pymongo.errors.OperationFailure: %s' % e))
        except Exception as exc:
            self._handle_exception(exc)
Пример #10
0
    def __publish_package_items(self, package, last_updated):
        """
        Publishes items of a package recursively
        """

        items = [ref.get('residRef') for group in package.get('groups', [])
                 for ref in group.get('refs', []) if 'residRef' in ref]

        if items:
            for guid in items:
                doc = super().find_one(req=None, _id=guid)
                original = copy(doc)
                try:
                    if doc['type'] == 'composite':
                        self.__publish_package_items(doc)

                    resolve_document_version(document=doc, resource=ARCHIVE, method='PATCH', latest_doc=doc)
                    doc[config.CONTENT_STATE] = self.published_state
                    doc[config.LAST_UPDATED] = last_updated
                    doc[config.ETAG] = document_etag(doc)
                    self.backend.update(self.datasource, guid, {config.CONTENT_STATE: doc[config.CONTENT_STATE],
                                                                config.ETAG: doc[config.ETAG],
                                                                config.VERSION: doc[config.VERSION],
                                                                config.LAST_UPDATED: doc[config.LAST_UPDATED]},
                                        original)
                    insert_into_versions(doc=doc)
                except KeyError:
                    raise SuperdeskApiError.badRequestError("A non-existent content id is requested to publish")
Пример #11
0
def build_response_document(
        document, resource, embedded_fields, latest_doc=None):
    """ Prepares a document for response including generation of ETag and
    metadata fields.

    :param document: the document to embed other documents into.
    :param resource: the resource name.
    :param embedded_fields: the list of fields we are allowed to embed.
    :param document: the latest version of document.

    .. versionadded:: 0.4
    """
    # need to update the document field since the etag must be computed on the
    # same document representation that might have been used in the collection
    # 'get' method
    document[config.DATE_CREATED] = date_created(document)
    document[config.LAST_UPDATED] = last_updated(document)
    # TODO: last_update could include consideration for embedded documents

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

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

    # add version numbers
    resolve_document_version(document, resource, 'GET', latest_doc)

    # media and embedded documents
    resolve_media_files(document, resource)
    resolve_embedded_documents(document, resource, embedded_fields)
Пример #12
0
def get_document(resource, concurrency_check, **lookup):
    """ Retrieves and return a single document. Since this function is used by
    the editing methods (PUT, PATCH, DELETE), we make sure that the client
    request references the current representation of the document before
    returning it. However, this concurrency control may be turned off by
    internal functions. If resource enables soft delete, soft deleted documents
    will be returned, and must be handled by callers.

    :param resource: the name of the resource to which the document belongs to.
    :param concurrency_check: boolean check for concurrency control
    :param **lookup: document lookup query

    .. versionchanged:: 0.6
        Return soft deleted documents.

    .. versionchanged:: 0.5
       Concurrency control optional for internal functions.
       ETAG are now stored with the document (#369).

    .. versionchanged:: 0.0.9
       More informative error messages.

    .. versionchanged:: 0.0.5
      Pass current resource to ``parse_request``, allowing for proper
      processing of new configuration settings: `filters`, `sorting`, `paging`.
    """
    req = parse_request(resource)
    if config.DOMAIN[resource]['soft_delete']:
        # get_document should always fetch soft deleted documents from the db
        # callers must handle soft deleted documents
        req.show_deleted = True

    document = app.data.find_one(resource, req, **lookup)
    if document:
        e_if_m = config.ENFORCE_IF_MATCH
        if_m = config.IF_MATCH
        if not req.if_match and e_if_m and if_m and concurrency_check:
            # we don't allow editing unless the client provides an etag
            # for the document or explicitly decides to allow editing by either
            # disabling the ``concurrency_check`` or ``IF_MATCH`` or
            # ``ENFORCE_IF_MATCH`` fields.
            abort(428, description='To edit a document '
                  'its etag must be provided using the If-Match header')

        # ensure the retrieved document has LAST_UPDATED and DATE_CREATED,
        # eventually with same default values as in GET.
        document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        if req.if_match and concurrency_check:
            ignore_fields = config.DOMAIN[resource]['etag_ignore_fields']
            etag = document.get(config.ETAG, document_etag(document,
                                ignore_fields=ignore_fields))
            if req.if_match != etag:
                # client and server etags must match, or we don't allow editing
                # (ensures that client's version of the document is up to date)
                abort(412, description='Client and server etags don\'t match')

    return document
Пример #13
0
    def create_in_mongo(self, endpoint_name, docs, **kwargs):
        for doc in docs:
            doc.setdefault(config.ETAG, document_etag(doc))
            self.set_default_dates(doc)

        backend = self._backend(endpoint_name)
        ids = backend.insert(endpoint_name, docs)
        return ids
Пример #14
0
    def create_in_mongo(self, endpoint_name, docs, **kwargs):
        for doc in docs:
            doc.setdefault(config.ETAG, document_etag(doc))
            self.set_default_dates(doc)

        backend = self._backend(endpoint_name)
        ids = backend.insert(endpoint_name, docs)
        return ids
Пример #15
0
Файл: get.py Проект: 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)
Пример #16
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
Пример #17
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
Пример #18
0
 def fix_patch_etag(resource, request, payload):
     if self._etag_doc is None:
         return
     # make doc from which the etag will be computed
     etag_doc = clean_doc(self._etag_doc)
     # load the response back agagin from json
     d = json.loads(payload.get_data(as_text=True))
     # compute new etag
     d[config.ETAG] = document_etag(etag_doc)
     payload.set_data(json.dumps(d))
Пример #19
0
 def fix_patch_etag(resource, request, payload):
     if self._etag_doc is None:
         return
     # make doc from which the etag will be computed
     etag_doc = clean_doc(self._etag_doc)
     # load the response back agagin from json
     d = json.loads(payload.get_data(as_text=True))
     # compute new etag
     d[config.ETAG] = document_etag(etag_doc)
     payload.set_data(json.dumps(d))
Пример #20
0
def get_document(resource, concurrency_check, **lookup):
    """ Retrieves and return a single document. Since this function is used by
    the editing methods (PUT, PATCH, DELETE), we make sure that the client
    request references the current representation of the document before
    returning it. However, this concurrency control may be turned off by
    internal functions. If resource enables soft delete, soft deleted documents
    will be returned, and must be handled by callers.

    :param resource: the name of the resource to which the document belongs to.
    :param concurrency_check: boolean check for concurrency control
    :param **lookup: document lookup query

    .. versionchanged:: 0.6
        Return soft deleted documents.

    .. versionchanged:: 0.5
       Concurrency control optional for internal functions.
       ETAG are now stored with the document (#369).

    .. versionchanged:: 0.0.9
       More informative error messages.

    .. versionchanged:: 0.0.5
      Pass current resource to ``parse_request``, allowing for proper
      processing of new configuration settings: `filters`, `sorting`, `paging`.
    """
    req = parse_request(resource)
    if config.DOMAIN[resource]['soft_delete']:
        # get_document should always fetch soft deleted documents from the db
        # callers must handle soft deleted documents
        req.show_deleted = True

    document = app.data.find_one(resource, req, **lookup)
    if document:
        if not req.if_match and config.IF_MATCH and concurrency_check:
            # we don't allow editing unless the client provides an etag
            # for the document
            abort(428, description='To edit a document '
                  'its etag must be provided using the If-Match header')

        # ensure the retrieved document has LAST_UPDATED and DATE_CREATED,
        # eventually with same default values as in GET.
        document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        if req.if_match and concurrency_check:
            ignore_fields = config.DOMAIN[resource]['etag_ignore_fields']
            etag = document.get(config.ETAG, document_etag(document,
                                ignore_fields=ignore_fields))
            if req.if_match != etag:
                # client and server etags must match, or we don't allow editing
                # (ensures that client's version of the document is up to date)
                abort(412, description='Client and server etags don\'t match')

    return document
Пример #21
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)
Пример #22
0
 def find_one(self, req, **lookup):
     session_doc = super().find_one(req, **lookup)
     user_doc = get_resource_service('users').find_one(req=None, _id=session_doc['user'])
     self.enhance_document_with_default_prefs(session_doc, user_doc)
     self.enhance_document_with_user_privileges(session_doc, user_doc)
     if req is None:
         req = parse_request('auth')
         session_doc['_etag'] = req.if_match
     else:
         session_doc['_etag'] = document_etag(session_doc)
     return session_doc
Пример #23
0
def resolve_document_etag(documents):
    """ Adds etags to documents.

    .. versionadded:: 0.5
    """
    if config.IF_MATCH:
        if not isinstance(documents, list):
            documents = [documents]

        for document in documents:
            document[config.ETAG] = document_etag(document)
Пример #24
0
def resolve_document_etag(documents):
    """ Adds etags to documents.

    .. versionadded:: 0.5
    """
    if config.IF_MATCH:
        if not isinstance(documents, list):
            documents = [documents]

        for document in documents:
            document[config.ETAG] = document_etag(document)
Пример #25
0
 def find_one(self, req, **lookup):
     session_doc = super().find_one(req, **lookup)
     user_doc = get_resource_service('users').find_one(
         req=None, _id=session_doc['user'])
     self.enhance_document_with_default_prefs(session_doc, user_doc)
     self.enhance_document_with_user_privileges(session_doc, user_doc)
     if req is None:
         req = parse_request('auth')
         session_doc['_etag'] = req.if_match
     else:
         session_doc['_etag'] = document_etag(session_doc)
     return session_doc
Пример #26
0
    def create_in_mongo(self, endpoint_name, docs, **kwargs):
        """Create items in mongo.

        :param endpoint_name: resource name
        :param docs: list of docs to create
        """
        for doc in docs:
            doc.setdefault(config.ETAG, document_etag(doc))
            self.set_default_dates(doc)

        backend = self._backend(endpoint_name)
        ids = backend.insert(endpoint_name, docs)
        return ids
Пример #27
0
    def create_in_mongo(self, endpoint_name, docs, **kwargs):
        """Create items in mongo.

        :param endpoint_name: resource name
        :param docs: list of docs to create
        """
        for doc in docs:
            self.set_default_dates(doc)
            if not doc.get(config.ETAG):
                doc[config.ETAG] = document_etag(doc)

        backend = self._backend(endpoint_name)
        ids = backend.insert(endpoint_name, docs)
        return ids
Пример #28
0
def resolve_document_etag(documents, resource):
    """ Adds etags to documents.

    .. versionadded:: 0.5
    """
    if config.IF_MATCH:
        ignore_fields = config.DOMAIN[resource]['etag_ignore_fields']

        if not isinstance(documents, list):
            documents = [documents]

        for document in documents:
            document[config.ETAG] =\
                document_etag(document, ignore_fields=ignore_fields)
Пример #29
0
 def find_one(self, req, **lookup):
     session_doc = super().find_one(req, **lookup)
     if not session_doc:  # fetching old session preferences using new session
         return
     user_doc = get_resource_service('users').find_one(req=None, _id=session_doc['user'])
     self.enhance_document_with_default_prefs(session_doc, user_doc)
     self.enhance_document_with_user_privileges(session_doc, user_doc)
     session_doc[_action_key] = get_privileged_actions(session_doc[_privileges_key])
     if req is None:
         req = parse_request('auth')
         session_doc['_etag'] = req.if_match
     else:
         session_doc['_etag'] = document_etag(session_doc)
     return session_doc
def create_delayed(endpoint_name, docs, **kwargs):
    """Insert documents into given collection.
    :param endpoint_name: api resource name
    :param docs: list of docs to be inserted
    """

    search_backend = app.data._search_backend(endpoint_name)
    if not search_backend:
        return

    for doc in docs:
        doc.setdefault(app.config['ETAG'], document_etag(doc))

    search_backend.insert(endpoint_name, docs, **kwargs)
Пример #31
0
Файл: get.py Проект: marchon/eve
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
Пример #32
0
def resolve_document_etag(documents, resource):
    """ Adds etags to documents.

    .. versionadded:: 0.5
    """
    if config.IF_MATCH:
        ignore_fields = config.DOMAIN[resource]['etag_ignore_fields']

        if not isinstance(documents, list):
            documents = [documents]

        for document in documents:
            document[config.ETAG] =\
                document_etag(document, ignore_fields=ignore_fields)
Пример #33
0
def get_document(resource, concurrency_check, **lookup):
    """ Retrieves and return a single document. Since this function is used by
    the editing methods (POST, PATCH, DELETE), we make sure that the client
    request references the current representation of the document before
    returning it. However, this concurrency control may be turned off by
    internal functions.

    :param resource: the name of the resource to which the document belongs to.
    :param concurrency_check: boolean check for concurrency control
    :param **lookup: document lookup query

    .. versionchanged:: 0.5
       Concurrency control optional for internal functions.
       ETAG are now stored with the document (#369).

    .. versionchanged:: 0.0.9
       More informative error messages.

    .. versionchanged:: 0.0.5
      Pass current resource to ``parse_request``, allowing for proper
      processing of new configuration settings: `filters`, `sorting`, `paging`.
    """
    req = parse_request(resource)
    document = app.data.find_one(resource, None, **lookup)
    if document:

        if not req.if_match and config.IF_MATCH and concurrency_check:
            # we don't allow editing unless the client provides an etag
            # for the document
            abort(403,
                  description=debug_error_message(
                      'An etag must be provided to edit a document'))

        # ensure the retrieved document has LAST_UPDATED and DATE_CREATED,
        # eventually with same default values as in GET.
        document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        if req.if_match and concurrency_check:
            etag = document.get(config.ETAG, document_etag(document))
            if req.if_match != etag:
                # client and server etags must match, or we don't allow editing
                # (ensures that client's version of the document is up to date)
                abort(412,
                      description=debug_error_message(
                          'Client and server etags don\'t match'))

    return document
Пример #34
0
def get_document(resource, concurrency_check, **lookup):
    """ Retrieves and return a single document. Since this function is used by
    the editing methods (POST, PATCH, DELETE), we make sure that the client
    request references the current representation of the document before
    returning it. However, this concurrency control may be turned off by
    internal functions.

    :param resource: the name of the resource to which the document belongs to.
    :param concurrency_check: boolean check for concurrency control
    :param **lookup: document lookup query

    .. versionchanged:: 0.5
       Concurrency control optional for internal functions.
       ETAG are now stored with the document (#369).

    .. versionchanged:: 0.0.9
       More informative error messages.

    .. versionchanged:: 0.0.5
      Pass current resource to ``parse_request``, allowing for proper
      processing of new configuration settings: `filters`, `sorting`, `paging`.
    """
    req = parse_request(resource)
    document = app.data.find_one(resource, None, **lookup)
    if document:

        if not req.if_match and config.IF_MATCH and concurrency_check:
            # we don't allow editing unless the client provides an etag
            # for the document
            abort(403, description=debug_error_message(
                'An etag must be provided to edit a document'
            ))

        # ensure the retrieved document has LAST_UPDATED and DATE_CREATED,
        # eventually with same default values as in GET.
        document[config.LAST_UPDATED] = last_updated(document)
        document[config.DATE_CREATED] = date_created(document)

        if req.if_match and concurrency_check:
            etag = document.get(config.ETAG, document_etag(document))
            if req.if_match != etag:
                # client and server etags must match, or we don't allow editing
                # (ensures that client's version of the document is up to date)
                abort(412, description=debug_error_message(
                    'Client and server etags don\'t match'
                ))

    return document
Пример #35
0
    def create(self, endpoint_name, docs, **kwargs):
        """Insert documents into given collection.

        :param endpoint_name: api resource name
        :param docs: list of docs to be inserted
        """
        for doc in docs:
            doc.setdefault(app.config['ETAG'], document_etag(doc))
            self.set_default_dates(doc)

        backend = self._backend(endpoint_name)
        ids = backend.insert(endpoint_name, docs, **kwargs)
        search_backend = self._lookup_backend(endpoint_name)
        if search_backend:
            search_backend.insert(endpoint_name, docs, **kwargs)
        return ids
Пример #36
0
    def create(self, endpoint_name, docs, **kwargs):
        """Insert documents into given collection.

        :param endpoint_name: api resource name
        :param docs: list of docs to be inserted
        """
        for doc in docs:
            doc.setdefault(app.config['ETAG'], document_etag(doc))
            self.set_default_dates(doc)

        backend = self._backend(endpoint_name)
        ids = backend.insert(endpoint_name, docs, **kwargs)
        search_backend = self._lookup_backend(endpoint_name)
        if search_backend:
            search_backend.insert(endpoint_name, docs, **kwargs)
        return ids
Пример #37
0
 def find_one(self, req, **lookup):
     session_doc = super().find_one(req, **lookup)
     if not session_doc:  # fetching old session preferences using new session
         return
     user_doc = get_resource_service('users').find_one(
         req=None, _id=session_doc['user'])
     self.enhance_document_with_default_prefs(session_doc, user_doc)
     self.enhance_document_with_user_privileges(session_doc, user_doc)
     session_doc[_action_key] = get_privileged_actions(
         session_doc[_privileges_key])
     if req is None:
         req = parse_request('auth')
         session_doc['_etag'] = req.if_match
     else:
         session_doc['_etag'] = document_etag(session_doc)
     return session_doc
Пример #38
0
def build_response_document(document,
                            resource,
                            embedded_fields,
                            latest_doc=None):
    """ Prepares a document for response including generation of ETag and
    metadata fields.

    :param document: the document to embed other documents into.
    :param resource: the resource name.
    :param embedded_fields: the list of fields we are allowed to embed.
    :param document: the latest version of document.

    .. versionchanged:: 0.5
       Only compute ETAG if necessary (#369).
       Add version support (#475).

    .. versionadded:: 0.4
    """
    # need to update the document field since the etag must be computed on the
    # same document representation that might have been used in the collection
    # 'get' method
    document[config.DATE_CREATED] = date_created(document)
    document[config.LAST_UPDATED] = last_updated(document)
    # TODO: last_update could include consideration for embedded documents

    # Up to v0.4 etags were not stored with the documents.
    if config.IF_MATCH and config.ETAG not in document:
        document[config.ETAG] = document_etag(document)

    # hateoas links
    if config.DOMAIN[resource]['hateoas'] and config.ID_FIELD in document:
        version = None
        if config.DOMAIN[resource]['versioning'] is True \
                and request.args.get(config.VERSION_PARAM):
            version = document[config.VERSION]
        document[config.LINKS] = {
            'self': document_link(resource, document[config.ID_FIELD], version)
        }

    # add version numbers
    resolve_document_version(document, resource, 'GET', latest_doc)

    # media and embedded documents
    resolve_media_files(document, resource)
    resolve_embedded_documents(document, resource, embedded_fields)
Пример #39
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)
Пример #40
0
def build_response_document(
        document, resource, embedded_fields, latest_doc=None):
    """ Prepares a document for response including generation of ETag and
    metadata fields.

    :param document: the document to embed other documents into.
    :param resource: the resource name.
    :param embedded_fields: the list of fields we are allowed to embed.
    :param document: the latest version of document.

    .. versionchanged:: 0.5
       Only compute ETAG if necessary (#369).
       Add version support (#475).

    .. versionadded:: 0.4
    """
    # need to update the document field since the etag must be computed on the
    # same document representation that might have been used in the collection
    # 'get' method
    document[config.DATE_CREATED] = date_created(document)
    document[config.LAST_UPDATED] = last_updated(document)
    # TODO: last_update could include consideration for embedded documents

    # Up to v0.4 etags were not stored with the documents.
    if config.IF_MATCH and config.ETAG not in document:
        document[config.ETAG] = document_etag(document)

    # hateoas links
    if config.DOMAIN[resource]['hateoas'] and config.ID_FIELD in document:
        version = None
        if config.DOMAIN[resource]['versioning'] is True \
                and request.args.get(config.VERSION_PARAM):
            version = document[config.VERSION]
        document[config.LINKS] = {'self':
                                  document_link(resource,
                                                document[config.ID_FIELD],
                                                version)}

    # add version numbers
    resolve_document_version(document, resource, 'GET', latest_doc)

    # media and embedded documents
    resolve_media_files(document, resource)
    resolve_embedded_documents(document, resource, embedded_fields)
Пример #41
0
    def update(self, endpoint_name, id, updates):
        """Update document with given id.

        :param endpoint_name: api resource name
        :param id: document id
        :param updates: changes made to document
        """
        # change etag on update so following request will refetch it
        updates.setdefault(app.config['LAST_UPDATED'], utcnow())
        updates.setdefault(app.config['ETAG'], document_etag(updates))

        backend = self._backend(endpoint_name)
        res = backend.update(endpoint_name, id, updates)

        search_backend = self._lookup_backend(endpoint_name)
        if search_backend is not None:
            doc = backend.find_one(endpoint_name, req=None, _id=id)
            search_backend.update(endpoint_name, id, doc)

        return res if res is not None else updates
Пример #42
0
    def update(self, endpoint_name, id, updates):
        """Update document with given id.

        :param endpoint_name: api resource name
        :param id: document id
        :param updates: changes made to document
        """
        # change etag on update so following request will refetch it
        updates.setdefault(app.config['LAST_UPDATED'], utcnow())
        updates.setdefault(app.config['ETAG'], document_etag(updates))

        backend = self._backend(endpoint_name)
        res = backend.update(endpoint_name, id, updates)

        search_backend = self._lookup_backend(endpoint_name)
        if search_backend is not None:
            doc = backend.find_one(endpoint_name, req=None, _id=id)
            search_backend.update(endpoint_name, id, doc)

        return res if res is not None else updates
Пример #43
0
def build_response_document(document,
                            resource,
                            embedded_fields,
                            latest_doc=None):
    """ Prepares a document for response including generation of ETag and
    metadata fields.

    :param document: the document to embed other documents into.
    :param resource: the resource name.
    :param embedded_fields: the list of fields we are allowed to embed.
    :param document: the latest version of document.

    .. versionadded:: 0.4
    """
    # need to update the document field since the etag must be computed on the
    # same document representation that might have been used in the collection
    # 'get' method
    document[config.DATE_CREATED] = date_created(document)
    document[config.LAST_UPDATED] = last_updated(document)
    # TODO: last_update could include consideration for embedded documents

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

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

    # add version numbers
    resolve_document_version(document, resource, 'GET', latest_doc)

    # media and embedded documents
    resolve_media_files(document, resource)
    resolve_embedded_documents(document, resource, embedded_fields)
Пример #44
0
 def etag(self, doc):
     return doc.get(config.ETAG, document_etag(doc))
Пример #45
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
Пример #46
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
Пример #47
0
def build_response_document(
        document, resource, embedded_fields, latest_doc=None):
    """ Prepares a document for response including generation of ETag and
    metadata fields.

    :param document: the document to embed other documents into.
    :param resource: the resource name.
    :param embedded_fields: the list of fields we are allowed to embed.
    :param document: the latest version of document.

    .. versionchanged:: 0.5
       Only compute ETAG if necessary (#369).
       Add version support (#475).

    .. versionadded:: 0.4
    """
    resource_def = config.DOMAIN[resource]

    # need to update the document field since the etag must be computed on the
    # same document representation that might have been used in the collection
    # 'get' method
    document[config.DATE_CREATED] = date_created(document)
    document[config.LAST_UPDATED] = last_updated(document)

    # Up to v0.4 etags were not stored with the documents.
    if config.IF_MATCH and config.ETAG not in document:
        ignore_fields = resource_def['etag_ignore_fields']
        document[config.ETAG] = document_etag(document,
                                              ignore_fields=ignore_fields)

    # hateoas links
    if resource_def['hateoas'] and resource_def['id_field'] in document:
        version = None
        if resource_def['versioning'] is True \
                and request.args.get(config.VERSION_PARAM):
            version = document[config.VERSION]

        self_dict = {'self': document_link(resource,
                                           document[resource_def['id_field']],
                                           version)}
        if config.LINKS not in document:
            document[config.LINKS] = self_dict
        elif 'self' not in document[config.LINKS]:
            document[config.LINKS].update(self_dict)

    # add version numbers
    resolve_document_version(document, resource, 'GET', latest_doc)

    # resolve media
    resolve_media_files(document, resource)

    # resolve soft delete
    if resource_def['soft_delete'] is True:
        if document.get(config.DELETED) is None:
            document[config.DELETED] = False
        elif document[config.DELETED] is True:
            # Soft deleted documents are sent without expansion of embedded
            # documents. Return before resolving them.
            return

    # resolve embedded documents
    resolve_embedded_documents(document, resource, embedded_fields)
Пример #48
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
Пример #49
0
def build_response_document(document,
                            resource,
                            embedded_fields,
                            latest_doc=None):
    """ Prepares a document for response including generation of ETag and
    metadata fields.

    :param document: the document to embed other documents into.
    :param resource: the resource name.
    :param embedded_fields: the list of fields we are allowed to embed.
    :param document: the latest version of document.

    .. versionchanged:: 0.5
       Only compute ETAG if necessary (#369).
       Add version support (#475).

    .. versionadded:: 0.4
    """
    resource_def = config.DOMAIN[resource]

    # need to update the document field since the etag must be computed on the
    # same document representation that might have been used in the collection
    # 'get' method
    document[config.DATE_CREATED] = date_created(document)
    document[config.LAST_UPDATED] = last_updated(document)

    # Up to v0.4 etags were not stored with the documents.
    if config.IF_MATCH and config.ETAG not in document:
        ignore_fields = resource_def['etag_ignore_fields']
        document[config.ETAG] = document_etag(document,
                                              ignore_fields=ignore_fields)

    # hateoas links
    if resource_def['hateoas'] and resource_def['id_field'] in document:
        version = None
        if resource_def['versioning'] is True \
                and request.args.get(config.VERSION_PARAM):
            version = document[config.VERSION]

        self_dict = {
            'self':
            document_link(resource, document[resource_def['id_field']],
                          version)
        }
        if config.LINKS not in document:
            document[config.LINKS] = self_dict
        elif 'self' not in document[config.LINKS]:
            document[config.LINKS].update(self_dict)

    # add version numbers
    resolve_document_version(document, resource, 'GET', latest_doc)

    # resolve media
    resolve_media_files(document, resource)

    # resolve soft delete
    if resource_def['soft_delete'] is True:
        if document.get(config.DELETED) is None:
            document[config.DELETED] = False
        elif document[config.DELETED] is True:
            # Soft deleted documents are sent without expansion of embedded
            # documents. Return before resolving them.
            return

    # resolve embedded documents
    resolve_embedded_documents(document, resource, embedded_fields)
Пример #50
0
 def test_document_etag(self):
     test = {'key1': 'value1', 'another': 'value2'}
     challenge = dumps(test, sort_keys=True).encode('utf-8')
     self.assertEqual(
         hashlib.sha1(challenge).hexdigest(), document_etag(test))
Пример #51
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
Пример #52
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
Пример #53
0
 def etag(self, doc):
     return doc.get(config.ETAG, document_etag(doc))
Пример #54
0
Файл: get.py Проект: bcattle/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.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)
Пример #55
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
Пример #56
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
Пример #57
0
def generate_etag(resource: str, items: list):
    if resource in Device.resource_types:
        for item in items:
            item['_etag'] = document_etag(
                item, app.config['DOMAIN'][Naming.resource(
                    item['@type'])]['etag_ignore_fields'])