diff --git a/docs/user_guide/defining-schema.md b/docs/user_guide/defining-schema.md index 29a4d98..14203a7 100644 --- a/docs/user_guide/defining-schema.md +++ b/docs/user_guide/defining-schema.md @@ -39,3 +39,73 @@ required: - thing - user ``` + +## Checking if a schema is valid + +When authoring a schema, how do you check if you schema is following the expected form? Jupyter Events offers a simple command line tool to validate your schema against its Jupyter Events metaschema. + +First, install the CLI: + +``` +pip install jupyter_events[cli] +``` + +Then, run the CLI against your schema: + +``` +jupyter events validate path/to/my_schema.json +``` + +The output will look like this, if it passes: + +``` +──────────────────────────────────── Validating the following schema ──────────────────────────────────── + + { + "$id": "http://event.jupyter.org/test", + "version": 1, + "title": "Simple Test Schema", + "description": "A simple schema for testing\n", + "type": "object", + "properties": { + "prop": { + "title": "Test Property", + "description": "Test property.", + "type": "string" + } + } + } + +──────────────────────────────────────────────── Results ──────────────────────────────────────────────── + +✔ Nice work! This schema is valid. +``` + +or this if fails: + +``` +──────────────────────────────────── Validating the following schema ──────────────────────────────────── + + { + "$id": "http://event.jupyter.org/test", + "version": 1, + "title": "Simple Test Schema", + "description": "A simple schema for testing\n", + "type": "object", + "properties": { + "__badName": { + "title": "Test Property", + "description": "Test property.", + "type": "string" + } + } + } + +──────────────────────────────────────────────── Results ──────────────────────────────────────────────── +❌ The schema failed to validate. + +We found the following error with your schema: + + '__badName' is an invalid property name because it starts with `__`. Properties starting with + 'dunder' are reserved as special meta-fields for Jupyter Events to use. +``` diff --git a/jupyter_events/cli.py b/jupyter_events/cli.py new file mode 100644 index 0000000..a0d5091 --- /dev/null +++ b/jupyter_events/cli.py @@ -0,0 +1,62 @@ +import json +import pathlib + +import click +from jsonschema import ValidationError +from rich.console import Console +from rich.json import JSON +from rich.markup import escape +from rich.padding import Padding +from rich.style import Style + +from jupyter_events.schema import EventSchema, EventSchemaLoadingError + +console = Console() + + +@click.group() +def main(): + """A simple CLI tool to quickly validate JSON schemas against + Jupyter Event's custom validator. + + You can see Jupyter Event's meta-schema here: + + https://raw.githubusercontent.com/jupyter/jupyter_events/main/jupyter_events/schemas/event-metaschema.yml + """ + pass + + +@click.command() +@click.argument("schema") +def validate(schema): + """Validate a SCHEMA against Jupyter Event's meta schema. + + SCHEMA can be a JSON/YAML string or filepath to a schema. + """ + console.rule("Validating the following schema", style=Style(color="blue")) + # Soft load the schema without validating. + try: + _schema = EventSchema._load_schema(schema) + except EventSchemaLoadingError: + schema_path = pathlib.Path(schema) + _schema = EventSchema._load_schema(schema_path) + # Print what was found. + schema_json = JSON(json.dumps(_schema)) + console.print(Padding(schema_json, (1, 0, 1, 4))) + # Now validate this schema against the meta-schema. + try: + EventSchema(_schema) + console.rule("Results", style=Style(color="green")) + out = Padding( + "[green]\u2714[white] Nice work! This schema is valid.", (1, 0, 1, 0) + ) + console.print(out) + except ValidationError as err: + console.rule("Results", style=Style(color="red")) + console.print("[red]\u274c [white]The schema failed to validate.\n") + console.print("We found the following error with your schema:") + out = escape(str(err)) + console.print(Padding(out, (1, 0, 1, 4))) + + +main.add_command(validate) diff --git a/jupyter_events/logger.py b/jupyter_events/logger.py index ff0f55d..6e72414 100644 --- a/jupyter_events/logger.py +++ b/jupyter_events/logger.py @@ -245,10 +245,11 @@ def add_listener( Parameters ---------- + modified: bool + If True (default), listens to the data after it has been mutated/modified + by the list of modifiers. schema_id: str $id of the schema - version: str - The schema version listener: Callable A callable function/method that executes when the named event occurs. """ diff --git a/jupyter_events/schema.py b/jupyter_events/schema.py index a0a5f57..c1859c3 100644 --- a/jupyter_events/schema.py +++ b/jupyter_events/schema.py @@ -49,9 +49,7 @@ def __init__( self._schema = _schema def __repr__(self): - out = f"Validator class: {self._validator.__class__.__name__}\n" - out += f"Schema: {json.dumps(self._schema, indent=2)}" - return out + return json.dumps(self._schema, indent=2) @staticmethod def _load_schema(schema: Union[dict, str, PurePath]) -> dict: diff --git a/jupyter_events/validators.py b/jupyter_events/validators.py index e64d671..df230ef 100644 --- a/jupyter_events/validators.py +++ b/jupyter_events/validators.py @@ -27,10 +27,11 @@ def validate_schema(schema: dict): except ValidationError as err: reserved_property_msg = " does not match '^(?!__.*)'" if reserved_property_msg in str(err): - bad_property = str(err)[: -(len(reserved_property_msg))] + idx = str(err).find(reserved_property_msg) + bad_property = str(err)[:idx].strip() raise ValidationError( f"{bad_property} is an invalid property name because it " "starts with `__`. Properties starting with 'dunder' " - "are reserved for Jupyter Events." + "are reserved as special meta-fields for Jupyter Events to use." ) raise err diff --git a/pyproject.toml b/pyproject.toml index d4a24b4..5e43b6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ file = 'COPYING.md' [project.urls] Homepage = "http://jupyter.org" +[project.scripts] +jupyter-events = "jupyter_events.cli:main" + [project.optional-dependencies] test = [ "coverage", @@ -47,6 +50,10 @@ test = [ "pytest-cov", "pytest>=6.0", ] +cli = [ + "click", + "rich" +] [tool.hatch.version] path = "jupyter_events/_version.py"