93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
"""Handler for PATCH requests.
|
|
|
|
This supports PATCH request in the sense described by William Durand:
|
|
http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/
|
|
|
|
Each PATCH should be a JSON dict with at least a key 'op' with the
|
|
name of the operation to perform.
|
|
"""
|
|
|
|
import logging
|
|
|
|
import flask
|
|
|
|
from pillar.api.utils import authorization
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class AbstractPatchHandler:
|
|
"""Abstract PATCH handler supporting multiple operations.
|
|
|
|
Each operation, i.e. possible value of the 'op' key in the PATCH body,
|
|
should be matched to a similarly named "patch_xxx" function in a subclass.
|
|
For example, the operation "set-owner" is mapped to "patch_set_owner".
|
|
|
|
:cvar route: the Flask/Werkzeug route to attach this handler to.
|
|
For most handlers, the default will be fine.
|
|
:cvar item_name: the name of the things to patch, like "job", "task" etc.
|
|
Only used for logging.
|
|
"""
|
|
|
|
route: str = '/<object_id>'
|
|
item_name: str = None
|
|
|
|
def __init_subclass__(cls, **kwargs):
|
|
if not cls.route:
|
|
raise ValueError('Subclass must set route')
|
|
if not cls.item_name:
|
|
raise ValueError('Subclass must set item_name')
|
|
|
|
def __init__(self, blueprint: flask.Blueprint):
|
|
self.log: logging.Logger = log.getChild(self.__class__.__name__)
|
|
self.patch_handlers = {
|
|
name[6:].replace('_', '-'): getattr(self, name)
|
|
for name in dir(self)
|
|
if name.startswith('patch_') and callable(getattr(self, name))
|
|
}
|
|
|
|
if self.log.isEnabledFor(logging.INFO):
|
|
self.log.info('Creating PATCH handler %s.%s%s for operations: %s',
|
|
blueprint.name, self.patch.__name__, self.route,
|
|
sorted(self.patch_handlers.keys()))
|
|
|
|
blueprint.add_url_rule(self.route,
|
|
self.patch.__name__,
|
|
self.patch,
|
|
methods=['PATCH'])
|
|
|
|
@authorization.require_login()
|
|
def patch(self, object_id: str):
|
|
from flask import request
|
|
import werkzeug.exceptions as wz_exceptions
|
|
from pillar.api.utils import str2id, authentication
|
|
|
|
# Parse the request
|
|
real_object_id = str2id(object_id)
|
|
patch = request.get_json()
|
|
if not patch:
|
|
self.log.info('Bad PATCH request, did not contain JSON')
|
|
raise wz_exceptions.BadRequest('Patch must contain JSON')
|
|
|
|
try:
|
|
patch_op = patch['op']
|
|
except KeyError:
|
|
self.log.info("Bad PATCH request, did not contain 'op' key")
|
|
raise wz_exceptions.BadRequest("PATCH should contain 'op' key to denote operation.")
|
|
|
|
log.debug('User %s wants to PATCH "%s" %s %s',
|
|
authentication.current_user_id(), patch_op, self.item_name, real_object_id)
|
|
|
|
# Find the PATCH handler for the operation.
|
|
try:
|
|
handler = self.patch_handlers[patch_op]
|
|
except KeyError:
|
|
log.warning('No %s PATCH handler for operation %r', self.item_name, patch_op)
|
|
raise wz_exceptions.BadRequest('Operation %r not supported' % patch_op)
|
|
|
|
# Let the PATCH handler do its thing.
|
|
response = handler(real_object_id, patch)
|
|
if response is None:
|
|
return '', 204
|
|
return response
|