summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--AUTHORS1
-rw-r--r--COPYING26
-rw-r--r--README11
-rw-r--r--jsonpatch.py221
-rw-r--r--setup.py33
6 files changed, 294 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..27ffc2f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.pyc
+build
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..2e5ad67
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Stefan Kögl <stefan@skoegl.net>
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..491196d
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,26 @@
+Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/README b/README
new file mode 100644
index 0000000..24750ed
--- /dev/null
+++ b/README
@@ -0,0 +1,11 @@
+
+python-json-patch: Applying JSON Patches
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Library to apply JSON Patches according to
+http://tools.ietf.org/html/draft-pbryan-json-patch-01
+
+See Sourcecode for Examples
+
+Website: https://github.com/stefankoegl/python-json-patch
+Repository: https://github.com/stefankoegl/python-json-patch.git
diff --git a/jsonpatch.py b/jsonpatch.py
new file mode 100644
index 0000000..3136567
--- /dev/null
+++ b/jsonpatch.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+#
+# python-json-patch - An implementation of the JSON Patch format
+# https://github.com/stefankoegl/python-json-patch
+#
+# Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+"""Apply JSON-Patches according to http://tools.ietf.org/html/draft-pbryan-json-patch-01"""
+
+# Will be parsed by setup.py to determine package metadata
+__author__ = 'Stefan Kögl <stefan@skoegl.net>'
+__version__ = '0.1'
+__website__ = 'https://github.com/stefankoegl/python-json-patch'
+__license__ = 'Modified BSD License'
+
+
+import copy
+import json
+
+
+class JsonPatchException(Exception):
+ pass
+
+
+class JsonPatch(object):
+ """ A JSON Patch is a list of Patch Operations """
+
+ def __init__(self, patch):
+ self.patch = patch
+
+ self.OPERATIONS = {
+ 'remove': RemoveOperation,
+ 'add': AddOperation,
+ 'replace': ReplaceOperation,
+ }
+
+
+ @classmethod
+ def from_string(cls, patch_str):
+ patch = json.loads(patch_str)
+ return cls(patch)
+
+
+ def apply(self, obj):
+ """ Applies the patch to a copy of the given object """
+
+ obj = copy.deepcopy(obj)
+
+ for operation in self.patch:
+ op = self._get_operation(operation)
+ op.apply(obj)
+
+ return obj
+
+
+ def _get_operation(self, operation):
+ for action, op_cls in self.OPERATIONS.items():
+ if action in operation:
+ location = operation[action]
+ op = op_cls(location, operation)
+ return op
+
+ raise JsonPatchException("invalid operation '%s'" % operation)
+
+
+
+class PatchOperation(object):
+ """ A single operation inside a JSON Patch """
+
+ def __init__(self, location, operation):
+ self.location = location
+ self.operation = operation
+
+
+ def locate(self, obj, location, last_must_exist=True):
+ """ Walks through the object according to location
+
+ Returns the last step as (sub-object, last location-step) """
+
+ parts = location.split('/')
+ if parts.pop(0) != '':
+ raise JsonPatchException('location must starts with /')
+
+ for part in parts[:-1]:
+ obj, loc_part = self._step(obj, part)
+
+ _, last_loc = self._step(obj, parts[-1], must_exist=last_must_exist)
+ return obj, last_loc
+
+
+ def _step(self, obj, loc_part, must_exist=True):
+ """ Goes one step in a locate() call """
+
+ # Its not clear if a location "1" should be considered as 1 or "1"
+ # We prefer the integer-variant if possible
+ part_variants = self._try_parse(loc_part) + [loc_part]
+
+ for variant in part_variants:
+ try:
+ return obj[variant], variant
+ except:
+ continue
+
+ if must_exist:
+ raise JsonPatchException('key %s not found' % loc_part)
+ else:
+ return obj, part_variants[0]
+
+
+ @staticmethod
+ def _try_parse(val, cls=int):
+ try:
+ return [cls(val)]
+ except:
+ return []
+
+
+class RemoveOperation(PatchOperation):
+ """ Removes an object property or an array element
+
+ >>> obj = { 'baz': 'qux', 'foo': 'bar' }
+ >>> patch = JsonPatch( [ { 'remove': '/baz' } ] )
+ >>> patch.apply(obj)
+ {'foo': 'bar'}
+
+ >>> obj = { 'foo': [ 'bar', 'qux', 'baz' ] }
+ >>> patch = JsonPatch( [ { "remove": "/foo/1" } ] )
+ >>> patch.apply(obj)
+ {'foo': ['bar', 'baz']}
+ """
+
+ def apply(self, obj):
+ subobj, part = self.locate(obj, self.location)
+ del subobj[part]
+
+
+class AddOperation(PatchOperation):
+ """ Adds an object property or an array element
+
+ >>> obj = { "foo": "bar" }
+ >>> patch = JsonPatch([ { "add": "/baz", "value": "qux" } ])
+ >>> patch.apply(obj)
+ {'foo': 'bar', 'baz': 'qux'}
+
+ >>> obj = { "foo": [ "bar", "baz" ] }
+ >>> patch = JsonPatch([ { "add": "/foo/1", "value": "qux" } ])
+ >>> patch.apply(obj)
+ {'foo': ['bar', 'qux', 'baz']}
+ """
+
+ def apply(self, obj):
+ value = self.operation["value"]
+ subobj, part = self.locate(obj, self.location, last_must_exist=False)
+
+ if isinstance(subobj, list):
+ if part > len(subobj) or part < 0:
+ raise JsonPatchException("can't insert outside of list")
+
+ subobj.insert(part, value)
+
+ elif isinstance(subobj, dict):
+ if part in subobj:
+ raise JsonPatchException("object '%s' already exists" % part)
+
+ subobj[part] = value
+
+ else:
+ raise JsonPatchException("can't add to type '%s'" % subobj.__class__.__name__)
+
+
+class ReplaceOperation(PatchOperation):
+ """ Replaces a value
+
+ >>> obj = { "baz": "qux", "foo": "bar" }
+ >>> patch = JsonPatch([ { "replace": "/baz", "value": "boo" } ])
+ >>> patch.apply(obj)
+ {'foo': 'bar', 'baz': 'boo'}
+ """
+
+ def apply(self, obj):
+ location = self. operation["replace"]
+ value = self.operation["value"]
+ subobj, part = self.locate(obj, self.location)
+
+ if isinstance(subobj, list):
+ if part > len(subobj) or part < 0:
+ raise JsonPatchException("can't replace outside of list")
+
+ elif isinstance(subobj, dict):
+ if not part in subobj:
+ raise JsonPatchException("can't replace non-existant object '%s'" % part)
+
+ else:
+ raise JsonPatchException("can't replace in type '%s'" % subobj.__class__.__name__)
+
+ subobj[part] = value
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..c9a3900
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+import re
+
+src = open('jsonpatch.py').read()
+metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", src))
+docstrings = re.findall('"""(.*)"""', src)
+
+PACKAGE = 'jsonpatch'
+
+MODULES = (
+ 'jsonpatch',
+)
+
+AUTHOR_EMAIL = metadata['author']
+VERSION = metadata['version']
+WEBSITE = metadata['website']
+LICENSE = metadata['license']
+DESCRIPTION = docstrings[0]
+
+# Extract name and e-mail ("Firstname Lastname <mail@example.org>")
+AUTHOR, EMAIL = re.match(r'(.*) <(.*)>', AUTHOR_EMAIL).groups()
+
+setup(name=PACKAGE,
+ version=VERSION,
+ description=DESCRIPTION,
+ author=AUTHOR,
+ author_email=EMAIL,
+ license=LICENSE,
+ url=WEBSITE,
+ py_modules=MODULES,
+)