def apply_changes(self, obj, requested_changes): """Merge `changes` into `object` fields. .. note:: This is used in the context of PATCH only. Override this to control field changes at object level, for example: .. code-block:: python def apply_changes(self, obj, requested_changes): # Ignore value change if inferior if object['position'] > changes.get('position', -1): changes.pop('position', None) return super().apply_changes(obj, requested_changes) :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest` if result does not comply with resource schema. :returns: the new object with `changes` applied. :rtype: tuple """ if self._is_json_patch: try: applied_changes = apply_json_patch(obj, requested_changes)["data"] updated = {**applied_changes} except ValueError as e: error_details = { "location": "body", "description": f"JSON Patch operation failed: {e}", } raise_invalid(self.request, **error_details) else: applied_changes = {**requested_changes} updated = {**obj} # recursive patch and remove field if null attribute is passed (RFC 7396) if self._is_merge_patch: recursive_update_dict(updated, applied_changes, ignores=(None,)) else: updated.update(**applied_changes) for field, value in applied_changes.items(): has_changed = obj.get(field, value) != value if self.schema.is_readonly(field) and has_changed: error_details = {"name": field, "description": f"Cannot modify {field}"} raise_invalid(self.request, **error_details) try: validated = self.schema().deserialize(updated) except colander.Invalid as e: # Transform the errors we got from colander into Cornice errors. # We could not rely on Service schema because the object should be # validated only once the changes are applied for field, error in e.asdict().items(): # pragma: no branch raise_invalid(self.request, name=field, description=error) return validated, applied_changes
def process_record(self, new, old=None): """Read permissions from request body, and in the case of ``PUT`` every existing ACE is removed (using empty list). """ new = super().process_record(new, old) # patch is specified as a list of of operations (RFC 6902) payload = self.request.validated['body'] if self._is_json_patch: permissions = apply_json_patch(old, payload)['permissions'] elif self._is_merge_patch: existing = old or {} permissions = existing.get('__permissions__', {}) recursive_update_dict(permissions, payload.get('permissions', {}), ignores=(None,)) else: permissions = {k: v for k, v in payload.get('permissions', {}).items() if v is not None} annotated = {**new} if permissions: is_put = (self.request.method.lower() == 'put') if is_put or self._is_merge_patch: # Remove every existing ACEs using empty lists. for perm in self.permissions: permissions.setdefault(perm, []) annotated[self.model.permissions_field] = permissions return annotated
def apply_changes(self, record, changes): """Merge `changes` into `record` fields. .. note:: This is used in the context of PATCH only. Override this to control field changes at record level, for example: .. code-block:: python def apply_changes(self, record, changes): # Ignore value change if inferior if record['position'] > changes.get('position', -1): changes.pop('position', None) return super(MyResource, self).apply_changes(record, changes) :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest` if result does not comply with resource schema. :returns: the new record with `changes` applied. :rtype: dict """ for field, value in changes.items(): has_changed = record.get(field, value) != value if self.mapping.is_readonly(field) and has_changed: error_details = { 'name': field, 'description': 'Cannot modify {0}'.format(field) } raise_invalid(self.request, **error_details) updated = record.copy() # recursive patch and remove field if null attribute is passed (RFC 7396) content_type = str(self.request.headers.get('Content-Type')) if content_type == 'application/merge-patch+json': recursive_update_dict(updated, changes, ignores=[None]) else: updated.update(**changes) try: return self.mapping.deserialize(updated) except colander.Invalid as e: # Transform the errors we got from colander into Cornice errors. # We could not rely on Service schema because the record should be # validated only once the changes are applied for field, error in e.asdict().items(): raise_invalid(self.request, name=field, description=error)
def swagger_view(request): # Only build json once try: return swagger_view.__json__ except AttributeError: pass settings = request.registry.settings # Base swagger spec files = [ settings.get('swagger_file', ''), # From config os.path.join(ORIGIN, 'swagger.yaml'), # Relative to the package root os.path.join(HERE, 'swagger.yaml') # Relative to this file. ] files = [f for f in files if os.path.exists(f)] # Get first file that exists if files: files = files[:1] else: raise httpexceptions.HTTPNotFound() # Plugin swagger extensions includes = aslist(settings.get('includes', '')) for app in includes: f = pkg_resources.resource_filename(app, 'swagger.yaml') if os.path.exists(f): files.append(f) swagger_view.__json__ = {} # Read and merge files for path in files: abs_path = os.path.abspath(path) with open(abs_path) as f: spec = yaml.safe_load(f) recursive_update_dict(swagger_view.__json__, spec) # Update instance fields info = dict(title=settings['project_name'], version=settings['http_api_version']) schemes = [settings.get('http_scheme') or 'http'] security_defs = swagger_view.__json__.get('securityDefinitions', {}) # BasicAuth is a non extension capability, so we should check it from config if 'basicauth' in aslist(settings.get('multiauth.policies', '')): basicauth = { 'type': 'basic', 'description': 'HTTP Basic Authentication.' } security_defs['basicAuth'] = basicauth # Security options are JSON objects with a single key security = swagger_view.__json__.get('security', []) security_names = [next(iter(security_def)) for security_def in security] # include securityDefinitions that are not on default security options for name, prop in security_defs.items(): security_def = {name: prop.get('scopes', {}).keys()} if name not in security_names: security.append(security_def) data = dict(info=info, host=request.host, basePath=request.path.replace(swagger.path, ''), schemes=schemes, securityDefinitions=security_defs, security=security) recursive_update_dict(swagger_view.__json__, data) return swagger_view.__json__
def test_merge_non_dict(self): a = {} recursive_update_dict(a, 1) self.assertEqual(a, {})
def test_merge(self): a = {} recursive_update_dict(a, {"b": {"c": 1}, "d": 2}) self.assertEqual(a["b"]["c"], 1) self.assertEqual(a["d"], 2)
def process_object(self, new, old=None): """Hook for processing objects before they reach storage, to introduce specific logics on fields for example. .. code-block:: python def process_object(self, new, old=None): new = super().process_object(new, old) version = old['version'] if old else 0 new['version'] = version + 1 return new Or add extra validation based on request: .. code-block:: python from kinto.core.errors import raise_invalid def process_object(self, new, old=None): new = super().process_object(new, old) if new['browser'] not in request.headers['User-Agent']: raise_invalid(self.request, name='browser', error='Wrong') return new :param dict new: the validated object to be created or updated. :param dict old: the old object to be updated, ``None`` for creation endpoints. :returns: the processed object. :rtype: dict """ modified_field = self.model.modified_field new_last_modified = new.get(modified_field) # Drop the new last_modified if it is not an integer. is_integer = isinstance(new_last_modified, int) if not is_integer: new.pop(modified_field, None) new_last_modified = None # Drop the new last_modified if lesser or equal to the old one. is_less_or_equal = ( new_last_modified and old is not None and new_last_modified <= old[modified_field] ) if is_less_or_equal: new.pop(modified_field, None) # patch is specified as a list of of operations (RFC 6902) payload = self.request.validated["body"] if self._is_json_patch: permissions = apply_json_patch(old, payload)["permissions"] elif self._is_merge_patch: existing = old or {} permissions = existing.get("__permissions__", {}) recursive_update_dict(permissions, payload.get("permissions", {}), ignores=(None,)) else: permissions = { k: v for k, v in payload.get("permissions", {}).items() if v is not None } annotated = {**new} if permissions: is_put = self.request.method.lower() == "put" if is_put or self._is_merge_patch: # Remove every existing ACEs using empty lists. for perm in self.permissions: permissions.setdefault(perm, []) annotated[self.model.permissions_field] = permissions return annotated
def test_merge(self): a = {} recursive_update_dict(a, {'b': {'c': 1}, 'd': 2}) self.assertEqual(a['b']['c'], 1) self.assertEqual(a['d'], 2)
def apply_changes(self, record, requested_changes): """Merge `changes` into `record` fields. .. note:: This is used in the context of PATCH only. Override this to control field changes at record level, for example: .. code-block:: python def apply_changes(self, record, requested_changes): # Ignore value change if inferior if record['position'] > changes.get('position', -1): changes.pop('position', None) return super().apply_changes(record, requested_changes) :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest` if result does not comply with resource schema. :returns: the new record with `changes` applied. :rtype: tuple """ if self._is_json_patch: try: applied_changes = apply_json_patch(record, requested_changes)['data'] updated = {**applied_changes} except ValueError as e: error_details = { 'location': 'body', 'description': 'JSON Patch operation failed: {}'.format(e) } raise_invalid(self.request, **error_details) else: applied_changes = {**requested_changes} updated = {**record} # recursive patch and remove field if null attribute is passed (RFC 7396) if self._is_merge_patch: recursive_update_dict(updated, applied_changes, ignores=(None,)) else: updated.update(**applied_changes) for field, value in applied_changes.items(): has_changed = record.get(field, value) != value if self.schema.is_readonly(field) and has_changed: error_details = { 'name': field, 'description': 'Cannot modify {}'.format(field) } raise_invalid(self.request, **error_details) try: validated = self.schema().deserialize(updated) except colander.Invalid as e: # Transform the errors we got from colander into Cornice errors. # We could not rely on Service schema because the record should be # validated only once the changes are applied for field, error in e.asdict().items(): # pragma: no branch raise_invalid(self.request, name=field, description=error) return validated, applied_changes