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/jsonschema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# jsonschema 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.
78 changes: 78 additions & 0 deletions examples/jsonschema/collect_all_errors/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# ---------------------------------------------------------------------
# Iterating over every problem in an instance, instead of stopping at
# the first one. Great for forms and bulk data validation.
# ---------------------------------------------------------------------

import jsonschema
from jsonschema import validate, ValidationError


heading("Lazy validation with iter_errors")
note(
"<code>validate()</code> stops at the first failure. For richer "
"feedback, build a Validator and call <code>iter_errors()</code> "
"to get every problem."
)

# A schema for a small online order.
order_schema = {
"type": "object",
"required": ["order_id", "items", "customer"],
"properties": {
"order_id": {"type": "string", "pattern": r"^ORD-\d{4}$"},
"customer": {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
},
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["sku", "quantity", "price"],
"properties": {
"sku": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"price": {"type": "number", "minimum": 0},
},
},
},
},
}

# An order with several intentional problems.
broken_order = {
"order_id": "12345", # wrong format
"customer": {"name": ""}, # empty name, missing email
"items": [
{"sku": "A-1", "quantity": 0, "price": 9.99}, # quantity < 1
{"sku": "B-2", "quantity": 2, "price": -3.00}, # negative price
],
}

# Pick a validator class for the JSON Schema draft you're targeting.
Validator = jsonschema.Draft202012Validator
validator = Validator(order_schema)

# is_valid is a quick boolean check.
note(f"<code>is_valid</code> says: <strong>{validator.is_valid(broken_order)}</strong>")

# iter_errors yields every ValidationError found in the instance.
errors = sorted(validator.iter_errors(broken_order), key=lambda e: list(e.absolute_path))

note(f"Found <strong>{len(errors)}</strong> problems:")
rows = ["<table border='1' cellpadding='4' style='border-collapse:collapse'>"]
rows.append("<tr><th>Path</th><th>Keyword</th><th>Message</th></tr>")
for error in errors:
path = "/".join(str(p) for p in error.absolute_path) or "(root)"
rows.append(
f"<tr><td><code>{path}</code></td>"
f"<td><code>{error.validator}</code></td>"
f"<td>{error.message}</td></tr>"
)
rows.append("</table>")
display(HTML("".join(rows)), append=True)
1 change: 1 addition & 0 deletions examples/jsonschema/collect_all_errors/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["jsonschema"]
19 changes: 19 additions & 0 deletions examples/jsonschema/collect_all_errors/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Lighter 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/jsonschema/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"validate_a_user_record",
"collect_all_errors",
"schema_references"
]
87 changes: 87 additions & 0 deletions examples/jsonschema/schema_references/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# ---------------------------------------------------------------------
# Splitting a schema into reusable pieces, then wiring them together
# with $ref via a referencing.Registry. This is the modern replacement
# for the deprecated RefResolver.
# ---------------------------------------------------------------------

import jsonschema
from jsonschema import Draft202012Validator, ValidationError
from referencing import Registry, Resource
from referencing.jsonschema import DRAFT202012


heading("Composing schemas with a Registry")
note(
"We define a small <code>address</code> schema once, register it "
"under a URI, then refer to it from a larger <code>person</code> "
"schema using <code>$ref</code>."
)

address_schema = {
"type": "object",
"required": ["street", "city", "postcode"],
"properties": {
"street": {"type": "string"},
"city": {"type": "string"},
"postcode": {"type": "string", "pattern": r"^[A-Z0-9 ]{3,10}$"},
},
}

# Wrap each schema as a Resource bound to a specific JSON Schema draft,
# then build an immutable Registry that maps URIs to these resources.
address_resource = DRAFT202012.create_resource(address_schema)
registry = Registry().with_resource(
uri="https://example.com/schemas/address",
resource=address_resource,
)

person_schema = {
"type": "object",
"required": ["name", "home_address"],
"properties": {
"name": {"type": "string"},
"home_address": {"$ref": "https://example.com/schemas/address"},
"work_address": {"$ref": "https://example.com/schemas/address"},
},
}

# Pass the registry when constructing the Validator so $ref can resolve.
person_validator = Draft202012Validator(person_schema, registry=registry)

heading("A person with two well-formed addresses")
grace = {
"name": "Grace Hopper",
"home_address": {
"street": "1 Cobol Way",
"city": "Arlington",
"postcode": "VA 22201",
},
"work_address": {
"street": "2 Compiler Ln",
"city": "Washington",
"postcode": "DC 20001",
},
}
person_validator.validate(grace)
note("Valid! Both addresses pass the referenced schema.")

heading("A person with a malformed work postcode")
alan = {
"name": "Alan Turing",
"home_address": {
"street": "78 High St",
"city": "Cambridge",
"postcode": "CB2 1TN",
},
"work_address": {
"street": "Bletchley Park",
"city": "Milton Keynes",
"postcode": "lowercase!", # fails the pattern
},
}

problems = list(person_validator.iter_errors(alan))
note(f"Found <strong>{len(problems)}</strong> error(s):")
for error in problems:
path = "/".join(str(p) for p in error.absolute_path) or "(root)"
display(HTML(f"<pre>at {path}: {error.message}</pre>"), append=True)
1 change: 1 addition & 0 deletions examples/jsonschema/schema_references/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["jsonschema"]
19 changes: 19 additions & 0 deletions examples/jsonschema/schema_references/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Lighter setup for example 3: 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)
61 changes: 61 additions & 0 deletions examples/jsonschema/validate_a_user_record/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
A first look at jsonschema: validate user records against a schema.

JSON Schema lets you describe the shape of data declaratively, and
jsonschema is the Python implementation that checks instances against
those schemas. See https://json-schema.org and
https://python-jsonschema.readthedocs.io/ for the full story.
"""
from IPython.core.display import display, HTML

# Package imports for this example.
import jsonschema
from jsonschema import validate, ValidationError


# A schema describing a "user" object: required fields, types, ranges,
# string formats, and an enum for role.
user_schema = {
"type": "object",
"required": ["username", "email", "age", "role"],
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 20,
},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 13, "maximum": 130},
"role": {"enum": ["reader", "author", "admin"]},
},
"additionalProperties": False,
}

heading("1. A valid user passes silently")
good_user = {
"username": "ada_lovelace",
"email": "ada@example.com",
"age": 36,
"role": "author",
}
# validate() raises ValidationError on failure and returns None on success.
validate(instance=good_user, schema=user_schema)
note(f"<code>{good_user}</code> is valid. No exception raised.")

heading("2. An invalid user raises ValidationError")
bad_user = {
"username": "x", # too short
"email": "ada@example.com",
"age": 36,
"role": "wizard", # not in the enum
}

try:
validate(instance=bad_user, schema=user_schema)
except ValidationError as error:
note("Caught a <code>ValidationError</code>:")
display(HTML(f"<pre>{error.message}</pre>"), append=True)
note(
f"Failed at path <code>{list(error.absolute_path)}</code> "
f"on the <code>{error.validator}</code> keyword."
)
1 change: 1 addition & 0 deletions examples/jsonschema/validate_a_user_record/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["jsonschema"]
41 changes: 41 additions & 0 deletions examples/jsonschema/validate_a_user_record/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)