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)