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) )
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))
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))
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))
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")
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)
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)
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
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)
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")
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)
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
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
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)
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
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
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))
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
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)
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
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)
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
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
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
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)
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)
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
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
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
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
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 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)
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)
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)
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
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)
def etag(self, doc): return doc.get(config.ETAG, document_etag(doc))
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
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
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)
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
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)
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))
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
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
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)
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
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
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'])