diff --git a/examples/jsonpatch/README.md b/examples/jsonpatch/README.md new file mode 100644 index 0000000..a294bee --- /dev/null +++ b/examples/jsonpatch/README.md @@ -0,0 +1,18 @@ +# jsonpatch Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/jsonpatch/apply_a_patch/code.py b/examples/jsonpatch/apply_a_patch/code.py new file mode 100644 index 0000000..0d0e3c4 --- /dev/null +++ b/examples/jsonpatch/apply_a_patch/code.py @@ -0,0 +1,72 @@ +""" +A first look at jsonpatch: applying RFC 6902 operations to a JSON document. + +JSON Patch is a small language for describing changes to a JSON document +as a list of operations like `add`, `remove`, `replace`, `move`, `copy`, +and `test`. Each operation targets a location via a JSON Pointer path. + +See the spec at https://tools.ietf.org/html/rfc6902 and the package docs +at https://python-json-patch.readthedocs.io/. +""" +from IPython.core.display import display, HTML + +# Package imports for this example. +import json +import jsonpatch + + + +def show_json(label, obj): + """Render a labeled JSON blob as a
 block."""
+    pretty = json.dumps(obj, indent=2)
+    display(HTML(f"{label}
{pretty}
"), append=True) + + +# A tiny user profile we'd like to update. +user = { + "name": "Ada Lovelace", + "email": "ada@example.com", + "roles": ["author"], + "active": False, +} + +heading("1. A patch is a list of operations") +note( + "Each operation has an op, a path (JSON " + "Pointer), and -- depending on the op -- a value or " + "from. Here we promote Ada to admin, switch her on, " + "and tidy up her contact info." +) + +operations = [ + {"op": "replace", "path": "/active", "value": True}, + {"op": "add", "path": "/roles/-", "value": "admin"}, + {"op": "add", "path": "/contact", "value": {"email": "ada@example.com"}}, + {"op": "remove", "path": "/email"}, +] + +patch = jsonpatch.JsonPatch(operations) + +show_json("Original document", user) +show_json("Patch", operations) + +# `apply` returns a new document by default; the original is untouched. +updated = patch.apply(user) +show_json("Patched document", updated) + +note( + "The path /roles/- means 'append to the array at " + "/roles'. Note that user itself is " + "unchanged -- pass in_place=True to mutate it." +) + +heading("2. The one-shot helper: apply_patch") +note( + "If you only need to apply a patch once, skip the object and use " + "jsonpatch.apply_patch. It accepts either a list of " + "operations or a JSON string." +) + +quick_patch = '[{"op": "replace", "path": "/name", "value": "Ada L."}]' +shorter = jsonpatch.apply_patch(updated, quick_patch) +show_json("After a one-shot patch from a JSON string", shorter) diff --git a/examples/jsonpatch/apply_a_patch/config.toml b/examples/jsonpatch/apply_a_patch/config.toml new file mode 100644 index 0000000..79df5f7 --- /dev/null +++ b/examples/jsonpatch/apply_a_patch/config.toml @@ -0,0 +1 @@ +packages = ["jsonpatch"] diff --git a/examples/jsonpatch/apply_a_patch/setup.py b/examples/jsonpatch/apply_a_patch/setup.py new file mode 100644 index 0000000..b4f3ee1 --- /dev/null +++ b/examples/jsonpatch/apply_a_patch/setup.py @@ -0,0 +1,41 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) diff --git a/examples/jsonpatch/diffing_two_documents/code.py b/examples/jsonpatch/diffing_two_documents/code.py new file mode 100644 index 0000000..6dbf2cc --- /dev/null +++ b/examples/jsonpatch/diffing_two_documents/code.py @@ -0,0 +1,65 @@ +# --------------------------------------------------------------------- +# Section 2: Generate a patch automatically by diffing two documents. +# --------------------------------------------------------------------- +import json +import jsonpatch + + +def show_json(label, obj): + pretty = json.dumps(obj, indent=2) + display(HTML(f"{label}
{pretty}
"), append=True) + + +heading("Diffing two versions of a document") +note( + "Often you don't write a patch by hand -- you have an old and a new " + "version of some JSON, and you want the minimal patch that turns one " + "into the other. jsonpatch.make_patch (also available as " + "JsonPatch.from_diff) does exactly that." +) + +# An order before and after the customer edited their cart. +before = { + "order_id": "A-1042", + "customer": {"name": "Grace H.", "city": "Yorktown"}, + "items": [ + {"sku": "BOOK-01", "qty": 1, "price": 12.50}, + {"sku": "MUG-07", "qty": 2, "price": 8.00}, + ], + "coupon": "SPRING10", +} + +after = { + "order_id": "A-1042", + "customer": {"name": "Grace Hopper", "city": "Yorktown"}, + "items": [ + {"sku": "BOOK-01", "qty": 2, "price": 12.50}, + {"sku": "MUG-07", "qty": 2, "price": 8.00}, + {"sku": "PEN-03", "qty": 5, "price": 1.25}, + ], +} + +show_json("Before", before) +show_json("After", after) + +# Compute the patch that transforms `before` into `after`. +diff_patch = jsonpatch.make_patch(before, after) + +# JsonPatch is iterable; each element is an operation dict. +note("Generated patch (the smallest set of operations that takes us from before to after):") +show_json("Operations", list(diff_patch)) + +# Sanity check: applying the patch reproduces `after` exactly. +roundtrip = diff_patch.apply(before) +note( + "Applying the patch to before reproduces " + f"after exactly: {roundtrip == after}." +) + +heading("Patches serialize to JSON for transport") +note( + "A JsonPatch can be turned into a JSON string with " + "str() -- handy when you want to send only the changes " + "over the wire instead of the whole document." +) +display(HTML(f"
{str(diff_patch)}
"), append=True) diff --git a/examples/jsonpatch/diffing_two_documents/config.toml b/examples/jsonpatch/diffing_two_documents/config.toml new file mode 100644 index 0000000..79df5f7 --- /dev/null +++ b/examples/jsonpatch/diffing_two_documents/config.toml @@ -0,0 +1 @@ +packages = ["jsonpatch"] diff --git a/examples/jsonpatch/diffing_two_documents/setup.py b/examples/jsonpatch/diffing_two_documents/setup.py new file mode 100644 index 0000000..d11b867 --- /dev/null +++ b/examples/jsonpatch/diffing_two_documents/setup.py @@ -0,0 +1,18 @@ +"""Lightweight setup for example 2: same names as cell 1, no IPython shim.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + diff --git a/examples/jsonpatch/order.json b/examples/jsonpatch/order.json new file mode 100644 index 0000000..adeb391 --- /dev/null +++ b/examples/jsonpatch/order.json @@ -0,0 +1,5 @@ +[ + "apply_a_patch", + "diffing_two_documents", + "test_op_and_errors" +] diff --git a/examples/jsonpatch/test_op_and_errors/code.py b/examples/jsonpatch/test_op_and_errors/code.py new file mode 100644 index 0000000..d3b45c6 --- /dev/null +++ b/examples/jsonpatch/test_op_and_errors/code.py @@ -0,0 +1,71 @@ +# --------------------------------------------------------------------- +# Section 3: Optimistic updates with `test`, plus what happens on failure. +# --------------------------------------------------------------------- + +heading("The 'test' operation: optimistic concurrency control") +note( + "RFC 6902 includes a test op that asserts a value at a " + "path. If the assertion fails, the whole patch is rejected with a " + "JsonPatchTestFailed exception. This is the JSON Patch " + "way of saying 'only apply my changes if the document still looks " + "like I expect.'" +) + +inventory = { + "sku": "MUG-07", + "stock": 12, + "price": 8.00, +} + +# Imagine two clients trying to update stock at the same time. Each +# sends a patch that first *tests* the stock level it observed. +patch_alice = jsonpatch.JsonPatch([ + {"op": "test", "path": "/stock", "value": 12}, + {"op": "replace", "path": "/stock", "value": 10}, +]) + +patch_bob = jsonpatch.JsonPatch([ + {"op": "test", "path": "/stock", "value": 12}, + {"op": "replace", "path": "/stock", "value": 9}, +]) + +# Alice applies first -- her test passes. +inventory = patch_alice.apply(inventory) +show_json("After Alice's patch", inventory) + +# Now Bob's test fails: stock is no longer 12. +try: + patch_bob.apply(inventory) +except jsonpatch.JsonPatchTestFailed as exc: + note( + f"Bob's patch was rejected: {exc}. " + "He'd need to re-read the document, re-test, and try again." + ) + +heading("Other failure modes") +note( + "Patches can also fail because a path doesn't exist or an operation " + "isn't valid for the target. These raise " + "JsonPatchConflict." +) + +bad_patch = jsonpatch.JsonPatch([ + {"op": "remove", "path": "/discount"}, # no such key +]) + +try: + bad_patch.apply(inventory) +except jsonpatch.JsonPatchConflict as exc: + note(f"Removing a missing key raised: {exc}.") + +heading("Mutating in place when you really mean it") +note( + "By default apply returns a new document. Pass " + "in_place=True if you want to mutate the input directly." +) + +bump_price = jsonpatch.JsonPatch([ + {"op": "replace", "path": "/price", "value": 8.50}, +]) +bump_price.apply(inventory, in_place=True) +show_json("Inventory after in-place price bump", inventory) diff --git a/examples/jsonpatch/test_op_and_errors/config.toml b/examples/jsonpatch/test_op_and_errors/config.toml new file mode 100644 index 0000000..79df5f7 --- /dev/null +++ b/examples/jsonpatch/test_op_and_errors/config.toml @@ -0,0 +1 @@ +packages = ["jsonpatch"] diff --git a/examples/jsonpatch/test_op_and_errors/setup.py b/examples/jsonpatch/test_op_and_errors/setup.py new file mode 100644 index 0000000..8493321 --- /dev/null +++ b/examples/jsonpatch/test_op_and_errors/setup.py @@ -0,0 +1,26 @@ +"""Lightweight setup for example 3.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import json +import jsonpatch + + +def show_json(label, obj): + pretty = json.dumps(obj, indent=2) + display(HTML(f"{label}
{pretty}
"), append=True)