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 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 post(resource): """ Adds one or more documents to a resource. Each document is validated against the domain schema. If validation passes the document is inserted and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the document are returned. If validation fails, a list of validation issues is returned. :param resource: name of the resource involved. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ if len(request.form) == 0: abort(400) response = dict() date_utc = datetime.utcnow() schema = app.config['DOMAIN'][resource]['schema'] validator = app.validator(schema, resource) for key, value in request.form.items(): response_item = dict() issues = list() try: document = parse(value, resource) validation = validator.validate(document) if validation: document[config.LAST_UPDATED] = \ document[config.DATE_CREATED] = date_utc document[config.ID_FIELD] = app.data.insert(resource, document) response_item[config.ID_FIELD] = document[config.ID_FIELD] response_item[config.LAST_UPDATED] = \ document[config.LAST_UPDATED] response_item['_links'] = \ {'self': document_link(resource, response_item[config.ID_FIELD])} else: issues.extend(validator.errors) except ValidationError as e: raise e except Exception as e: issues.append(str(e)) if len(issues): response_item['issues'] = issues response_item['status'] = config.STATUS_ERR else: response_item['status'] = config.STATUS_OK response[key] = response_item return response, None, None, 200
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 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 post(resource): """ Adds one or more documents to a resource. Each document is validated against the domain schema. If validation passes the document is inserted and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the document are returned. If validation fails, a list of validation issues is returned. :param resource: name of the resource involved. """ if len(request.form) == 0: abort(400) response = dict() date_utc = datetime.utcnow() schema = app.config['DOMAIN'][resource]['schema'] validator = app.validator(schema, resource) for key, value in request.form.items(): response_item = dict() issues = list() try: document = parse(value, resource) validation = validator.validate(document) if validation: document[config.LAST_UPDATED] = \ document[config.DATE_CREATED] = date_utc document[config.ID_FIELD] = app.data.insert(resource, document) response_item[config.ID_FIELD] = document[config.ID_FIELD] response_item[config.LAST_UPDATED] = \ document[config.LAST_UPDATED] response_item['link'] = \ document_link(resource, response_item[config.ID_FIELD]) else: issues.extend(validator.errors) except ValidationError as e: raise e except Exception as e: issues.append(str(e)) if len(issues): response_item['issues'] = issues response_item['status'] = config.STATUS_ERR else: response_item['status'] = config.STATUS_OK response[key] = response_item return response, None, None, 200
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 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 getitem(resource, **lookup): """ :param resource: the name of the resource to which the document belongs. :param **lookup: the lookup query. .. versionchanged:: 0.3 Support for media fields. When IF_MATCH is disabled, no etag is included in the payload. .. versionchanged:: 0.1.1 Support for Embeded Resource Serialization. .. versionchanged:: 0.1.0 Support for optional HATEOAS. .. versionchanged: 0.0.8 'on_getting_item' event is raised when a document has been read from the database and is about to be sent to the client. .. versionchanged:: 0.0.7 Support for Rate-Limiting. .. versionchanged:: 0.0.6 Support for HEAD requests. .. versionchanged:: 0.0.6 ETag added to payload. .. versionchanged:: 0.0.5 Support for user-restricted access to resources. Support for LAST_UPDATED field missing from documents, because they were created outside the API context. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 Superflous ``response`` container removed. Links wrapped with ``_links``. Links are now properly JSON formatted. """ response = {} req = parse_request(resource) document = app.data.find_one(resource, **lookup) if document: # need to update the document field as well since the etag must # be computed on the same document representation that might have # been used in the collection 'get' method last_modified = document[config.LAST_UPDATED] = last_updated(document) document[config.DATE_CREATED] = date_created(document) etag = None if config.IF_MATCH: etag = document[config.ETAG] = document_etag(document) if req.if_none_match and document[config.ETAG] == \ req.if_none_match: # request etag matches the current server representation of the # document, return a 304 Not-Modified. return response, last_modified, etag, 304 if req.if_modified_since and last_modified <= req.if_modified_since: # request If-Modified-Since conditional request match. We test # this after the etag since Last-Modified dates have lower # resolution (1 second). return response, last_modified, document.get(config.ETAG), 304 _resolve_embedded_documents(resource, req, [document]) _resolve_media_files(document, resource) if config.DOMAIN[resource]['hateoas']: response[config.LINKS] = { 'self': document_link(resource, document[config.ID_FIELD]), 'collection': {'title': config.DOMAIN[resource]['resource_title'], 'href': _collection_link(resource, True)}, 'parent': home_link() } # notify registered callback functions. Please note that, should the # functions modify the document, last_modified and etag won't be # updated to reflect the changes (they always reflect the documents # state on the database). item_title = config.DOMAIN[resource]['item_title'].lower() getattr(app, "on_fetch_item")(resource, document[config.ID_FIELD], document) getattr(app, "on_fetch_item_%s" % item_title)(document[config.ID_FIELD], document) response.update(document) return response, last_modified, etag, 200 abort(404)
def get(resource, lookup): """ Retrieves the resource documents that match the current request. :param resource: the name of the resource. .. versionchanged:: 0.3 Don't return 304 if resource is empty. Fixes #243. Support for media fields. When IF_MATCH is disabled, no etag is included in the payload. When If-Modified-Since header is present, either no documents (304) or all documents (200) are sent per the HTTP spec. Original behavior can be achieved with: /resource?where={"updated":{"$gt":"if-modified-since-date"}} .. versionchanged:: 0.2 Use the new ITEMS configuration setting. Raise 'on_pre_<method>' event. Let cursor add extra info to response. .. versionchanged:: 0.1.0 Support for optional HATEOAS. Support for embeddable documents. .. versionchanged:: 0.0.9 Event hooks renamed to be more robuts and consistent: 'on_getting' renamed to 'on_fetch'. .. versionchanged:: 0.0.8 'on_getting' and 'on_getting_<resource>' events are raised when documents have been read from the database and are about to be sent to the client. .. versionchanged:: 0.0.6 Support for HEAD requests. .. versionchanged:: 0.0.5 Support for user-restricted access to resources. Support for LAST_UPDATED field missing from documents, because they were created outside the API context. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 Superflous ``response`` container removed. Collection items wrapped with ``_items``. Links wrapped with ``_links``. Links are now properly JSON formatted. """ documents = [] response = {} etag = None req = parse_request(resource) if req.if_modified_since: # client has made this request before, has it changed? preflight_req = copy.copy(req) preflight_req.max_results = 1 cursor = app.data.find(resource, preflight_req, lookup) if cursor.count() == 0: # make sure the datasource is not empty (#243). if not app.data.is_empty(resource): # the if-modified-since conditional request returned no # documents, we send back a 304 Not-Modified, which means that # the client already has the up-to-date representation of the # resultset. status = 304 last_modified = None return response, last_modified, etag, status # continue processing the full request last_update = epoch() req.if_modified_since = None cursor = app.data.find(resource, req, lookup) for document in cursor: document[config.LAST_UPDATED] = last_updated(document) document[config.DATE_CREATED] = date_created(document) if document[config.LAST_UPDATED] > last_update: last_update = document[config.LAST_UPDATED] # document metadata if config.IF_MATCH: document[config.ETAG] = document_etag(document) if config.DOMAIN[resource]['hateoas']: document[config.LINKS] = {'self': document_link(resource, document[config.ID_FIELD])} _resolve_media_files(document, resource) documents.append(document) _resolve_embedded_documents(resource, req, documents) status = 200 last_modified = last_update if last_update > epoch() else None # notify registered callback functions. Please note that, should the # functions modify the documents, the last_modified and etag won't be # updated to reflect the changes (they always reflect the documents # state on the database.) getattr(app, "on_fetch_resource")(resource, documents) getattr(app, "on_fetch_resource_%s" % resource)(documents) if config.DOMAIN[resource]['hateoas']: response[config.ITEMS] = documents response[config.LINKS] = _pagination_links(resource, req, cursor.count()) else: response = documents # the 'extra' cursor field, if present, will be added to the response. # Can be used by Eve extensions to add extra, custom data to any # response. if hasattr(cursor, 'extra'): getattr(cursor, 'extra')(response) return response, last_modified, etag, status
def get(resource): """Retrieves the resource documents that match the current request. :param resource: the name of the resource. .. versionchanged: 0.0.9 Event hooks renamed to be more robuts and consistent: 'on_getting' renamed to 'on_fetch'. .. versionchanged: 0.0.8 'on_getting' and 'on_getting_<resource>' events are raised when documents have been read from the database and are about to be sent to the client. .. versionchanged:: 0.0.6 Support for HEAD requests. .. versionchanged:: 0.0.5 Support for user-restricted access to resources. Support for LAST_UPDATED field missing from documents, because they were created outside the API context. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 Superflous ``response`` container removed. Collection items wrapped with ``_items``. Links wrapped with ``_links``. Links are now properly JSON formatted. """ documents = [] response = {} last_updated = _epoch() req = parse_request(resource) cursor = app.data.find(resource, req) for document in cursor: document[config.LAST_UPDATED] = _last_updated(document) document[config.DATE_CREATED] = _date_created(document) if document[config.LAST_UPDATED] > last_updated: last_updated = document[config.LAST_UPDATED] # document metadata document['etag'] = document_etag(document) document['_links'] = { 'self': document_link(resource, document[config.ID_FIELD]) } documents.append(document) if req.if_modified_since and len(documents) == 0: # the if-modified-since conditional request returned no documents, we # send back a 304 Not-Modified, which means that the client already # has the up-to-date representation of the resultset. status = 304 last_modified = None else: status = 200 last_modified = last_updated if last_updated > _epoch() else None # notify registered callback functions. Please note that, should the # functions modify the documents, the last_modified and etag won't be # updated to reflect the changes (they always reflect the documents # state on the database.) getattr(app, "on_fetch_resource")(resource, documents) getattr(app, "on_fetch_resource_%s" % resource)(documents) response['_items'] = documents response['_links'] = _pagination_links(resource, req, cursor.count()) etag = None return response, last_modified, etag, status
def patch(resource, **lookup): """Perform a document patch/update. Updates are first validated against the resource schema. If validation passes, the document is updated and an OK status update is returned. If validation fails, a set of validation issues is returned. :param resource: the name of the resource to which the document belongs. :param **lookup: document lookup query. """ if len(request.form) > 1 or len(request.form) == 0: # only one update-per-document supported abort(400) original = get_document(resource, **lookup) if not original: # not found abort(404) schema = app.config['DOMAIN'][resource]['schema'] validator = app.validator(schema, resource) object_id = original[config.ID_FIELD] last_modified = None etag = None issues = list() key = request.form.keys()[0] value = request.form[key] response_item = dict() try: updates = parse(value, resource) validation = validator.validate_update(updates, object_id) if validation: updates[config.LAST_UPDATED] = datetime.utcnow() app.data.update(resource, object_id, updates) # TODO computing etag without reloading the document # would be ideal. However, for reasons that need fruther # investigation, an etag computed on original.update(updates) # won't provide the same result as the saved document. # this has probably comething to do with a) the different # precision between the BSON (milliseconds) python datetime and, # b), the string representation of the documents (being dicts) # not matching. # # TL;DR: find a way to compute a reliable etag without reloading updated = app.data.find_one(resource, **{config.ID_FIELD: object_id}) updated[config.LAST_UPDATED] = \ updated[config.LAST_UPDATED].replace(tzinfo=None) etag = document_etag(updated) response_item[config.ID_FIELD] = object_id last_modified = response_item[config.LAST_UPDATED] = \ updated[config.LAST_UPDATED] # metadata response_item['etag'] = etag response_item['link'] = document_link(resource, object_id) else: issues.extend(validator.errors) except ValidationError, e: # TODO should probably log the error and abort 400 instead (when we # got logging) issues.append(str(e))
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 post(resource): """ Adds one or more documents to a resource. Each document is validated against the domain schema. If validation passes the document is inserted and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the document are returned. If validation fails, a list of validation issues is returned. :param resource: name of the resource involved. .. versionchanged:: 0.0.5 Support for 'application/json' Content-Type . Support for 'user-restricted resource access'. .. versionchanged:: 0.0.4 Added the ``reqiores_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ response = {} date_utc = datetime.utcnow() schema = app.config['DOMAIN'][resource]['schema'] validator = app.validator(schema, resource) for key, value in payload().items(): response_item = {} issues = [] try: document = parse(value, resource) validation = validator.validate(document) if validation: document[config.LAST_UPDATED] = \ document[config.DATE_CREATED] = date_utc # if 'user-restricted resource access' is enabled and there's # an Auth request active, inject the username into the document username_field = \ app.config['DOMAIN'][resource]['auth_username_field'] if username_field and request.authorization: document[username_field] = request.authorization.username document[config.ID_FIELD] = app.data.insert(resource, document) response_item[config.ID_FIELD] = document[config.ID_FIELD] response_item[config.LAST_UPDATED] = \ document[config.LAST_UPDATED] response_item['_links'] = \ {'self': document_link(resource, response_item[config.ID_FIELD])} else: issues.extend(validator.errors) except ValidationError as e: raise e except Exception as e: issues.append(str(e)) if len(issues): response_item['issues'] = issues response_item['status'] = config.STATUS_ERR else: response_item['status'] = config.STATUS_OK response[key] = response_item return response, None, None, 200
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.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 get(resource, lookup): """ Retrieves the resource documents that match the current request. :param resource: the name of the resource. .. versionchanged:: 0.3 Don't return 304 if resource is empty. Fixes #243. Support for media fields. When IF_MATCH is disabled, no etag is included in the payload. When If-Modified-Since header is present, either no documents (304) or all documents (200) are sent per the HTTP spec. Original behavior can be achieved with: /resource?where={"updated":{"$gt":"if-modified-since-date"}} .. versionchanged:: 0.2 Use the new ITEMS configuration setting. Raise 'on_pre_<method>' event. Let cursor add extra info to response. .. versionchanged:: 0.1.0 Support for optional HATEOAS. Support for embeddable documents. .. versionchanged:: 0.0.9 Event hooks renamed to be more robuts and consistent: 'on_getting' renamed to 'on_fetch'. .. versionchanged:: 0.0.8 'on_getting' and 'on_getting_<resource>' events are raised when documents have been read from the database and are about to be sent to the client. .. versionchanged:: 0.0.6 Support for HEAD requests. .. versionchanged:: 0.0.5 Support for user-restricted access to resources. Support for LAST_UPDATED field missing from documents, because they were created outside the API context. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 Superflous ``response`` container removed. Collection items wrapped with ``_items``. Links wrapped with ``_links``. Links are now properly JSON formatted. """ documents = [] response = {} etag = None req = parse_request(resource) if req.if_modified_since: # client has made this request before, has it changed? preflight_req = copy.copy(req) preflight_req.max_results = 1 cursor = app.data.find(resource, preflight_req, lookup) if cursor.count() == 0: # make sure the datasource is not empty (#243). if not app.data.is_empty(resource): # the if-modified-since conditional request returned no # documents, we send back a 304 Not-Modified, which means that # the client already has the up-to-date representation of the # resultset. status = 304 last_modified = None return response, last_modified, etag, status # continue processing the full request last_update = epoch() req.if_modified_since = None cursor = app.data.find(resource, req, lookup) for document in cursor: document[config.LAST_UPDATED] = last_updated(document) document[config.DATE_CREATED] = date_created(document) if document[config.LAST_UPDATED] > last_update: last_update = document[config.LAST_UPDATED] # document metadata if config.IF_MATCH: document[config.ETAG] = document_etag(document) if config.DOMAIN[resource]['hateoas']: document[config.LINKS] = { 'self': document_link(resource, document[config.ID_FIELD]) } _resolve_media_files(document, resource) documents.append(document) _resolve_embedded_documents(resource, req, documents) status = 200 last_modified = last_update if last_update > epoch() else None # notify registered callback functions. Please note that, should the # functions modify the documents, the last_modified and etag won't be # updated to reflect the changes (they always reflect the documents # state on the database.) getattr(app, "on_fetch_resource")(resource, documents) getattr(app, "on_fetch_resource_%s" % resource)(documents) if config.DOMAIN[resource]['hateoas']: response[config.ITEMS] = documents response[config.LINKS] = _pagination_links(resource, req, cursor.count()) else: response = documents # the 'extra' cursor field, if present, will be added to the response. # Can be used by Eve extensions to add extra, custom data to any # response. if hasattr(cursor, 'extra'): getattr(cursor, 'extra')(response) return response, last_modified, etag, status
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 getitem(resource, **lookup): """ :param resource: the name of the resource to which the document belongs. :param **lookup: the lookup query. .. versionchanged:: 0.3 Support for media fields. When IF_MATCH is disabled, no etag is included in the payload. .. versionchanged:: 0.1.1 Support for Embeded Resource Serialization. .. versionchanged:: 0.1.0 Support for optional HATEOAS. .. versionchanged: 0.0.8 'on_getting_item' event is raised when a document has been read from the database and is about to be sent to the client. .. versionchanged:: 0.0.7 Support for Rate-Limiting. .. versionchanged:: 0.0.6 Support for HEAD requests. .. versionchanged:: 0.0.6 ETag added to payload. .. versionchanged:: 0.0.5 Support for user-restricted access to resources. Support for LAST_UPDATED field missing from documents, because they were created outside the API context. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 Superflous ``response`` container removed. Links wrapped with ``_links``. Links are now properly JSON formatted. """ response = {} req = parse_request(resource) document = app.data.find_one(resource, **lookup) if document: # need to update the document field as well since the etag must # be computed on the same document representation that might have # been used in the collection 'get' method last_modified = document[config.LAST_UPDATED] = last_updated(document) document[config.DATE_CREATED] = date_created(document) etag = None if config.IF_MATCH: etag = document[config.ETAG] = document_etag(document) if req.if_none_match and document[config.ETAG] == \ req.if_none_match: # request etag matches the current server representation of the # document, return a 304 Not-Modified. return response, last_modified, etag, 304 if req.if_modified_since and last_modified <= req.if_modified_since: # request If-Modified-Since conditional request match. We test # this after the etag since Last-Modified dates have lower # resolution (1 second). return response, last_modified, document.get(config.ETAG), 304 _resolve_embedded_documents(resource, req, [document]) _resolve_media_files(document, resource) if config.DOMAIN[resource]['hateoas']: response[config.LINKS] = { 'self': document_link(resource, document[config.ID_FIELD]), 'collection': { 'title': config.DOMAIN[resource]['resource_title'], 'href': _collection_link(resource, True) }, 'parent': home_link() } # notify registered callback functions. Please note that, should the # functions modify the document, last_modified and etag won't be # updated to reflect the changes (they always reflect the documents # state on the database). item_title = config.DOMAIN[resource]['item_title'].lower() getattr(app, "on_fetch_item")(resource, document[config.ID_FIELD], document) getattr(app, "on_fetch_item_%s" % item_title)(document[config.ID_FIELD], document) response.update(document) return response, last_modified, etag, 200 abort(404)
def post(resource, payl=None): """ Adds one or more documents to a resource. Each document is validated against the domain schema. If validation passes the document is inserted and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the document are returned. If validation fails, a list of validation issues is returned. :param resource: name of the resource involved. :param payl: alternative payload. When calling post() from your own code you can provide an alternative payload This can be useful, for example, when you have a callback function hooked to a certain endpoint, and want to perform additional post() calls from there. Please be advised that in order to successfully use this option, a request context must be available. See https://github.com/nicolaiarocci/eve/issues/74 for a discussion, and a typical use case. .. versionchanged:: 0.3 Return 201 if at least one document has been successfully inserted. Fix #231 auth field not set if resource level authentication is set. Support for media fields. When IF_MATCH is disabled, no etag is included in the payload. Support for new validation format introduced with Cerberus v0.5. .. versionchanged:: 0.2 Use the new STATUS setting. Use the new ISSUES setting. Raise 'on_pre_<method>' event. Explictly resolve default values instead of letting them be resolved by common.parse. This avoids a validation error when a read-only field also has a default value. .. versionchanged:: 0.1.1 auth.request_auth_value is now used to store the auth_field value. .. versionchanged:: 0.1.0 More robust handling of auth_field. Support for optional HATEOAS. .. versionchanged: 0.0.9 Event hooks renamed to be more robuts and consistent: 'on_posting' renamed to 'on_insert'. You can now pass a pre-defined custom payload to the funcion. .. versionchanged:: 0.0.9 Storing self.app.auth.userid in auth_field when 'user-restricted resource access' is enabled. .. versionchanged: 0.0.7 Support for Rate-Limiting. Support for 'extra_response_fields'. 'on_posting' and 'on_posting_<resource>' events are raised before the documents are inserted into the database. This allows callback functions to arbitrarily edit/update the documents being stored. .. versionchanged:: 0.0.6 Support for bulk inserts. Please note: validation constraints are checked against the database, and not between the payload documents themselves. This causes an interesting corner case: in the event of a multiple documents payload where two or more documents carry the same value for a field where the 'unique' constraint is set, the payload will validate successfully, as there are no duplicates in the database (yet). If this is an issue, the client can always send the documents once at a time for insertion, or validate locally before submitting the payload to the API. .. versionchanged:: 0.0.5 Support for 'application/json' Content-Type . Support for 'user-restricted resource access'. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ date_utc = datetime.utcnow().replace(microsecond=0) resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] validator = app.validator(schema, resource) documents = [] issues = [] # validation, and additional fields if payl is None: payl = payload() if isinstance(payl, dict): payl = [payl] for value in payl: document = [] doc_issues = {} try: document = parse(value, resource) validation = validator.validate(document) if validation: # validation is successful document[config.LAST_UPDATED] = \ document[config.DATE_CREATED] = date_utc resolve_user_restricted_access(document, resource) resolve_default_values(document, resource) resolve_media_files(document, resource) else: # validation errors added to list of document issues doc_issues = validator.errors except ValidationError as e: doc_issues['validation exception'] = str(e) except Exception as e: # most likely a problem with the incoming payload, report back to # the client as if it was a validation issue doc_issues['exception'] = str(e) issues.append(doc_issues) if len(doc_issues) == 0: documents.append(document) if len(documents): # notify callbacks getattr(app, "on_insert")(resource, documents) getattr(app, "on_insert_%s" % resource)(documents) # bulk insert ids = app.data.insert(resource, documents) # request was received and accepted; at least one document passed # validation and was accepted for insertion. return_code = 201 else: # request was received and accepted; no document passed validation # though. return_code = 200 # build response payload response = [] for doc_issues in issues: item = {} if len(doc_issues): item[config.STATUS] = config.STATUS_ERR item[config.ISSUES] = doc_issues else: item[config.STATUS] = config.STATUS_OK document = documents.pop(0) item[config.LAST_UPDATED] = document[config.LAST_UPDATED] # either return the custom ID_FIELD or the id returned by # data.insert(). item[config.ID_FIELD] = document.get(config.ID_FIELD, ids.pop(0)) if config.IF_MATCH: item[config.ETAG] = document_etag(document) if resource_def['hateoas']: item[config.LINKS] = \ {'self': document_link(resource, item[config.ID_FIELD])} # add any additional field that might be needed allowed_fields = [ x for x in resource_def['extra_response_fields'] if x in document.keys() ] for field in allowed_fields: item[field] = document[field] response.append(item) if len(response) == 1: response = response.pop(0) return response, None, None, return_code
def post(resource, payl=None): """ Adds one or more documents to a resource. Each document is validated against the domain schema. If validation passes the document is inserted and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the document are returned. If validation fails, a list of validation issues is returned. :param resource: name of the resource involved. :param payl: alternative payload. When calling post() from your own code you can provide an alternative payload This can be useful, for example, when you have a callback function hooked to a certain endpoint, and want to perform additional post() calls from there. Please be advised that in order to successfully use this option, a request context must be available. See https://github.com/nicolaiarocci/eve/issues/74 for a discussion, and a typical use case. .. versionchanged:: 0.3 Return 201 if at least one document has been successfully inserted. Fix #231 auth field not set if resource level authentication is set. Support for media fields. When IF_MATCH is disabled, no etag is included in the payload. Support for new validation format introduced with Cerberus v0.5. .. versionchanged:: 0.2 Use the new STATUS setting. Use the new ISSUES setting. Raise 'on_pre_<method>' event. Explictly resolve default values instead of letting them be resolved by common.parse. This avoids a validation error when a read-only field also has a default value. .. versionchanged:: 0.1.1 auth.request_auth_value is now used to store the auth_field value. .. versionchanged:: 0.1.0 More robust handling of auth_field. Support for optional HATEOAS. .. versionchanged: 0.0.9 Event hooks renamed to be more robuts and consistent: 'on_posting' renamed to 'on_insert'. You can now pass a pre-defined custom payload to the funcion. .. versionchanged:: 0.0.9 Storing self.app.auth.userid in auth_field when 'user-restricted resource access' is enabled. .. versionchanged: 0.0.7 Support for Rate-Limiting. Support for 'extra_response_fields'. 'on_posting' and 'on_posting_<resource>' events are raised before the documents are inserted into the database. This allows callback functions to arbitrarily edit/update the documents being stored. .. versionchanged:: 0.0.6 Support for bulk inserts. Please note: validation constraints are checked against the database, and not between the payload documents themselves. This causes an interesting corner case: in the event of a multiple documents payload where two or more documents carry the same value for a field where the 'unique' constraint is set, the payload will validate successfully, as there are no duplicates in the database (yet). If this is an issue, the client can always send the documents once at a time for insertion, or validate locally before submitting the payload to the API. .. versionchanged:: 0.0.5 Support for 'application/json' Content-Type . Support for 'user-restricted resource access'. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ date_utc = datetime.utcnow().replace(microsecond=0) resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] validator = app.validator(schema, resource) documents = [] issues = [] # validation, and additional fields if payl is None: payl = payload() if isinstance(payl, dict): payl = [payl] for value in payl: document = [] doc_issues = {} try: document = parse(value, resource) validation = validator.validate(document) if validation: # validation is successful document[config.LAST_UPDATED] = \ document[config.DATE_CREATED] = date_utc resolve_user_restricted_access(document, resource) resolve_default_values(document, resource) resolve_media_files(document, resource) else: # validation errors added to list of document issues doc_issues = validator.errors except ValidationError as e: doc_issues['validation exception'] = str(e) except Exception as e: # most likely a problem with the incoming payload, report back to # the client as if it was a validation issue doc_issues['exception'] = str(e) issues.append(doc_issues) if len(doc_issues) == 0: documents.append(document) if len(documents): # notify callbacks getattr(app, "on_insert")(resource, documents) getattr(app, "on_insert_%s" % resource)(documents) # bulk insert ids = app.data.insert(resource, documents) # request was received and accepted; at least one document passed # validation and was accepted for insertion. return_code = 201 else: # request was received and accepted; no document passed validation # though. return_code = 200 # build response payload response = [] for doc_issues in issues: item = {} if len(doc_issues): item[config.STATUS] = config.STATUS_ERR item[config.ISSUES] = doc_issues else: item[config.STATUS] = config.STATUS_OK document = documents.pop(0) item[config.LAST_UPDATED] = document[config.LAST_UPDATED] # either return the custom ID_FIELD or the id returned by # data.insert(). item[config.ID_FIELD] = document.get(config.ID_FIELD, ids.pop(0)) if config.IF_MATCH: item[config.ETAG] = document_etag(document) if resource_def['hateoas']: item[config.LINKS] = \ {'self': document_link(resource, item[config.ID_FIELD])} # add any additional field that might be needed allowed_fields = [x for x in resource_def['extra_response_fields'] if x in document.keys()] for field in allowed_fields: item[field] = document[field] response.append(item) if len(response) == 1: response = response.pop(0) return response, None, None, return_code
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 get(resource): """Retrieves the resource documents that match the current request. :param resource: the name of the resource. .. versionchanged: 0.0.9 Event hooks renamed to be more robuts and consistent: 'on_getting' renamed to 'on_fetch'. .. versionchanged: 0.0.8 'on_getting' and 'on_getting_<resource>' events are raised when documents have been read from the database and are about to be sent to the client. .. versionchanged:: 0.0.6 Support for HEAD requests. .. versionchanged:: 0.0.5 Support for user-restricted access to resources. Support for LAST_UPDATED field missing from documents, because they were created outside the API context. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 Superflous ``response`` container removed. Collection items wrapped with ``_items``. Links wrapped with ``_links``. Links are now properly JSON formatted. """ documents = [] response = {} last_updated = _epoch() req = parse_request(resource) cursor = app.data.find(resource, req) for document in cursor: document[config.LAST_UPDATED] = _last_updated(document) document[config.DATE_CREATED] = _date_created(document) if document[config.LAST_UPDATED] > last_updated: last_updated = document[config.LAST_UPDATED] # document metadata document["etag"] = document_etag(document) document["_links"] = {"self": document_link(resource, document[config.ID_FIELD])} documents.append(document) if req.if_modified_since and len(documents) == 0: # the if-modified-since conditional request returned no documents, we # send back a 304 Not-Modified, which means that the client already # has the up-to-date representation of the resultset. status = 304 last_modified = None else: status = 200 last_modified = last_updated if last_updated > _epoch() else None # notify registered callback functions. Please note that, should the # functions modify the documents, the last_modified and etag won't be # updated to reflect the changes (they always reflect the documents # state on the database.) getattr(app, "on_fetch_resource")(resource, documents) getattr(app, "on_fetch_resource_%s" % resource)(documents) response["_items"] = documents response["_links"] = _pagination_links(resource, req, cursor.count()) etag = None return response, last_modified, etag, status
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 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 patch(resource, **lookup): """Perform a document patch/update. Updates are first validated against the resource schema. If validation passes, the document is updated and an OK status update is returned. If validation fails, a set of validation issues is returned. :param resource: the name of the resource to which the document belongs. :param **lookup: document lookup query. .. versionchanged:: 0.0.8 Let ``werkzeug.exceptions.InternalServerError`` go through as they have probably been explicitly raised by the data driver. .. versionchanged:: 0.0.7 Support for Rate-Limiting. .. versionchanged:: 0.0.6 ETag is now computed without the need of an additional db lookup .. versionchanged:: 0.0.5 Support for 'aplication/json' Content-Type. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ payload = payload_() if len(payload) > 1: # only one update-per-document supported abort(400) original = get_document(resource, **lookup) if not original: # not found abort(404) schema = app.config['DOMAIN'][resource]['schema'] validator = app.validator(schema, resource) object_id = original[config.ID_FIELD] last_modified = None etag = None issues = [] key = payload.keys()[0] value = payload[key] response_item = {} try: updates = parse(value, resource) validation = validator.validate_update(updates, object_id) if validation: # the mongo driver has a different precision than the python # datetime. since we don't want to reload the document once it has # been updated, and we still have to provide an updated etag, # we're going to update the local version of the 'original' # document, and we will use it for the etag computation. original.update(updates) # some datetime precision magic updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = \ datetime.utcnow().replace(microsecond=0) etag = document_etag(original) app.data.update(resource, object_id, updates) response_item[config.ID_FIELD] = object_id last_modified = response_item[config.LAST_UPDATED] = \ original[config.LAST_UPDATED] # metadata response_item['etag'] = etag response_item['_links'] = { 'self': document_link(resource, object_id) } else: issues.extend(validator.errors) except ValidationError, e: # TODO should probably log the error and abort 400 instead (when we # got logging) issues.append(str(e))
def post(resource): """ Adds one or more documents to a resource. Each document is validated against the domain schema. If validation passes the document is inserted and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the document are returned. If validation fails, a list of validation issues is returned. :param resource: name of the resource involved. .. versionchanged:: 0.0.6 Support for bulk inserts. Please note: validation constraints are checked against the database, and not between the payload documents themselves. This causes an interesting corner case: in the event of a multiple documents payload where two or more documents carry the same value for a field where the 'unique' constraint is set, the payload will validate successfully, as there are no duplicates in the database (yet). If this is an issue, the client can always send the documents once at a time for insertion, or validate locally before submitting the payload to the API. .. versionchanged:: 0.0.5 Support for 'application/json' Content-Type . Support for 'user-restricted resource access'. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ date_utc = datetime.utcnow().replace(microsecond=0) schema = app.config['DOMAIN'][resource]['schema'] validator = app.validator(schema, resource) documents = [] issues = [] # validation, and additional fields payl = payload() for key, value in payl.items(): document = [] doc_issues = [] try: document = parse(value, resource) validation = validator.validate(document) if validation: # validation is successful document[config.LAST_UPDATED] = \ document[config.DATE_CREATED] = date_utc # if 'user-restricted resource access' is enabled and there's # an Auth request active, inject the username into the document username_field = \ app.config['DOMAIN'][resource]['auth_username_field'] if username_field and request.authorization: document[username_field] = request.authorization.username else: # validation errors added to list of document issues doc_issues.extend(validator.errors) except ValidationError as e: raise e except Exception as e: # most likely a problem with the incoming payload, report back to # the client as if it was a validation issue doc_issues.append(str(e)) issues.append(doc_issues) if len(doc_issues) == 0: documents.append(document) # bulk insert if len(documents): ids = app.data.insert(resource, documents) # build response payload response = {} for key, doc_issues in zip(payl.keys(), issues): response_item = {} if len(doc_issues): response_item['status'] = config.STATUS_ERR response_item['issues'] = doc_issues else: response_item['status'] = config.STATUS_OK response_item[config.ID_FIELD] = ids.pop(0) document = documents.pop(0) response_item[config.LAST_UPDATED] = document[config.LAST_UPDATED] response_item['etag'] = document_etag(document) response_item['_links'] = \ {'self': document_link(resource, response_item[config.ID_FIELD])} response[key] = response_item return response, None, None, 200
def patch(resource, **lookup): """ Perform a document patch/update. Updates are first validated against the resource schema. If validation passes, the document is updated and an OK status update is returned. If validation fails, a set of validation issues is returned. :param resource: the name of the resource to which the document belongs. :param **lookup: document lookup query. .. versionchanged:: 0.3 Support for media fields. When IF_MATCH is disabled, no etag is included in the payload. Support for new validation format introduced with Cerberus v0.5. .. versionchanged:: 0.2 Use the new STATUS setting. Use the new ISSUES setting. Raise 'on_pre_<method>' event. .. versionchanged:: 0.1.1 Item-identifier wrapper stripped from both request and response payload. .. versionchanged:: 0.1.0 Support for optional HATEOAS. Re-raises `exceptions.Unauthorized`, this could occur if the `auth_field` condition fails .. versionchanged:: 0.0.9 More informative error messages. Support for Python 3.3. .. versionchanged:: 0.0.8 Let ``werkzeug.exceptions.InternalServerError`` go through as they have probably been explicitly raised by the data driver. .. versionchanged:: 0.0.7 Support for Rate-Limiting. .. versionchanged:: 0.0.6 ETag is now computed without the need of an additional db lookup .. versionchanged:: 0.0.5 Support for 'aplication/json' Content-Type. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ payload = payload_() original = get_document(resource, **lookup) if not original: # not found abort(404) resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] validator = app.validator(schema, resource) object_id = original[config.ID_FIELD] last_modified = None etag = None issues = {} response = {} try: updates = parse(payload, resource) validation = validator.validate_update(updates, object_id) if validation: resolve_media_files(updates, resource, original) # the mongo driver has a different precision than the python # datetime. since we don't want to reload the document once it has # been updated, and we still have to provide an updated etag, # we're going to update the local version of the 'original' # document, and we will use it for the etag computation. original.update(updates) # some datetime precision magic updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = \ datetime.utcnow().replace(microsecond=0) app.data.update(resource, object_id, updates) response[config.ID_FIELD] = original[config.ID_FIELD] last_modified = response[config.LAST_UPDATED] = \ original[config.LAST_UPDATED] # metadata if config.IF_MATCH: etag = response[config.ETAG] = document_etag(original) if resource_def['hateoas']: response[config.LINKS] = { 'self': document_link(resource, original[config.ID_FIELD]) } else: issues = validator.errors except ValidationError as e: # TODO should probably log the error and abort 400 instead (when we # got logging) issues['validator exception'] = str(e) except (exceptions.InternalServerError, exceptions.Unauthorized) as e: raise e except Exception as e: # consider all other exceptions as Bad Requests abort(400, description=debug_error_message( 'An exception occurred: %s' % e )) if len(issues): response[config.ISSUES] = issues response[config.STATUS] = config.STATUS_ERR else: response[config.STATUS] = config.STATUS_OK return response, last_modified, etag, 200
def patch(resource, **lookup): """Perform a document patch/update. Updates are first validated against the resource schema. If validation passes, the document is updated and an OK status update is returned. If validation fails, a set of validation issues is returned. :param resource: the name of the resource to which the document belongs. :param **lookup: document lookup query. .. versionchanged:: 0.0.7 Support for Rate-Limiting. .. versionchanged:: 0.0.6 ETag is now computed without the need of an additional db lookup .. versionchanged:: 0.0.5 Support for 'aplication/json' Content-Type. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ payload = payload_() if len(payload) > 1: # only one update-per-document supported abort(400) original = get_document(resource, **lookup) if not original: # not found abort(404) schema = app.config["DOMAIN"][resource]["schema"] validator = app.validator(schema, resource) object_id = original[config.ID_FIELD] last_modified = None etag = None issues = [] key = payload.keys()[0] value = payload[key] response_item = {} try: updates = parse(value, resource) validation = validator.validate_update(updates, object_id) if validation: # the mongo driver has a different precision than the python # datetime. since we don't want to reload the document once it has # been updated, and we still have to provide an updated etag, # we're going to update the local version of the 'original' # document, and we will use it for the etag computation. original.update(updates) # some datetime precision magic updates[config.LAST_UPDATED] = original[config.LAST_UPDATED] = datetime.utcnow().replace(microsecond=0) etag = document_etag(original) app.data.update(resource, object_id, updates) response_item[config.ID_FIELD] = object_id last_modified = response_item[config.LAST_UPDATED] = original[config.LAST_UPDATED] # metadata response_item["etag"] = etag response_item["_links"] = {"self": document_link(resource, object_id)} else: issues.extend(validator.errors) except ValidationError, e: # TODO should probably log the error and abort 400 instead (when we # got logging) issues.append(str(e))
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 post(resource, payl=None): """ Adds one or more documents to a resource. Each document is validated against the domain schema. If validation passes the document is inserted and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the document are returned. If validation fails, a list of validation issues is returned. :param resource: name of the resource involved. :param payl: alternative payload. When calling post() from your own code you can provide an alternative payload This can be useful, for example, when you have a callback function hooked to a certain endpoint, and want to perform additional post() calls from there. Please be advised that in order to successfully use this option, a request context must be available. See https://github.com/nicolaiarocci/eve/issues/74 for a discussion, and a typical use case. .. versionchanged:: 0.1.0 More robust handling of auth_field. Support for optional HATEOAS. .. versionchanged: 0.0.9 Event hooks renamed to be more robuts and consistent: 'on_posting' renamed to 'on_insert'. You can now pass a pre-defined custom payload to the funcion. .. versionchanged:: 0.0.9 Storing self.app.auth.userid in auth_field when 'user-restricted resource access' is enabled. .. versionchanged: 0.0.7 Support for Rate-Limiting. Support for 'extra_response_fields'. 'on_posting' and 'on_posting_<resource>' events are raised before the documents are inserted into the database. This allows callback functions to arbitrarily edit/update the documents being stored. .. versionchanged:: 0.0.6 Support for bulk inserts. Please note: validation constraints are checked against the database, and not between the payload documents themselves. This causes an interesting corner case: in the event of a multiple documents payload where two or more documents carry the same value for a field where the 'unique' constraint is set, the payload will validate successfully, as there are no duplicates in the database (yet). If this is an issue, the client can always send the documents once at a time for insertion, or validate locally before submitting the payload to the API. .. versionchanged:: 0.0.5 Support for 'application/json' Content-Type . Support for 'user-restricted resource access'. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. .. versionchanged:: 0.0.3 JSON links. Superflous ``response`` container removed. """ date_utc = datetime.utcnow().replace(microsecond=0) resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] validator = app.validator(schema, resource) documents = [] issues = [] # validation, and additional fields if payl is None: payl = payload() for key, value in payl.items(): document = [] doc_issues = [] try: document = parse(value, resource) validation = validator.validate(document) if validation: # validation is successful document[config.LAST_UPDATED] = \ document[config.DATE_CREATED] = date_utc # if 'user-restricted resource access' is enabled # and there's an Auth request active, # inject the auth_field into the document auth_field = resource_def['auth_field'] if auth_field: auth_field_value = getattr(app.auth, auth_field) if auth_field_value and request.authorization: document[auth_field] = auth_field_value else: # validation errors added to list of document issues doc_issues.extend(validator.errors) except ValidationError as e: raise e except Exception as e: # most likely a problem with the incoming payload, report back to # the client as if it was a validation issue doc_issues.append(str(e)) issues.append(doc_issues) if len(doc_issues) == 0: documents.append(document) if len(documents): # notify callbacks getattr(app, "on_insert")(resource, documents) getattr(app, "on_insert_%s" % resource)(documents) # bulk insert ids = app.data.insert(resource, documents) # build response payload response = {} for key, doc_issues in zip(payl.keys(), issues): response_item = {} if len(doc_issues): response_item['status'] = config.STATUS_ERR response_item['issues'] = doc_issues else: response_item['status'] = config.STATUS_OK response_item[config.ID_FIELD] = ids.pop(0) document = documents.pop(0) response_item[config.LAST_UPDATED] = document[config.LAST_UPDATED] response_item['etag'] = document_etag(document) if resource_def['hateoas']: response_item['_links'] = \ {'self': document_link(resource, response_item[config.ID_FIELD])} # add any additional field that might be needed allowed_fields = [x for x in resource_def['extra_response_fields'] if x in document.keys()] for field in allowed_fields: response_item[field] = document[field] response[key] = response_item return response, None, None, 200
def put(resource, **lookup): """Perform a document replacement. Updates are first validated against the resource schema. If validation passes, the document is repalced and an OK status update is returned. If validation fails a set of validation issues is returned. :param resource: the name of the resource to which the document belongs. :param **lookup: document lookup query. .. versionadded:: 0.1.0 """ resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] validator = app.validator(schema, resource) payload = payload_() if len(payload) > 1: abort(400, description=debug_error_message( 'Only one update-per-document supported')) original = get_document(resource, **lookup) if not original: # not found abort(404) last_modified = None etag = None issues = [] object_id = original[config.ID_FIELD] # TODO the list is needed for Py33. Find a less ridiculous alternative? key = list(payload.keys())[0] document = payload[key] response_item = {} try: document = parse(document, resource) validation = validator.validate_replace(document, object_id) if validation: last_modified = datetime.utcnow().replace(microsecond=0) document[config.ID_FIELD] = object_id document[config.LAST_UPDATED] = last_modified # TODO what do we need here: the original creation date or the # PUT date? Going for the former seems reasonable. document[config.DATE_CREATED] = original[config.DATE_CREATED] # if 'user-restricted resource access' is enabled and there's # an Auth request active, inject the username into the document auth_field = resource_def['auth_field'] if auth_field: userid = app.auth.user_id if userid and request.authorization: document[auth_field] = userid etag = document_etag(document) app.data.replace(resource, object_id, document) response_item[config.ID_FIELD] = object_id response_item[config.LAST_UPDATED] = last_modified # metadata response_item['etag'] = etag if resource_def['hateoas']: response_item['_links'] = {'self': document_link(resource, object_id)} else: issues.extend(validator.errors) except ValidationError as e: # TODO should probably log the error and abort 400 instead (when we # got logging) issues.append(str(e)) except exceptions.InternalServerError as e: raise e except Exception as e: # consider all other exceptions as Bad Requests abort(400, description=debug_error_message( 'An exception occurred: %s' % e )) if len(issues): response_item['issues'] = issues response_item['status'] = config.STATUS_ERR else: response_item['status'] = config.STATUS_OK response = {} response[key] = response_item return response, last_modified, etag, 200