Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
stefankoegl committed Jun 18, 2011
0 parents commit df999c9
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
build
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stefan Kögl <stefan@skoegl.net>
26 changes: 26 additions & 0 deletions COPYING
Original file line number Diff line number Diff line change
@@ -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.

11 changes: 11 additions & 0 deletions README
Original file line number Diff line number Diff line change
@@ -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
221 changes: 221 additions & 0 deletions jsonpatch.py
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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,
)

0 comments on commit df999c9

Please sign in to comment.