Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/jsonpatch/README.md
Original file line number Diff line number Diff line change
@@ -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.
72 changes: 72 additions & 0 deletions examples/jsonpatch/apply_a_patch/code.py
Original file line number Diff line number Diff line change
@@ -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 <pre> block."""
pretty = json.dumps(obj, indent=2)
display(HTML(f"<strong>{label}</strong><pre>{pretty}</pre>"), 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 <code>op</code>, a <code>path</code> (JSON "
"Pointer), and -- depending on the op -- a <code>value</code> or "
"<code>from</code>. 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 <code>/roles/-</code> means 'append to the array at "
"<code>/roles</code>'. Note that <code>user</code> itself is "
"unchanged -- pass <code>in_place=True</code> 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 "
"<code>jsonpatch.apply_patch</code>. 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)
1 change: 1 addition & 0 deletions examples/jsonpatch/apply_a_patch/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["jsonpatch"]
41 changes: 41 additions & 0 deletions examples/jsonpatch/apply_a_patch/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
65 changes: 65 additions & 0 deletions examples/jsonpatch/diffing_two_documents/code.py
Original file line number Diff line number Diff line change
@@ -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"<strong>{label}</strong><pre>{pretty}</pre>"), 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. <code>jsonpatch.make_patch</code> (also available as "
"<code>JsonPatch.from_diff</code>) 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 <code>before</code> reproduces "
f"<code>after</code> exactly: <strong>{roundtrip == after}</strong>."
)

heading("Patches serialize to JSON for transport")
note(
"A <code>JsonPatch</code> can be turned into a JSON string with "
"<code>str()</code> -- handy when you want to send only the changes "
"over the wire instead of the whole document."
)
display(HTML(f"<pre>{str(diff_patch)}</pre>"), append=True)
1 change: 1 addition & 0 deletions examples/jsonpatch/diffing_two_documents/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["jsonpatch"]
18 changes: 18 additions & 0 deletions examples/jsonpatch/diffing_two_documents/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

5 changes: 5 additions & 0 deletions examples/jsonpatch/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"apply_a_patch",
"diffing_two_documents",
"test_op_and_errors"
]
71 changes: 71 additions & 0 deletions examples/jsonpatch/test_op_and_errors/code.py
Original file line number Diff line number Diff line change
@@ -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 <code>test</code> op that asserts a value at a "
"path. If the assertion fails, the whole patch is rejected with a "
"<code>JsonPatchTestFailed</code> 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: <code>{exc}</code>. "
"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 "
"<code>JsonPatchConflict</code>."
)

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: <code>{exc}</code>.")

heading("Mutating in place when you really mean it")
note(
"By default <code>apply</code> returns a new document. Pass "
"<code>in_place=True</code> 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)
1 change: 1 addition & 0 deletions examples/jsonpatch/test_op_and_errors/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["jsonpatch"]
26 changes: 26 additions & 0 deletions examples/jsonpatch/test_op_and_errors/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)


import json
import jsonpatch


def show_json(label, obj):
pretty = json.dumps(obj, indent=2)
display(HTML(f"<strong>{label}</strong><pre>{pretty}</pre>"), append=True)