Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jsonpatch create and apply #28

Merged
merged 3 commits into from
Apr 7, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
matrix:
python-version: [ '3.8', '3.9', '3.10' ]
platform: [ubuntu-latest, macOS-latest, windows-latest]
extra: [ 'jinja', 'ttp']
extra: [ 'jinja', 'ttp', 'jsonpatch']
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The following groups are available (more details in the pyproject.toml):
- scrapli
- nornir
- pandas
- jsonpatch
- tui

```bash
Expand Down Expand Up @@ -134,6 +135,18 @@ Many features are not implemented yet and many features will come.

![yaml dump](imgs/yaml-dump.png)


### JSON Patch ([RFC 6902](http://tools.ietf.org/html/rfc6902))

#### create

![JSON Patch create](imgs/jsonpatch-create.png)

#### apply

![JSON Patch apply](imgs/jsonpatch-apply.png)


### Help

![Help QRcode](imgs/nettowel-help.png)
Expand Down
Binary file added imgs/jsonpatch-apply.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/jsonpatch-create.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
163 changes: 163 additions & 0 deletions nettowel/cli/jsonpatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from typing import List

import typer
from rich import print_json, print
from rich.panel import Panel
from rich.columns import Columns
from rich.json import JSON

from nettowel.cli._common import (
auto_complete_paths,
read_yaml,
get_typer_app,
)
from nettowel.exceptions import NettowelInputError

from nettowel import jsonpatch


app = get_typer_app(help="JSON Patch [RFC 6902](http://tools.ietf.org/html/rfc6902)")


@app.command()
def create(
ctx: typer.Context,
src_file_name: typer.FileText = typer.Argument(
...,
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
resolve_path=True,
allow_dash=True,
metavar="src",
help="Source data (YAML/JSON)",
autocompletion=auto_complete_paths,
),
dst_file_name: typer.FileText = typer.Argument(
...,
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
resolve_path=True,
allow_dash=True,
metavar="dst",
help="Destination data (YAML/JSON)",
autocompletion=auto_complete_paths,
),
json: bool = typer.Option(False, "--json", help="JSON output"),
raw: bool = typer.Option(False, "--raw", help="Raw result output"),
only_result: bool = typer.Option(
False, "--print-result-only", help="Only print the result"
),
) -> None:
try:
src = read_yaml(src_file_name)
dst = read_yaml(dst_file_name)

patch = jsonpatch.create(src=src, dst=dst)
patch_str = patch.to_string()
if json or raw:
print_json(json=patch_str)
else:
panels: List[Panel] = []
if not only_result:
panels.append(
Panel(
JSON.from_data(src), title="[yellow]Source", border_style="blue"
)
)
panels.append(
Panel(
JSON.from_data(dst),
title="[yellow]Destrination",
border_style="blue",
)
)
panels.append(
Panel(JSON(patch_str), title="[yellow]JSON Patch", border_style="blue")
)
print(Columns(panels, equal=True))
raise typer.Exit(0)
except NettowelInputError as exc:
typer.echo("Input is not valide", err=True)
typer.echo(str(exc), err=True)
raise typer.Exit(3)


@app.command()
def apply(
ctx: typer.Context,
patch_file_name: typer.FileText = typer.Argument(
...,
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
resolve_path=True,
allow_dash=True,
metavar="patch",
help="Patch opterations (list of mappings) (YAML/JSON)",
autocompletion=auto_complete_paths,
),
data_file_name: typer.FileText = typer.Argument(
...,
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
resolve_path=True,
allow_dash=True,
metavar="data",
help="Data to patch (YAML/JSON)",
autocompletion=auto_complete_paths,
),
json: bool = typer.Option(False, "--json", help="JSON output"),
raw: bool = typer.Option(False, "--raw", help="Raw result output"),
only_result: bool = typer.Option(
False, "--print-result-only", help="Only print the result"
),
) -> None:
try:
patch_data = read_yaml(patch_file_name)
data_input = read_yaml(data_file_name)

new_data = jsonpatch.apply(patch=patch_data, data=data_input)

if json or raw:
print_json(data=new_data)
else:
panels: List[Panel] = []
if not only_result:
panels.append(
Panel(
JSON.from_data(patch_data),
title="[yellow]Patch",
border_style="blue",
)
)
panels.append(
Panel(
JSON.from_data(data_input),
title="[yellow]Input Data",
border_style="blue",
)
)
panels.append(
Panel(
JSON.from_data(data=new_data),
title="[yellow]Patched Data with JSON Patch",
border_style="blue",
)
)
print(Columns(panels, equal=True))
raise typer.Exit(0)
except NettowelInputError as exc:
typer.echo("Input is not valide", err=True)
typer.echo(str(exc), err=True)
raise typer.Exit(3)


if __name__ == "__main__":
app()
2 changes: 2 additions & 0 deletions nettowel/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from nettowel.cli.scrapli import app as scrapli_app
from nettowel.cli.restconf import app as restconf_app
from nettowel.cli.pandas import app as pandas_app
from nettowel.cli.jsonpatch import app as jsonpatch_app
from nettowel.cli.help import get_qrcode, HELP_MARKDOWN

from nettowel.exceptions import NettowelDependencyMissing
Expand All @@ -40,6 +41,7 @@
(scrapli_app, "scrapli"),
(restconf_app, "restconf"),
(pandas_app, "pandas"),
(jsonpatch_app, "jsonpatch"),
]:
app.add_typer(subapp, name=name)

Expand Down
46 changes: 46 additions & 0 deletions nettowel/jsonpatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Any, List, Dict
from nettowel.logger import log
from nettowel._common import needs

_module = "jsonpatch"

try:
from jsonpatch import JsonPatch

log.debug("Successfully imported %s", _module)
JSONPATCH_INSTALLED = True

except ImportError:
log.warning("Failed to import %s", _module)
JSONPATCH_INSTALLED = False


def create(src: Any, dst: Any) -> "JsonPatch":
"""Create a JSON patch [RFC 6902](http://tools.ietf.org/html/rfc6902)

Args:
src (any): Data source datastructure
dst (any): Data destination datastructure

Returns:
JsonPatch: JsonPatch object containing the patch
"""
needs(JSONPATCH_INSTALLED, "jsonpatch", _module)
patch = JsonPatch.from_diff(src=src, dst=dst)
return patch


def apply(patch: List[Dict[str, Any]], data: Any) -> Any:
"""Apply a JSON patch [RFC 6902](http://tools.ietf.org/html/rfc6902)

Args:
patch (list[dict]): List of patch instructions
data (any): Data to apply the patch onto

Returns:
result: Updated data object
"""
needs(JSONPATCH_INSTALLED, "jsonpatch", _module)
jp = JsonPatch(patch)
result = jp.apply(data)
return result
36 changes: 31 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ nornir-netmiko = {version = "^1.0.0", optional = true}
nornir-rich = {version = "^0.1", optional = true}
pandas = {version = "^2", optional = true}
trogon = {version = "^0.5.0", optional = true}
jsonpatch = {version = "^1.33", optional = true}


[tool.poetry.dev-dependencies]
black = "^22|^23|^24"
Expand All @@ -56,6 +58,7 @@ scrapli = ["scrapli"]
nornir = ["nornir", "nornir-napalm", "nornir-scrapli", "nornir-utils", "nornir-jinja2", "nornir-pyxl", "nornir-http", "nornir-netmiko", "nornir-rich"]
pandas = ["pandas"]
tui = ["trogon"]
jsonpatch = ["jsonpatch"]
full = [
"Jinja2",
"jinja2schema",
Expand All @@ -74,7 +77,8 @@ full = [
"nornir-netmiko",
"nornir-rich",
"pandas",
"trogon"
"trogon",
"jsonpatch"
]

[tool.poetry.scripts]
Expand Down
7 changes: 7 additions & 0 deletions tests/jsonpatch/data1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interfaces:
- name: Loopback0
addr: 10.10.10.10
mask: 255.255.255.255
- name: Ethernet0/1
addr: 192.168.1.1
mask: 255.255.255.0
7 changes: 7 additions & 0 deletions tests/jsonpatch/data2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interfaces:
- name: Loopback0
addr: 10.10.10.10
mask: 255.255.255.255
- name: Ethernet0/1
addr: 192.168.1.1
mask: 255.255.255.192
7 changes: 7 additions & 0 deletions tests/jsonpatch/data_patch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"op": "replace",
"path": "/interfaces/1/mask",
"value": "255.255.255.192"
}
]
Loading
Loading