From 3085fcf8d1f5d7d117b976e398bf2e4f10dc65ca Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 09:53:57 -0700 Subject: [PATCH 01/22] Output formatting --- netmiko/cli_tools/cli_helpers.py | 58 +++++++++++++++++++++++++++++++ netmiko/cli_tools/netmiko_show.py | 26 ++++++++------ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/netmiko/cli_tools/cli_helpers.py b/netmiko/cli_tools/cli_helpers.py index d7ddd7e5e..c3a75f973 100644 --- a/netmiko/cli_tools/cli_helpers.py +++ b/netmiko/cli_tools/cli_helpers.py @@ -1,9 +1,16 @@ from typing import Any, Dict +import json +from rich import print, print_json +from rich.console import Console +from rich.panel import Panel +from rich.theme import Theme + from netmiko import ConnectHandler from netmiko.utilities import load_devices, obtain_all_devices from netmiko.cli_tools import ERROR_PATTERN + def ssh_conn(device_name, device_params, cli_command=None, cfg_command=None): try: output = "" @@ -58,3 +65,54 @@ def update_device_params(params, username=None, password=None, secret=None): if secret: params["secret"] = secret return params + + +def output_text(results): + # Create a custom theme for consistent coloring + custom_theme = Theme({ + "device_name": "bold magenta", + "border": "cyan", + "output": "green", + }) + + console = Console(theme=custom_theme) + for device_name, output in results.items(): + panel = Panel( + output, + title=device_name, + expand=False, + border_style="border", + title_align="left", + padding=(1, 1), + ) + console.print() + console.print(panel) + console.print() +# print() +# print("-" * 40) +# print(device_name) +# print(output) +# print("-" * 40) + + +def output_json(results): + for device_name, output in results.items(): + if output_json: + # Try to parse the output as JSON + json_data = json.loads(output) + print_json(json.dumps({device_name: json_data})) + + +def output_yaml(results): + pass + + +def output_dispatcher(out_format, results): + + output_functions = { + 'text': output_text, + 'json': output_json, + 'yaml': output_yaml + } + func = output_functions.get(out_format, output_text) + return func(results) diff --git a/netmiko/cli_tools/netmiko_show.py b/netmiko/cli_tools/netmiko_show.py index 7b45de55c..66ffaa882 100755 --- a/netmiko/cli_tools/netmiko_show.py +++ b/netmiko/cli_tools/netmiko_show.py @@ -8,6 +8,7 @@ from datetime import datetime from getpass import getpass +from rich import print from netmiko.utilities import load_devices, display_inventory from netmiko.utilities import obtain_netmiko_filename, write_tmp_file, ensure_dir_exists @@ -15,6 +16,7 @@ from netmiko.utilities import SHOW_RUN_MAPPER from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ from netmiko.cli_tools.cli_helpers import obtain_devices, update_device_params, ssh_conn +from netmiko.cli_tools.cli_helpers import output_dispatcher def grepx(files, pattern, grep_options, use_colors=True): @@ -70,6 +72,9 @@ def parse_arguments(args): parser.add_argument( "--hide-failed", help="Hide failed devices", action="store_true" ) + parser.add_argument( + "--json", help="Output results in JSON format", action="store_true" + ) parser.add_argument("--version", help="Display version", action="store_true") cli_args = parser.parse_args(args) if not cli_args.list_devices and not cli_args.version: @@ -102,6 +107,7 @@ def main(args): display_inventory(my_devices) return 0 + output_json = cli_args.json cli_command = cli_args.cmd cmd_arg = False if cli_command: @@ -147,16 +153,14 @@ def main(args): device_name, output = future.result() results[device_name] = output - netmiko_base_dir, netmiko_full_dir = find_netmiko_dir() - ensure_dir_exists(netmiko_base_dir) - ensure_dir_exists(netmiko_full_dir) - for device_name, output in results.items(): + # OUTPUT PROCESSING ##### + out_format = "text" + if output_json: + out_format = "json" + # elif output_yaml: + # out_format = "yaml" + output_dispatcher(out_format, results) - file_name = write_tmp_file(device_name, output) - if ERROR_PATTERN not in output: - my_files.append(file_name) - else: - failed_devices.append(device_name) else: for device_name in devices: file_name = obtain_netmiko_filename(device_name) @@ -170,8 +174,8 @@ def main(args): else: failed_devices.append(device_name) - grep_options = [] - grepx(my_files, pattern, grep_options) + # grep_options = [] + # grepx(my_files, pattern, grep_options) if cli_args.display_runtime: print("Total time: {0}".format(datetime.now() - start_time)) From 41d324b03c107634d3bc35909fda8cc91ab681cb Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 10:13:34 -0700 Subject: [PATCH 02/22] Creating separate module for output formatting --- .../cli_tools/{cli_helpers.py => helpers.py} | 57 ------------------- netmiko/cli_tools/netmiko_show.py | 4 +- netmiko/cli_tools/outputters.py | 57 +++++++++++++++++++ setup.cfg | 5 +- 4 files changed, 63 insertions(+), 60 deletions(-) rename netmiko/cli_tools/{cli_helpers.py => helpers.py} (61%) create mode 100644 netmiko/cli_tools/outputters.py diff --git a/netmiko/cli_tools/cli_helpers.py b/netmiko/cli_tools/helpers.py similarity index 61% rename from netmiko/cli_tools/cli_helpers.py rename to netmiko/cli_tools/helpers.py index c3a75f973..d5f80d003 100644 --- a/netmiko/cli_tools/cli_helpers.py +++ b/netmiko/cli_tools/helpers.py @@ -1,16 +1,10 @@ from typing import Any, Dict -import json -from rich import print, print_json -from rich.console import Console -from rich.panel import Panel -from rich.theme import Theme from netmiko import ConnectHandler from netmiko.utilities import load_devices, obtain_all_devices from netmiko.cli_tools import ERROR_PATTERN - def ssh_conn(device_name, device_params, cli_command=None, cfg_command=None): try: output = "" @@ -65,54 +59,3 @@ def update_device_params(params, username=None, password=None, secret=None): if secret: params["secret"] = secret return params - - -def output_text(results): - # Create a custom theme for consistent coloring - custom_theme = Theme({ - "device_name": "bold magenta", - "border": "cyan", - "output": "green", - }) - - console = Console(theme=custom_theme) - for device_name, output in results.items(): - panel = Panel( - output, - title=device_name, - expand=False, - border_style="border", - title_align="left", - padding=(1, 1), - ) - console.print() - console.print(panel) - console.print() -# print() -# print("-" * 40) -# print(device_name) -# print(output) -# print("-" * 40) - - -def output_json(results): - for device_name, output in results.items(): - if output_json: - # Try to parse the output as JSON - json_data = json.loads(output) - print_json(json.dumps({device_name: json_data})) - - -def output_yaml(results): - pass - - -def output_dispatcher(out_format, results): - - output_functions = { - 'text': output_text, - 'json': output_json, - 'yaml': output_yaml - } - func = output_functions.get(out_format, output_text) - return func(results) diff --git a/netmiko/cli_tools/netmiko_show.py b/netmiko/cli_tools/netmiko_show.py index 66ffaa882..dab6b314f 100755 --- a/netmiko/cli_tools/netmiko_show.py +++ b/netmiko/cli_tools/netmiko_show.py @@ -15,8 +15,8 @@ from netmiko.utilities import find_netmiko_dir from netmiko.utilities import SHOW_RUN_MAPPER from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ -from netmiko.cli_tools.cli_helpers import obtain_devices, update_device_params, ssh_conn -from netmiko.cli_tools.cli_helpers import output_dispatcher +from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn +from netmiko.cli_tools.outputters import output_dispatcher def grepx(files, pattern, grep_options, use_colors=True): diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py new file mode 100644 index 000000000..9ee7f824b --- /dev/null +++ b/netmiko/cli_tools/outputters.py @@ -0,0 +1,57 @@ +import json +from rich import print_json +from rich.console import Console +from rich.panel import Panel +from rich.theme import Theme + +CUSTOM_THEME = Theme( + { + "device_name": "bold magenta", + "border": "cyan", + "output": "green", + } +) + + +def output_text(results): + # Create a custom theme for consistent coloring + + console = Console(theme=CUSTOM_THEME) + for device_name, output in results.items(): + panel = Panel( + output, + title=device_name, + expand=False, + border_style="border", + title_align="left", + padding=(1, 1), + ) + console.print() + console.print(panel) + console.print() + + +# print() +# print("-" * 40) +# print(device_name) +# print(output) +# print("-" * 40) + + +def output_json(results): + for device_name, output in results.items(): + if output_json: + # Try to parse the output as JSON + json_data = json.loads(output) + print_json(json.dumps({device_name: json_data})) + + +def output_yaml(results): + pass + + +def output_dispatcher(out_format, results): + + output_functions = {"text": output_text, "json": output_json, "yaml": output_yaml} + func = output_functions.get(out_format, output_text) + return func(results) diff --git a/setup.cfg b/setup.cfg index e474b8824..1de3c3ce1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,5 +47,8 @@ ignore_errors = True [mypy-netmiko.cli_tools.netmiko_show] ignore_errors = True -[mypy-netmiko.cli_tools.cli_helpers] +[mypy-netmiko.cli_tools.helpers] +ignore_errors = True + +[mypy-netmiko.cli_tools.outputters] ignore_errors = True From 1f5396d7857c2ca5b7662874bc7464ced0366b83 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 10:25:10 -0700 Subject: [PATCH 03/22] Improving outputter formatting --- netmiko/cli_tools/outputters.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 9ee7f824b..65165b6d9 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -31,19 +31,28 @@ def output_text(results): console.print() -# print() -# print("-" * 40) -# print(device_name) -# print(output) -# print("-" * 40) - - def output_json(results): for device_name, output in results.items(): if output_json: - # Try to parse the output as JSON - json_data = json.loads(output) - print_json(json.dumps({device_name: json_data})) + try: + json_data = json.loads(output) + print_json(json.dumps({device_name: json_data})) + except json.decoder.JSONDecodeError as e: + msg = f""" + +Error processing output for device: {device_name} + +In order to use display the output in JSON (--json arg), you must provide the output in JSON +format (i.e. device supports '| json' or parse with TextFSM) + +""" + new_exception = json.decoder.JSONDecodeError( + msg, + e.doc, + e.pos + ) + # Raise the new exception, chaining it to the original one + raise new_exception from e def output_yaml(results): From 4f2f56a906b021914cf6c81ef543d965cbff8571 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 11:54:08 -0700 Subject: [PATCH 04/22] Working --raw mode --- netmiko/cli_tools/netmiko_show.py | 4 ++ netmiko/cli_tools/outputters.py | 66 +++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/netmiko/cli_tools/netmiko_show.py b/netmiko/cli_tools/netmiko_show.py index dab6b314f..f7334d41d 100755 --- a/netmiko/cli_tools/netmiko_show.py +++ b/netmiko/cli_tools/netmiko_show.py @@ -75,6 +75,7 @@ def parse_arguments(args): parser.add_argument( "--json", help="Output results in JSON format", action="store_true" ) + parser.add_argument("--raw", help="Display raw output", action="store_true") parser.add_argument("--version", help="Display version", action="store_true") cli_args = parser.parse_args(args) if not cli_args.list_devices and not cli_args.version: @@ -108,6 +109,7 @@ def main(args): return 0 output_json = cli_args.json + output_raw = cli_args.raw cli_command = cli_args.cmd cmd_arg = False if cli_command: @@ -157,6 +159,8 @@ def main(args): out_format = "text" if output_json: out_format = "json" + elif output_raw: + out_format = "raw" # elif output_yaml: # out_format = "yaml" output_dispatcher(out_format, results) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 65165b6d9..cde58cb51 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -1,8 +1,9 @@ import json -from rich import print_json +import rich from rich.console import Console from rich.panel import Panel from rich.theme import Theme +from rich.syntax import Syntax CUSTOM_THEME = Theme( { @@ -13,9 +14,22 @@ ) -def output_text(results): - # Create a custom theme for consistent coloring +def output_raw(results): + border_color = CUSTOM_THEME.styles.get("border").color.name + + if len(results) == 1: + for device_name, output in results.items(): + print(output) + else: + for device_name, output in results.items(): + banner = "-" * len(device_name) + rich.print(f"[bold {border_color}]{device_name}[/]") + rich.print(f"[{border_color}]{banner}[/]") + print(output) + print() + +def output_text(results): console = Console(theme=CUSTOM_THEME) for device_name, output in results.items(): panel = Panel( @@ -32,13 +46,30 @@ def output_text(results): def output_json(results): + console = Console(theme=CUSTOM_THEME) for device_name, output in results.items(): - if output_json: - try: - json_data = json.loads(output) - print_json(json.dumps({device_name: json_data})) - except json.decoder.JSONDecodeError as e: - msg = f""" + try: + json_data = json.loads(output) + formatted_json = json.dumps({device_name: json_data}, indent=2) + + # Create a syntax-highlighted version of the JSON + syntax = Syntax(formatted_json, "json", theme="monokai") + + panel = Panel( + syntax, + title=device_name, + expand=False, + border_style="border", + title_align="left", + padding=(1, 3), + width=80, + ) + console.print() + console.print(panel) + console.print() + + except json.decoder.JSONDecodeError as e: + msg = f""" Error processing output for device: {device_name} @@ -46,13 +77,9 @@ def output_json(results): format (i.e. device supports '| json' or parse with TextFSM) """ - new_exception = json.decoder.JSONDecodeError( - msg, - e.doc, - e.pos - ) - # Raise the new exception, chaining it to the original one - raise new_exception from e + new_exception = json.decoder.JSONDecodeError(msg, e.doc, e.pos) + # Raise the new exception, chaining it to the original one + raise new_exception from e def output_yaml(results): @@ -61,6 +88,11 @@ def output_yaml(results): def output_dispatcher(out_format, results): - output_functions = {"text": output_text, "json": output_json, "yaml": output_yaml} + output_functions = { + "text": output_text, + "json": output_json, + "yaml": output_yaml, + "raw": output_raw, + } func = output_functions.get(out_format, output_text) return func(results) From 627a26b2c0df65805261ce6046c6bb47795795a5 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 16:49:59 -0700 Subject: [PATCH 05/22] Working raw and json --- netmiko/cli_tools/netmiko_show.py | 4 ++- netmiko/cli_tools/outputters.py | 49 +++++++++++++++++++------------ 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/netmiko/cli_tools/netmiko_show.py b/netmiko/cli_tools/netmiko_show.py index f7334d41d..18fe5c81c 100755 --- a/netmiko/cli_tools/netmiko_show.py +++ b/netmiko/cli_tools/netmiko_show.py @@ -157,7 +157,9 @@ def main(args): # OUTPUT PROCESSING ##### out_format = "text" - if output_json: + if output_json and output_raw: + out_format = "json_raw" + elif output_json: out_format = "json" elif output_raw: out_format = "raw" diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index cde58cb51..cf0c1d8cd 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -45,28 +45,30 @@ def output_text(results): console.print() -def output_json(results): +def output_json(results, raw=False): console = Console(theme=CUSTOM_THEME) + border_color = CUSTOM_THEME.styles.get("border").color.name + for device_name, output in results.items(): try: json_data = json.loads(output) formatted_json = json.dumps({device_name: json_data}, indent=2) - - # Create a syntax-highlighted version of the JSON - syntax = Syntax(formatted_json, "json", theme="monokai") - - panel = Panel( - syntax, - title=device_name, - expand=False, - border_style="border", - title_align="left", - padding=(1, 3), - width=80, - ) - console.print() - console.print(panel) - console.print() + banner = "-" * len(device_name) + if raw: + print() + print(device_name) + print(banner) + print(formatted_json) + print() + else: + # Create a syntax-highlighted version of the JSON + syntax = Syntax(formatted_json, "json", theme="monokai") + + console.print() + console.print(f"[bold {border_color}]{device_name}[/]") + rich.print(f"[{border_color}]{banner}[/]") + console.print(syntax) + console.print() except json.decoder.JSONDecodeError as e: msg = f""" @@ -88,11 +90,20 @@ def output_yaml(results): def output_dispatcher(out_format, results): + # Sort the results dictionary by device_name + results = dict(sorted(results.items())) + output_functions = { "text": output_text, "json": output_json, "yaml": output_yaml, "raw": output_raw, + "json_raw": output_json, } - func = output_functions.get(out_format, output_text) - return func(results) + kwargs = {} + if out_format == "json_raw": + func = output_functions.get(out_format, output_text) + kwargs["raw"] = True + else: + func = output_functions.get(out_format, output_text) + return func(results, **kwargs) From cead65c00832bbb41088525f4041c2a94dcf218a Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 17:37:07 -0700 Subject: [PATCH 06/22] Improve JSON outputting --- netmiko/cli_tools/outputters.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index cf0c1d8cd..f345e7fc3 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -47,12 +47,11 @@ def output_text(results): def output_json(results, raw=False): console = Console(theme=CUSTOM_THEME) - border_color = CUSTOM_THEME.styles.get("border").color.name for device_name, output in results.items(): try: json_data = json.loads(output) - formatted_json = json.dumps({device_name: json_data}, indent=2) + formatted_json = json.dumps(json_data, indent=2) banner = "-" * len(device_name) if raw: print() @@ -61,12 +60,11 @@ def output_json(results, raw=False): print(formatted_json) print() else: - # Create a syntax-highlighted version of the JSON - syntax = Syntax(formatted_json, "json", theme="monokai") + syntax = Syntax(formatted_json, "json", theme="one-dark") console.print() - console.print(f"[bold {border_color}]{device_name}[/]") - rich.print(f"[{border_color}]{banner}[/]") + print(device_name) + print(banner) console.print(syntax) console.print() From ff293616405f30cdc2ccddcc85a34287a9c94fa4 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 18:35:23 -0700 Subject: [PATCH 07/22] Output function for failed devices --- netmiko/cli_tools/netmiko_show.py | 124 +++++++++++++----------------- netmiko/cli_tools/outputters.py | 10 +++ 2 files changed, 65 insertions(+), 69 deletions(-) diff --git a/netmiko/cli_tools/netmiko_show.py b/netmiko/cli_tools/netmiko_show.py index 18fe5c81c..7bc978212 100755 --- a/netmiko/cli_tools/netmiko_show.py +++ b/netmiko/cli_tools/netmiko_show.py @@ -11,12 +11,11 @@ from rich import print from netmiko.utilities import load_devices, display_inventory -from netmiko.utilities import obtain_netmiko_filename, write_tmp_file, ensure_dir_exists from netmiko.utilities import find_netmiko_dir from netmiko.utilities import SHOW_RUN_MAPPER from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn -from netmiko.cli_tools.outputters import output_dispatcher +from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices def grepx(files, pattern, grep_options, use_colors=True): @@ -115,85 +114,72 @@ def main(args): if cli_command: cmd_arg = True device_or_group = cli_args.devices.strip() - pattern = r"." - use_cached_files = cli_args.use_cache + use_cached_files = cli_args.use_cache # noqa hide_failed = cli_args.hide_failed # DEVICE LOADING ##### devices = obtain_devices(device_or_group) # Retrieve output from devices - my_files = [] failed_devices = [] results = {} - if not use_cached_files: - - # UPDATE DEVICE PARAMS (WITH CLI ARGS) / Create Task List ##### - device_tasks = [] - for device_name, device_params in devices.items(): - update_device_params( - device_params, - username=cli_username, - password=cli_password, - secret=cli_secret, - ) - if not cmd_arg: - device_type = device_params["device_type"] - cli_command = SHOW_RUN_MAPPER.get(device_type, "show run") - device_tasks.append( - { - "device_name": device_name, - "device_params": device_params, - "cli_command": cli_command, - } - ) - - # THREADING ##### - with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: - futures = [executor.submit(ssh_conn, **kwargs) for kwargs in device_tasks] - for future in as_completed(futures): - device_name, output = future.result() - results[device_name] = output - - # OUTPUT PROCESSING ##### - out_format = "text" - if output_json and output_raw: - out_format = "json_raw" - elif output_json: - out_format = "json" - elif output_raw: - out_format = "raw" - # elif output_yaml: - # out_format = "yaml" - output_dispatcher(out_format, results) - - else: - for device_name in devices: - file_name = obtain_netmiko_filename(device_name) - try: - with open(file_name) as f: - output = f.read() - except IOError: - return "Some cache files are missing: unable to use --use-cache option." - if ERROR_PATTERN not in output: - my_files.append(file_name) - else: - failed_devices.append(device_name) - - # grep_options = [] - # grepx(my_files, pattern, grep_options) + + # UPDATE DEVICE PARAMS (WITH CLI ARGS) / Create Task List ##### + device_tasks = [] + for device_name, device_params in devices.items(): + update_device_params( + device_params, + username=cli_username, + password=cli_password, + secret=cli_secret, + ) + if not cmd_arg: + device_type = device_params["device_type"] + cli_command = SHOW_RUN_MAPPER.get(device_type, "show run") + device_tasks.append( + { + "device_name": device_name, + "device_params": device_params, + "cli_command": cli_command, + } + ) + + # THREADING ##### + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + futures = [executor.submit(ssh_conn, **kwargs) for kwargs in device_tasks] + for future in as_completed(futures): + device_name, output = future.result() + results[device_name] = output + + # FIND FAILED DEVICES ##### + # NEED NEW WAY TO CACHE AND RE-USE CACHED FILES + valid_results = {} + for device_name, output in results.items(): + # Cache output(?) + # file_name = write_tmp_file(device_name, output) + if ERROR_PATTERN in output: + failed_devices.append(device_name) + continue + valid_results[device_name] = output + + # OUTPUT PROCESSING ##### + out_format = "text" + if output_json and output_raw: + out_format = "json_raw" + elif output_json: + out_format = "json" + elif output_raw: + out_format = "raw" + # elif output_yaml: + # out_format = "yaml" + output_dispatcher(out_format, valid_results) + if cli_args.display_runtime: print("Total time: {0}".format(datetime.now() - start_time)) if not hide_failed: - if failed_devices: - print("\n") - print("-" * 20) - print("Failed devices:") - failed_devices.sort() - for device_name in failed_devices: - print(" {}".format(device_name)) - print() + output_failed_devices(failed_devices) + return 0 diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index f345e7fc3..65cd7b5f2 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -105,3 +105,13 @@ def output_dispatcher(out_format, results): else: func = output_functions.get(out_format, output_text) return func(results, **kwargs) + +def output_failed_devices(failed_devices): + if failed_devices: + print("\n") + print("-" * 20) + print("Failed devices:") + failed_devices.sort() + for device_name in failed_devices: + print(" {}".format(device_name)) + print() From 89724e174b7d3ef6d92c22f6cf580c7071033ac2 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 19:04:58 -0700 Subject: [PATCH 08/22] Handling failed devices --- netmiko/cli_tools/outputters.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 65cd7b5f2..76f0077a5 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -4,12 +4,16 @@ from rich.panel import Panel from rich.theme import Theme from rich.syntax import Syntax +from rich.text import Text CUSTOM_THEME = Theme( { "device_name": "bold magenta", "border": "cyan", "output": "green", + "failed_title": "bold red", + "failed_device": "red", + "failed_border": "black", } ) @@ -106,12 +110,29 @@ def output_dispatcher(out_format, results): func = output_functions.get(out_format, output_text) return func(results, **kwargs) + def output_failed_devices(failed_devices): if failed_devices: - print("\n") - print("-" * 20) - print("Failed devices:") + console = Console(theme=CUSTOM_THEME) + + # Sort the failed devices list failed_devices.sort() + + # Create the content for the panel + content = Text() for device_name in failed_devices: - print(" {}".format(device_name)) - print() + content.append(f" {device_name}\n", style="failed_device") + + # Create and print the panel + panel = Panel( + content, + title="Failed devices", + expand=False, + border_style="failed_border", + title_align="left", + padding=(1, 1), + ) + + console.print() + console.print(panel, style="failed_title") + console.print() From 5806ee0f314c2f283e01f7ae76570da926cb031c Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 20 Sep 2024 19:13:58 -0700 Subject: [PATCH 09/22] Adding output formatting to netmiko_cfg --- netmiko/cli_tools/netmiko_cfg.py | 49 +++++++++++++++++++------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/netmiko/cli_tools/netmiko_cfg.py b/netmiko/cli_tools/netmiko_cfg.py index b74909268..694e3396d 100755 --- a/netmiko/cli_tools/netmiko_cfg.py +++ b/netmiko/cli_tools/netmiko_cfg.py @@ -9,10 +9,10 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from netmiko.utilities import load_devices, display_inventory -from netmiko.utilities import write_tmp_file, ensure_dir_exists from netmiko.utilities import find_netmiko_dir from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ from netmiko.cli_tools.cli_helpers import obtain_devices, update_device_params, ssh_conn +from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices def grepx(files, pattern, grep_options, use_colors=True): @@ -68,6 +68,10 @@ def parse_arguments(args): parser.add_argument( "--hide-failed", help="Hide failed devices", action="store_true" ) + parser.add_argument( + "--json", help="Output results in JSON format", action="store_true" + ) + parser.add_argument("--raw", help="Display raw output", action="store_true") parser.add_argument("--version", help="Display version", action="store_true") cli_args = parser.parse_args(args) if not cli_args.list_devices and not cli_args.version: @@ -100,6 +104,8 @@ def main(args): display_inventory(my_devices) return 0 + output_json = cli_args.json + output_raw = cli_args.raw cfg_command = cli_args.cmd if cfg_command: if r"\n" in cfg_command: @@ -112,14 +118,12 @@ def main(args): else: raise ValueError("No configuration commands provided.") device_or_group = cli_args.devices.strip() - pattern = r"." hide_failed = cli_args.hide_failed # DEVICE LOADING ##### devices = obtain_devices(device_or_group) # Retrieve output from devices - my_files = [] failed_devices = [] results = {} @@ -147,30 +151,35 @@ def main(args): device_name, output = future.result() results[device_name] = output - netmiko_base_dir, netmiko_full_dir = find_netmiko_dir() - ensure_dir_exists(netmiko_base_dir) - ensure_dir_exists(netmiko_full_dir) + # FIND FAILED DEVICES ##### + # NEED NEW WAY TO CACHE AND RE-USE CACHED FILES + valid_results = {} for device_name, output in results.items(): - file_name = write_tmp_file(device_name, output) - if ERROR_PATTERN not in output: - my_files.append(file_name) - else: + # Cache output(?) + # file_name = write_tmp_file(device_name, output) + if ERROR_PATTERN in output: failed_devices.append(device_name) + continue + valid_results[device_name] = output + + # OUTPUT PROCESSING ##### + out_format = "text" + if output_json and output_raw: + out_format = "json_raw" + elif output_json: + out_format = "json" + elif output_raw: + out_format = "raw" + # elif output_yaml: + # out_format = "yaml" + output_dispatcher(out_format, valid_results) - grep_options = [] - grepx(my_files, pattern, grep_options) if cli_args.display_runtime: print("Total time: {0}".format(datetime.now() - start_time)) if not hide_failed: - if failed_devices: - print("\n") - print("-" * 20) - print("Failed devices:") - failed_devices.sort() - for device_name in failed_devices: - print(" {}".format(device_name)) - print() + output_failed_devices(failed_devices) + return 0 From 51678e26f37015e4cc199d18fbe3270a6b7e9835 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Thu, 26 Sep 2024 12:26:35 -0700 Subject: [PATCH 10/22] Improving output colors --- netmiko/cli_tools/outputters.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 76f0077a5..0bf772a02 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -2,18 +2,21 @@ import rich from rich.console import Console from rich.panel import Panel +from rich.style import Style from rich.theme import Theme from rich.syntax import Syntax from rich.text import Text +NAVY_BLUE = "#000080" +BLUE = "#1E90FF" CUSTOM_THEME = Theme( { "device_name": "bold magenta", - "border": "cyan", + "border": BLUE, "output": "green", - "failed_title": "bold red", - "failed_device": "red", - "failed_border": "black", + "failed_title": "bold #800000", + "failed_device": "black", + "failed_border": "#800000", } ) @@ -64,11 +67,20 @@ def output_json(results, raw=False): print(formatted_json) print() else: - syntax = Syntax(formatted_json, "json", theme="one-dark") - + # theme = random.choice(rich_syntax_themes) + # "xcode" alternate theme + theme = "one-dark" + syntax = Syntax(formatted_json, "json", theme=theme) +# panel = Panel( +# syntax, +# border_style=Style(color=NAVY_BLUE), +# expand=False, +# padding=(1, 1) +# ) +# console.print() print(device_name) - print(banner) +# console.print(panel) console.print(syntax) console.print() From f79ece6bb462fe00034dd802d66fb4e9c39f788a Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Thu, 26 Sep 2024 12:34:13 -0700 Subject: [PATCH 11/22] Working on color scheme --- netmiko/cli_tools/outputters.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 0bf772a02..fdd1e82ec 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -67,21 +67,18 @@ def output_json(results, raw=False): print(formatted_json) print() else: - # theme = random.choice(rich_syntax_themes) # "xcode" alternate theme - theme = "one-dark" syntax = Syntax(formatted_json, "json", theme=theme) -# panel = Panel( -# syntax, -# border_style=Style(color=NAVY_BLUE), -# expand=False, -# padding=(1, 1) -# ) -# + panel = Panel( + syntax, + border_style=Style(color=NAVY_BLUE), + expand=False, + padding=(1, 1) + ) + console.print() print(device_name) -# console.print(panel) - console.print(syntax) + console.print(panel) console.print() except json.decoder.JSONDecodeError as e: From b450b61a7b9c5b75fcdf2817fbd0f27cec7f0d0f Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Thu, 26 Sep 2024 12:49:05 -0700 Subject: [PATCH 12/22] Improving color output --- netmiko/cli_tools/outputters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index fdd1e82ec..07ec22ed4 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -5,7 +5,6 @@ from rich.style import Style from rich.theme import Theme from rich.syntax import Syntax -from rich.text import Text NAVY_BLUE = "#000080" BLUE = "#1E90FF" @@ -68,12 +67,13 @@ def output_json(results, raw=False): print() else: # "xcode" alternate theme + theme = "trac" syntax = Syntax(formatted_json, "json", theme=theme) panel = Panel( syntax, border_style=Style(color=NAVY_BLUE), expand=False, - padding=(1, 1) + padding=(1, 1), ) console.print() @@ -128,9 +128,9 @@ def output_failed_devices(failed_devices): failed_devices.sort() # Create the content for the panel - content = Text() + content = "\n" for device_name in failed_devices: - content.append(f" {device_name}\n", style="failed_device") + content += f" {device_name}\n" # Create and print the panel panel = Panel( @@ -143,5 +143,5 @@ def output_failed_devices(failed_devices): ) console.print() - console.print(panel, style="failed_title") + console.print(panel) console.print() From 6e3769c1221feef172aa2fa93b61bbe2436ef354 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Thu, 26 Sep 2024 13:24:55 -0700 Subject: [PATCH 13/22] outputter test code --- tests/unit/test_clitools_outputters.py | 147 +++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/unit/test_clitools_outputters.py diff --git a/tests/unit/test_clitools_outputters.py b/tests/unit/test_clitools_outputters.py new file mode 100644 index 000000000..09aed6d27 --- /dev/null +++ b/tests/unit/test_clitools_outputters.py @@ -0,0 +1,147 @@ +import pytest +import json +from pathlib import Path +import netmiko.cli_tools.outputters as outputters + + +def read_json_file(filename): + file_path = Path(__file__).parent / filename + return json.loads(file_path.read_text()) + + +@pytest.fixture +def sample_results(): + return { + "arista1": json.dumps(read_json_file("arista1.json")), + "arista2": json.dumps(read_json_file("arista2.json")), + } + + +def test_output_raw(sample_results, capsys): + outputters.output_raw(sample_results) + captured = capsys.readouterr() + assert "arista1" in captured.out + assert "arista2" in captured.out + assert '"address": "10.220.88.28"' in captured.out + assert '"address": "10.220.88.29"' in captured.out + + +def test_output_raw_single_device(capsys): + single_result = {"arista1": json.dumps(read_json_file("arista1.json"))} + outputters.output_raw(single_result) + captured = capsys.readouterr() + assert "arista1" not in captured.out # Device name should not be printed for single device + assert '"address": "10.220.88.28"' in captured.out + assert '"address": "10.220.88.29"' not in captured.out # Ensure arista2 data is not present + + +def test_output_json(sample_results, capsys): + outputters.output_json(sample_results) + captured = capsys.readouterr() + assert "arista1" in captured.out + assert "arista2" in captured.out + assert '"address": "10.220.88.28"' in captured.out + assert '"address": "10.220.88.29"' in captured.out + + +# @pytest.fixture +# def sample_results(): +# return { +# "device1": "Output for device1", +# "device2": "Output for device2" +# } +# +# @pytest.fixture +# def sample_json_results(): +# return { +# "device1": json.dumps({"key": "value1"}), +# "device2": json.dumps({"key": "value2"}) +# } +# +# def test_output_raw(sample_results, capsys): +# outputters.output_raw(sample_results) +# captured = capsys.readouterr() +# assert "device1" in captured.out +# assert "Output for device1" in captured.out +# assert "device2" in captured.out +# assert "Output for device2" in captured.out +# +# def test_output_raw_single_device(capsys): +# single_result = {"device1": "Output for device1"} +# outputters.output_raw(single_result) +# captured = capsys.readouterr() +# assert "device1" not in captured.out +# assert "Output for device1" in captured.out +# +# @patch('rich.console.Console.print') +# def test_output_text(mock_print, sample_results): +# outputters.output_text(sample_results) +# assert mock_print.call_count >= 2 # At least one call per device +# +# @patch('json.loads') +# @patch('rich.console.Console.print') +# def test_output_json(mock_print, mock_json_loads, sample_json_results): +# mock_json_loads.side_effect = lambda x: json.loads(x) +# outputters.output_json(sample_json_results) +# assert mock_print.call_count >= 2 # At least one call per device +# assert mock_json_loads.call_count == 2 +# +# def test_output_json_raw(sample_json_results, capsys): +# outputters.output_json(sample_json_results, raw=True) +# captured = capsys.readouterr() +# assert "device1" in captured.out +# assert "device2" in captured.out +# assert '"key": "value1"' in captured.out +# assert '"key": "value2"' in captured.out +# +# def test_output_json_invalid_json(): +# invalid_json_results = {"device1": "Not a JSON string"} +# with pytest.raises(json.decoder.JSONDecodeError): +# outputters.output_json(invalid_json_results) +# +# def test_output_yaml(): +# # This function is not implemented yet, so we'll just check if it exists +# assert callable(outputters.output_yaml) +# +# @patch('outputters.output_text') +# def test_output_dispatcher_default(mock_output_text, sample_results): +# outputters.output_dispatcher("unknown_format", sample_results) +# mock_output_text.assert_called_once_with(sample_results) +# +# @patch('outputters.output_raw') +# def test_output_dispatcher_raw(mock_output_raw, sample_results): +# outputters.output_dispatcher("raw", sample_results) +# mock_output_raw.assert_called_once_with(sample_results) +# +# @patch('outputters.output_json') +# def test_output_dispatcher_json_raw(mock_output_json, sample_results): +# outputters.output_dispatcher("json_raw", sample_results) +# mock_output_json.assert_called_once_with(sample_results, raw=True) +# +# @patch('rich.console.Console.print') +# def test_output_failed_devices(mock_print): +# failed_devices = ["device1", "device2"] +# outputters.output_failed_devices(failed_devices) +# assert mock_print.call_count >= 1 +# +# def test_output_failed_devices_empty(): +# # Test with an empty list to ensure it doesn't raise any errors +# outputters.output_failed_devices([]) +# +## Test the CUSTOM_THEME +# def test_custom_theme(): +# assert "device_name" in outputters.CUSTOM_THEME.styles +# assert "border" in outputters.CUSTOM_THEME.styles +# assert "output" in outputters.CUSTOM_THEME.styles +# assert "failed_title" in outputters.CUSTOM_THEME.styles +# assert "failed_device" in outputters.CUSTOM_THEME.styles +# assert "failed_border" in outputters.CUSTOM_THEME.styles +# +## Test sorting of results +# def test_results_sorting(): +# unsorted_results = {"c": "3", "a": "1", "b": "2"} +# with patch('outputters.output_text') as mock_output_text: +# outputters.output_dispatcher("text", unsorted_results) +# # Check if the first argument passed to output_text is sorted +# called_results = mock_output_text.call_args[0][0] +# assert list(called_results.keys()) == ["a", "b", "c"] From 10d16dd5da698a7a77afcb6d38e1b544c19949a2 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Thu, 26 Sep 2024 13:37:02 -0700 Subject: [PATCH 14/22] Working on test code for outputters --- tests/unit/arista1.json | 30 +++++ tests/unit/arista1.txt | 6 + tests/unit/arista2.json | 30 +++++ tests/unit/arista2.txt | 6 + tests/unit/test_clitools_outputters.py | 159 +++++++------------------ 5 files changed, 113 insertions(+), 118 deletions(-) create mode 100644 tests/unit/arista1.json create mode 100644 tests/unit/arista1.txt create mode 100644 tests/unit/arista2.json create mode 100644 tests/unit/arista2.txt diff --git a/tests/unit/arista1.json b/tests/unit/arista1.json new file mode 100644 index 000000000..3123b7e6a --- /dev/null +++ b/tests/unit/arista1.json @@ -0,0 +1,30 @@ +{ + "interfaces": { + "Management1": { + "name": "Management1", + "interfaceStatus": "disabled", + "interfaceAddress": { + "ipAddr": { + "maskLen": 0, + "address": "0.0.0.0" + } + }, + "ipv4Routable240": false, + "lineProtocolStatus": "down", + "mtu": 1500 + }, + "Vlan1": { + "name": "Vlan1", + "interfaceStatus": "connected", + "interfaceAddress": { + "ipAddr": { + "maskLen": 24, + "address": "10.220.88.28" + } + }, + "ipv4Routable240": false, + "lineProtocolStatus": "up", + "mtu": 1500 + } + } +} diff --git a/tests/unit/arista1.txt b/tests/unit/arista1.txt new file mode 100644 index 000000000..52384829c --- /dev/null +++ b/tests/unit/arista1.txt @@ -0,0 +1,6 @@ + Address +Interface IP Address Status Protocol MTU Owner +----------------- --------------------- ---------------- -------------- ---------- ------- +Management1 unassigned admin down down 1500 +Vlan1 10.220.88.28/24 up up 1500 + diff --git a/tests/unit/arista2.json b/tests/unit/arista2.json new file mode 100644 index 000000000..2d58a9f5a --- /dev/null +++ b/tests/unit/arista2.json @@ -0,0 +1,30 @@ +{ + "interfaces": { + "Management1": { + "name": "Management1", + "interfaceStatus": "disabled", + "interfaceAddress": { + "ipAddr": { + "maskLen": 0, + "address": "0.0.0.0" + } + }, + "ipv4Routable240": false, + "lineProtocolStatus": "down", + "mtu": 1500 + }, + "Vlan1": { + "name": "Vlan1", + "interfaceStatus": "connected", + "interfaceAddress": { + "ipAddr": { + "maskLen": 24, + "address": "10.220.88.29" + } + }, + "ipv4Routable240": false, + "lineProtocolStatus": "up", + "mtu": 1500 + } + } +} diff --git a/tests/unit/arista2.txt b/tests/unit/arista2.txt new file mode 100644 index 000000000..03426b005 --- /dev/null +++ b/tests/unit/arista2.txt @@ -0,0 +1,6 @@ + Address +Interface IP Address Status Protocol MTU Owner +----------------- --------------------- ---------------- -------------- ---------- ------- +Management1 unassigned admin down down 1500 +Vlan1 10.220.88.29/24 up up 1500 + diff --git a/tests/unit/test_clitools_outputters.py b/tests/unit/test_clitools_outputters.py index 09aed6d27..ccbcadff3 100644 --- a/tests/unit/test_clitools_outputters.py +++ b/tests/unit/test_clitools_outputters.py @@ -4,39 +4,49 @@ import netmiko.cli_tools.outputters as outputters -def read_json_file(filename): +def read_file(filename): file_path = Path(__file__).parent / filename - return json.loads(file_path.read_text()) + return file_path.read_text() + + +def read_json_file(filename): + return json.loads(read_file(filename)) @pytest.fixture def sample_results(): return { - "arista1": json.dumps(read_json_file("arista1.json")), - "arista2": json.dumps(read_json_file("arista2.json")), + "arista1": { + "json": json.dumps(read_json_file("arista1.json")), + "raw": read_file("arista1.txt"), + }, + "arista2": { + "json": json.dumps(read_json_file("arista2.json")), + "raw": read_file("arista2.txt"), + }, } def test_output_raw(sample_results, capsys): - outputters.output_raw(sample_results) + raw_results = {device: data["raw"] for device, data in sample_results.items()} + outputters.output_raw(raw_results) captured = capsys.readouterr() assert "arista1" in captured.out assert "arista2" in captured.out - assert '"address": "10.220.88.28"' in captured.out - assert '"address": "10.220.88.29"' in captured.out - - -def test_output_raw_single_device(capsys): - single_result = {"arista1": json.dumps(read_json_file("arista1.json"))} - outputters.output_raw(single_result) - captured = capsys.readouterr() - assert "arista1" not in captured.out # Device name should not be printed for single device - assert '"address": "10.220.88.28"' in captured.out - assert '"address": "10.220.88.29"' not in captured.out # Ensure arista2 data is not present + assert ( + "Interface IP Address Status Protocol" + in captured.out + ) + assert ( + "Management1 unassigned admin down down" in captured.out + ) + assert "Vlan1 10.220.88.28/24 up up" in captured.out + assert "Vlan1 10.220.88.29/24 up up" in captured.out def test_output_json(sample_results, capsys): - outputters.output_json(sample_results) + json_results = {device: data["json"] for device, data in sample_results.items()} + outputters.output_json(json_results) captured = capsys.readouterr() assert "arista1" in captured.out assert "arista2" in captured.out @@ -44,104 +54,17 @@ def test_output_json(sample_results, capsys): assert '"address": "10.220.88.29"' in captured.out -# @pytest.fixture -# def sample_results(): -# return { -# "device1": "Output for device1", -# "device2": "Output for device2" -# } -# -# @pytest.fixture -# def sample_json_results(): -# return { -# "device1": json.dumps({"key": "value1"}), -# "device2": json.dumps({"key": "value2"}) -# } -# -# def test_output_raw(sample_results, capsys): -# outputters.output_raw(sample_results) -# captured = capsys.readouterr() -# assert "device1" in captured.out -# assert "Output for device1" in captured.out -# assert "device2" in captured.out -# assert "Output for device2" in captured.out -# -# def test_output_raw_single_device(capsys): -# single_result = {"device1": "Output for device1"} -# outputters.output_raw(single_result) -# captured = capsys.readouterr() -# assert "device1" not in captured.out -# assert "Output for device1" in captured.out -# -# @patch('rich.console.Console.print') -# def test_output_text(mock_print, sample_results): -# outputters.output_text(sample_results) -# assert mock_print.call_count >= 2 # At least one call per device -# -# @patch('json.loads') -# @patch('rich.console.Console.print') -# def test_output_json(mock_print, mock_json_loads, sample_json_results): -# mock_json_loads.side_effect = lambda x: json.loads(x) -# outputters.output_json(sample_json_results) -# assert mock_print.call_count >= 2 # At least one call per device -# assert mock_json_loads.call_count == 2 -# -# def test_output_json_raw(sample_json_results, capsys): -# outputters.output_json(sample_json_results, raw=True) -# captured = capsys.readouterr() -# assert "device1" in captured.out -# assert "device2" in captured.out -# assert '"key": "value1"' in captured.out -# assert '"key": "value2"' in captured.out -# -# def test_output_json_invalid_json(): -# invalid_json_results = {"device1": "Not a JSON string"} -# with pytest.raises(json.decoder.JSONDecodeError): -# outputters.output_json(invalid_json_results) -# -# def test_output_yaml(): -# # This function is not implemented yet, so we'll just check if it exists -# assert callable(outputters.output_yaml) -# -# @patch('outputters.output_text') -# def test_output_dispatcher_default(mock_output_text, sample_results): -# outputters.output_dispatcher("unknown_format", sample_results) -# mock_output_text.assert_called_once_with(sample_results) -# -# @patch('outputters.output_raw') -# def test_output_dispatcher_raw(mock_output_raw, sample_results): -# outputters.output_dispatcher("raw", sample_results) -# mock_output_raw.assert_called_once_with(sample_results) -# -# @patch('outputters.output_json') -# def test_output_dispatcher_json_raw(mock_output_json, sample_results): -# outputters.output_dispatcher("json_raw", sample_results) -# mock_output_json.assert_called_once_with(sample_results, raw=True) -# -# @patch('rich.console.Console.print') -# def test_output_failed_devices(mock_print): -# failed_devices = ["device1", "device2"] -# outputters.output_failed_devices(failed_devices) -# assert mock_print.call_count >= 1 -# -# def test_output_failed_devices_empty(): -# # Test with an empty list to ensure it doesn't raise any errors -# outputters.output_failed_devices([]) -# -## Test the CUSTOM_THEME -# def test_custom_theme(): -# assert "device_name" in outputters.CUSTOM_THEME.styles -# assert "border" in outputters.CUSTOM_THEME.styles -# assert "output" in outputters.CUSTOM_THEME.styles -# assert "failed_title" in outputters.CUSTOM_THEME.styles -# assert "failed_device" in outputters.CUSTOM_THEME.styles -# assert "failed_border" in outputters.CUSTOM_THEME.styles -# -## Test sorting of results -# def test_results_sorting(): -# unsorted_results = {"c": "3", "a": "1", "b": "2"} -# with patch('outputters.output_text') as mock_output_text: -# outputters.output_dispatcher("text", unsorted_results) -# # Check if the first argument passed to output_text is sorted -# called_results = mock_output_text.call_args[0][0] -# assert list(called_results.keys()) == ["a", "b", "c"] +def test_output_raw_single_device(sample_results, capsys): + single_result = {"arista1": sample_results["arista1"]["raw"]} + outputters.output_raw(single_result) + captured = capsys.readouterr() + assert "arista1" not in captured.out # Device name should not be in raw output + assert ( + "Interface IP Address Status Protocol" + in captured.out + ) + assert ( + "Management1 unassigned admin down down" in captured.out + ) + assert "Vlan1 10.220.88.28/24 up up" in captured.out + assert "10.220.88.29" not in captured.out # Ensure arista2 data is not present From 057cdab2b48420d3179d359fa3b9f992739b6478 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Thu, 26 Sep 2024 13:37:25 -0700 Subject: [PATCH 15/22] Explicitly add rich library --- poetry.lock | 386 ++++++++++++++++++++++++------------------------- pyproject.toml | 1 + 2 files changed, 194 insertions(+), 193 deletions(-) diff --git a/poetry.lock b/poetry.lock index b4d3be305..3340b6d3b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,102 +24,102 @@ files = [ [[package]] name = "aiohttp" -version = "3.10.5" +version = "3.10.6" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, - {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, - {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, - {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, - {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, - {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, - {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, - {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, - {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, - {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, - {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, - {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, - {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, - {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, + {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:682836fc672972cc3101cc9e30d49c5f7e8f1d010478d46119fe725a4545acfd"}, + {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:289fa8a20018d0d5aa9e4b35d899bd51bcb80f0d5f365d9a23e30dac3b79159b"}, + {file = "aiohttp-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8617c96a20dd57e7e9d398ff9d04f3d11c4d28b1767273a5b1a018ada5a654d3"}, + {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbeff1b062751c2a2a55b171f7050fb7073633c699299d042e962aacdbe1a07"}, + {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea35d849cdd4a9268f910bff4497baebbc1aa3f2f625fd8ccd9ac99c860c621"}, + {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473961b3252f3b949bb84873d6e268fb6d8aa0ccc6eb7404fa58c76a326bb8e1"}, + {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d2665c5df629eb2f981dab244c01bfa6cdc185f4ffa026639286c4d56fafb54"}, + {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25d92f794f1332f656e3765841fc2b7ad5c26c3f3d01e8949eeb3495691cf9f4"}, + {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9bd6b2033993d5ae80883bb29b83fb2b432270bbe067c2f53cc73bb57c46065f"}, + {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d7f408c43f5e75ea1edc152fb375e8f46ef916f545fb66d4aebcbcfad05e2796"}, + {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:cf8b8560aa965f87bf9c13bf9fed7025993a155ca0ce8422da74bf46d18c2f5f"}, + {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14477c4e52e2f17437b99893fd220ffe7d7ee41df5ebf931a92b8ca82e6fd094"}, + {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb138fbf9f53928e779650f5ed26d0ea1ed8b2cab67f0ea5d63afa09fdc07593"}, + {file = "aiohttp-3.10.6-cp310-cp310-win32.whl", hash = "sha256:9843d683b8756971797be171ead21511d2215a2d6e3c899c6e3107fbbe826791"}, + {file = "aiohttp-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:f8b8e49fe02f744d38352daca1dbef462c3874900bd8166516f6ea8e82b5aacf"}, + {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52e54fd776ad0da1006708762213b079b154644db54bcfc62f06eaa5b896402"}, + {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:995ab1a238fd0d19dc65f2d222e5eb064e409665c6426a3e51d5101c1979ee84"}, + {file = "aiohttp-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0749c4d5a08a802dd66ecdf59b2df4d76b900004017468a7bb736c3b5a3dd902"}, + {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e05b39158f2af0e2438cc2075cfc271f4ace0c3cc4a81ec95b27a0432e161951"}, + {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f196c970db2dcde4f24317e06615363349dc357cf4d7a3b0716c20ac6d7bcd"}, + {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47647c8af04a70e07a2462931b0eba63146a13affa697afb4ecbab9d03a480ce"}, + {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c0efe7e99f6d94d63274c06344bd0e9c8daf184ce5602a29bc39e00a18720"}, + {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9721cdd83a994225352ca84cd537760d41a9da3c0eacb3ff534747ab8fba6d0"}, + {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b82c8ebed66ce182893e7c0b6b60ba2ace45b1df104feb52380edae266a4850"}, + {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b169f8e755e541b72e714b89a831b315bbe70db44e33fead28516c9e13d5f931"}, + {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0be3115753baf8b4153e64f9aa7bf6c0c64af57979aa900c31f496301b374570"}, + {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e1f80cd17d81a404b6e70ef22bfe1870bafc511728397634ad5f5efc8698df56"}, + {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6419728b08fb6380c66a470d2319cafcec554c81780e2114b7e150329b9a9a7f"}, + {file = "aiohttp-3.10.6-cp311-cp311-win32.whl", hash = "sha256:bd294dcdc1afdc510bb51d35444003f14e327572877d016d576ac3b9a5888a27"}, + {file = "aiohttp-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:bf861da9a43d282d6dd9dcd64c23a0fccf2c5aa5cd7c32024513c8c79fb69de3"}, + {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2708baccdc62f4b1251e59c2aac725936a900081f079b88843dabcab0feeeb27"}, + {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7475da7a5e2ccf1a1c86c8fee241e277f4874c96564d06f726d8df8e77683ef7"}, + {file = "aiohttp-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02108326574ff60267b7b35b17ac5c0bbd0008ccb942ce4c48b657bb90f0b8aa"}, + {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:029a019627b37fa9eac5c75cc54a6bb722c4ebbf5a54d8c8c0fb4dd8facf2702"}, + {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a637d387db6fdad95e293fab5433b775fd104ae6348d2388beaaa60d08b38c4"}, + {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1a16f3fc1944c61290d33c88dc3f09ba62d159b284c38c5331868425aca426"}, + {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b292f37969f9cc54f4643f0be7dacabf3612b3b4a65413661cf6c350226787"}, + {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0754690a3a26e819173a34093798c155bafb21c3c640bff13be1afa1e9d421f9"}, + {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:164ecd32e65467d86843dbb121a6666c3deb23b460e3f8aefdcaacae79eb718a"}, + {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438c5863feb761f7ca3270d48c292c334814459f61cc12bab5ba5b702d7c9e56"}, + {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ba18573bb1de1063d222f41de64a0d3741223982dcea863b3f74646faf618ec7"}, + {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c82a94ddec996413a905f622f3da02c4359952aab8d817c01cf9915419525e95"}, + {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92351aa5363fc3c1f872ca763f86730ced32b01607f0c9662b1fa711087968d0"}, + {file = "aiohttp-3.10.6-cp312-cp312-win32.whl", hash = "sha256:3e15e33bfc73fa97c228f72e05e8795e163a693fd5323549f49367c76a6e5883"}, + {file = "aiohttp-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:fe517113fe4d35d9072b826c3e147d63c5f808ca8167d450b4f96c520c8a1d8d"}, + {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:482f74057ea13d387a7549d7a7ecb60e45146d15f3e58a2d93a0ad2d5a8457cd"}, + {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03fa40d1450ee5196e843315ddf74a51afc7e83d489dbfc380eecefea74158b1"}, + {file = "aiohttp-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e52e59ed5f4cc3a3acfe2a610f8891f216f486de54d95d6600a2c9ba1581f4d"}, + {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b3935a22c9e41a8000d90588bed96cf395ef572dbb409be44c6219c61d900d"}, + {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bef1480ee50f75abcfcb4b11c12de1005968ca9d0172aec4a5057ba9f2b644f"}, + {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:671745ea7db19693ce867359d503772177f0b20fa8f6ee1e74e00449f4c4151d"}, + {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b50b367308ca8c12e0b50cba5773bc9abe64c428d3fd2bbf5cd25aab37c77bf"}, + {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a504d7cdb431a777d05a124fd0b21efb94498efa743103ea01b1e3136d2e4fb"}, + {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66bc81361131763660b969132a22edce2c4d184978ba39614e8f8f95db5c95f8"}, + {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:27cf19a38506e2e9f12fc17e55f118f04897b0a78537055d93a9de4bf3022e3d"}, + {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3468b39f977a11271517c6925b226720e148311039a380cc9117b1e2258a721f"}, + {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9d26da22a793dfd424be1050712a70c0afd96345245c29aced1e35dbace03413"}, + {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:844d48ff9173d0b941abed8b2ea6a412f82b56d9ab1edb918c74000c15839362"}, + {file = "aiohttp-3.10.6-cp313-cp313-win32.whl", hash = "sha256:2dd56e3c43660ed3bea67fd4c5025f1ac1f9ecf6f0b991a6e5efe2e678c490c5"}, + {file = "aiohttp-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:c91781d969fbced1993537f45efe1213bd6fccb4b37bfae2a026e20d6fbed206"}, + {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4407a80bca3e694f2d2a523058e20e1f9f98a416619e04f6dc09dc910352ac8b"}, + {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1cb045ec5961f51af3e2c08cd6fe523f07cc6e345033adee711c49b7b91bb954"}, + {file = "aiohttp-3.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fabdcdc781a36b8fd7b2ca9dea8172f29a99e11d00ca0f83ffeb50958da84a1"}, + {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a9f42efcc2681790595ab3d03c0e52d01edc23a0973ea09f0dc8d295e12b8e"}, + {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca776a440795db437d82c07455761c85bbcf3956221c3c23b8c93176c278ce7"}, + {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5582de171f0898139cf51dd9fcdc79b848e28d9abd68e837f0803fc9f30807b1"}, + {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370e2d47575c53c817ee42a18acc34aad8da4dbdaac0a6c836d58878955f1477"}, + {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:444d1704e2af6b30766debed9be8a795958029e552fe77551355badb1944012c"}, + {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40271a2a375812967401c9ca8077de9368e09a43a964f4dce0ff603301ec9358"}, + {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f3af26f86863fad12e25395805bb0babbd49d512806af91ec9708a272b696248"}, + {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4752df44df48fd42b80f51d6a97553b482cda1274d9dc5df214a3a1aa5d8f018"}, + {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2cd5290ab66cfca2f90045db2cc6434c1f4f9fbf97c9f1c316e785033782e7d2"}, + {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3427031064b0d5c95647e6369c4aa3c556402f324a3e18107cb09517abe5f962"}, + {file = "aiohttp-3.10.6-cp38-cp38-win32.whl", hash = "sha256:614fc21e86adc28e4165a6391f851a6da6e9cbd7bb232d0df7718b453a89ee98"}, + {file = "aiohttp-3.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:58c5d7318a136a3874c78717dd6de57519bc64f6363c5827c2b1cb775bea71dd"}, + {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5db26bbca8e7968c4c977a0c640e0b9ce7224e1f4dcafa57870dc6ee28e27de6"}, + {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fb4216e3ec0dbc01db5ba802f02ed78ad8f07121be54eb9e918448cc3f61b7c"}, + {file = "aiohttp-3.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a976ef488f26e224079deb3d424f29144c6d5ba4ded313198169a8af8f47fb82"}, + {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a86610174de8a85a920e956e2d4f9945e7da89f29a00e95ac62a4a414c4ef4e"}, + {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:217791c6a399cc4f2e6577bb44344cba1f5714a2aebf6a0bea04cfa956658284"}, + {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba3662d41abe2eab0eeec7ee56f33ef4e0b34858f38abf24377687f9e1fb00a5"}, + {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4dfa5ad4bce9ca30a76117fbaa1c1decf41ebb6c18a4e098df44298941566f9"}, + {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0009258e97502936d3bd5bf2ced15769629097d0abb81e6495fba1047824fe0"}, + {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a75d5c9fb4f06c41d029ae70ad943c3a844c40c0a769d12be4b99b04f473d3d"}, + {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8198b7c002aae2b40b2d16bfe724b9a90bcbc9b78b2566fc96131ef4e382574d"}, + {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4611db8c907f90fe86be112efdc2398cd7b4c8eeded5a4f0314b70fdea8feab0"}, + {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ff99ae06eef85c7a565854826114ced72765832ee16c7e3e766c5e4c5b98d20e"}, + {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7641920bdcc7cd2d3ddfb8bb9133a6c9536b09dbd49490b79e125180b2d25b93"}, + {file = "aiohttp-3.10.6-cp39-cp39-win32.whl", hash = "sha256:e2e7d5591ea868d5ec82b90bbeb366a198715672841d46281b623e23079593db"}, + {file = "aiohttp-3.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:b504c08c45623bf5c7ca41be380156d925f00199b3970efd758aef4a77645feb"}, + {file = "aiohttp-3.10.6.tar.gz", hash = "sha256:d2578ef941be0c2ba58f6f421a703527d08427237ed45ecb091fed6f83305336"}, ] [package.dependencies] @@ -129,7 +129,7 @@ async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" +yarl = ">=1.12.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] @@ -1157,13 +1157,13 @@ six = "*" [[package]] name = "keyring" -version = "25.4.0" +version = "25.4.1" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-25.4.0-py3-none-any.whl", hash = "sha256:a2a630d5c9bef5d3f0968d15ef4e42b894a83e17494edcb67b154c36491c9920"}, - {file = "keyring-25.4.0.tar.gz", hash = "sha256:ae8263fd9264c94f91ad82d098f8a5bb1b7fa71ce0a72388dc4fc0be3f6a034e"}, + {file = "keyring-25.4.1-py3-none-any.whl", hash = "sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf"}, + {file = "keyring-25.4.1.tar.gz", hash = "sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b"}, ] [package.dependencies] @@ -1671,13 +1671,13 @@ files = [ [[package]] name = "ntc-templates" -version = "7.0.0" +version = "7.1.0" description = "TextFSM Templates for Network Devices, and Python wrapper for TextFSM's CliTable." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "ntc_templates-7.0.0-py3-none-any.whl", hash = "sha256:f61bebf889b05c49fd8af22c3a68f349a8c82146b66c30986d6c46d13cab6014"}, - {file = "ntc_templates-7.0.0.tar.gz", hash = "sha256:24bc014b5f8c91a7cc7fb695bf7d61babd9db2dc5fb9d0ce323b142504b36e21"}, + {file = "ntc_templates-7.1.0-py3-none-any.whl", hash = "sha256:0890b1b5b30d3bb6a6051b51267d037927d1e74ae2de6243c767e5365d27d14b"}, + {file = "ntc_templates-7.1.0.tar.gz", hash = "sha256:6e854c44e57d0c04d6968d8c82d6462417997ae79b13db9d90d707e8b8ec9232"}, ] [package.dependencies] @@ -3273,103 +3273,103 @@ dev = ["Sphinx", "coverage", "restview", "sphinx-rtd-theme", "sphinxcontrib-napo [[package]] name = "yarl" -version = "1.11.1" +version = "1.12.1" description = "Yet another URL library" optional = false python-versions = ">=3.8" files = [ - {file = "yarl-1.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00"}, - {file = "yarl-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d"}, - {file = "yarl-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46"}, - {file = "yarl-1.11.1-cp310-cp310-win32.whl", hash = "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91"}, - {file = "yarl-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998"}, - {file = "yarl-1.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68"}, - {file = "yarl-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe"}, - {file = "yarl-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e"}, - {file = "yarl-1.11.1-cp311-cp311-win32.whl", hash = "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6"}, - {file = "yarl-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b"}, - {file = "yarl-1.11.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0"}, - {file = "yarl-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265"}, - {file = "yarl-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45"}, - {file = "yarl-1.11.1-cp312-cp312-win32.whl", hash = "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447"}, - {file = "yarl-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639"}, - {file = "yarl-1.11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c"}, - {file = "yarl-1.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e"}, - {file = "yarl-1.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da"}, - {file = "yarl-1.11.1-cp313-cp313-win32.whl", hash = "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979"}, - {file = "yarl-1.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367"}, - {file = "yarl-1.11.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4"}, - {file = "yarl-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b"}, - {file = "yarl-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420"}, - {file = "yarl-1.11.1-cp38-cp38-win32.whl", hash = "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a"}, - {file = "yarl-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6"}, - {file = "yarl-1.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269"}, - {file = "yarl-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26"}, - {file = "yarl-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df"}, - {file = "yarl-1.11.1-cp39-cp39-win32.whl", hash = "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74"}, - {file = "yarl-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0"}, - {file = "yarl-1.11.1-py3-none-any.whl", hash = "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38"}, - {file = "yarl-1.11.1.tar.gz", hash = "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53"}, + {file = "yarl-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64c5b0f2b937fe40d0967516eee5504b23cb247b8b7ffeba7213a467d9646fdc"}, + {file = "yarl-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2e430ac432f969ef21770645743611c1618362309e3ad7cab45acd1ad1a540ff"}, + {file = "yarl-1.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e26e64f42bce5ddf9002092b2c37b13071c2e6413d5c05f9fa9de58ed2f7749"}, + {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0103c52f8dfe5d573c856322149ddcd6d28f51b4d4a3ee5c4b3c1b0a05c3d034"}, + {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b63465b53baeaf2122a337d4ab57d6bbdd09fcadceb17a974cfa8a0300ad9c67"}, + {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17d4dc4ff47893a06737b8788ed2ba2f5ac4e8bb40281c8603920f7d011d5bdd"}, + {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b54949267bd5704324397efe9fbb6aa306466dee067550964e994d309db5f1"}, + {file = "yarl-1.12.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10b690cd78cbaca2f96a7462f303fdd2b596d3978b49892e4b05a7567c591572"}, + {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c85ab016e96a975afbdb9d49ca90f3bca9920ef27c64300843fe91c3d59d8d20"}, + {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c1caa5763d1770216596e0a71b5567f27aac28c95992110212c108ec74589a48"}, + {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:595bbcdbfc4a9c6989d7489dca8510cba053ff46b16c84ffd95ac8e90711d419"}, + {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e64f0421892a207d3780903085c1b04efeb53b16803b23d947de5a7261b71355"}, + {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:319c206e83e46ec2421b25b300c8482b6fe8a018baca246be308c736d9dab267"}, + {file = "yarl-1.12.1-cp310-cp310-win32.whl", hash = "sha256:da045bd1147d12bd43fb032296640a7cc17a7f2eaba67495988362e99db24fd2"}, + {file = "yarl-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:aebbd47df77190ada603157f0b3670d578c110c31746ecc5875c394fdcc59a99"}, + {file = "yarl-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:28389a68981676bf74e2e199fe42f35d1aa27a9c98e3a03e6f58d2d3d054afe1"}, + {file = "yarl-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f736f54565f8dd7e3ab664fef2bc461d7593a389a7f28d4904af8d55a91bd55f"}, + {file = "yarl-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dee0496d5f1a8f57f0f28a16f81a2033fc057a2cf9cd710742d11828f8c80e2"}, + {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8981a94a27ac520a398302afb74ae2c0be1c3d2d215c75c582186a006c9e7b0"}, + {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff54340fc1129e8e181827e2234af3ff659b4f17d9bbe77f43bc19e6577fadec"}, + {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c8cee662b5f8c30ad7eedfc26123f845f007798e4ff1001d9528fe959fd23c"}, + {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97a29b37830ba1262d8dfd48ddb5b28ad4d3ebecc5d93a9c7591d98641ec737"}, + {file = "yarl-1.12.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c89894cc6f6ddd993813e79244b36b215c14f65f9e4f1660b1f2ba9e5594b95"}, + {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:712ba8722c0699daf186de089ddc4677651eb9875ed7447b2ad50697522cbdd9"}, + {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6e9a9f50892153bad5046c2a6df153224aa6f0573a5a8ab44fc54a1e886f6e21"}, + {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1d4017e78fb22bc797c089b746230ad78ecd3cdb215bc0bd61cb72b5867da57e"}, + {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f494c01b28645c431239863cb17af8b8d15b93b0d697a0320d5dd34cd9d7c2fa"}, + {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de4544b1fb29cf14870c4e2b8a897c0242449f5dcebd3e0366aa0aa3cf58a23a"}, + {file = "yarl-1.12.1-cp311-cp311-win32.whl", hash = "sha256:7564525a4673fde53dee7d4c307a961c0951918f0b8c7f09b2c9e02067cf6504"}, + {file = "yarl-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:f23bb1a7a6e8e8b612a164fdd08e683bcc16c76f928d6dbb7bdbee2374fbfee6"}, + {file = "yarl-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3e2aff8b822ab0e0bdbed9f50494b3a35629c4b9488ae391659973a37a9f53f"}, + {file = "yarl-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22dda2799c8d39041d731e02bf7690f0ef34f1691d9ac9dfcb98dd1e94c8b058"}, + {file = "yarl-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18c2a7757561f05439c243f517dbbb174cadfae3a72dee4ae7c693f5b336570f"}, + {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:835010cc17d0020e7931d39e487d72c8e01c98e669b6896a8b8c9aa8ca69a949"}, + {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2254fe137c4a360b0a13173a56444f756252c9283ba4d267ca8e9081cd140ea"}, + {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a071d2c3d39b4104f94fc08ab349e9b19b951ad4b8e3b6d7ea92d6ef7ccaf8"}, + {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73a183042ae0918c82ce2df38c3db2409b0eeae88e3afdfc80fb67471a95b33b"}, + {file = "yarl-1.12.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326b8a079a9afcac0575971e56dabdf7abb2ea89a893e6949b77adfeb058b50e"}, + {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:126309c0f52a2219b3d1048aca00766429a1346596b186d51d9fa5d2070b7b13"}, + {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba1c779b45a399cc25f511c681016626f69e51e45b9d350d7581998722825af9"}, + {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:af1107299cef049ad00a93df4809517be432283a0847bcae48343ebe5ea340dc"}, + {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:20d817c0893191b2ab0ba30b45b77761e8dfec30a029b7c7063055ca71157f84"}, + {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d4f818f6371970d6a5d1e42878389bbfb69dcde631e4bbac5ec1cb11158565ca"}, + {file = "yarl-1.12.1-cp312-cp312-win32.whl", hash = "sha256:0ac33d22b2604b020569a82d5f8a03ba637ba42cc1adf31f616af70baf81710b"}, + {file = "yarl-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd24996e12e1ba7c397c44be75ca299da14cde34d74bc5508cce233676cc68d0"}, + {file = "yarl-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dea360778e0668a7ad25d7727d03364de8a45bfd5d808f81253516b9f2217765"}, + {file = "yarl-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1f50a37aeeb5179d293465e522fd686080928c4d89e0ff215e1f963405ec4def"}, + {file = "yarl-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0274b1b7a9c9c32b7bf250583e673ff99fb9fccb389215841e2652d9982de740"}, + {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4f3ab9eb8ab2d585ece959c48d234f7b39ac0ca1954a34d8b8e58a52064bdb3"}, + {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d31dd0245d88cf7239e96e8f2a99f815b06e458a5854150f8e6f0e61618d41b"}, + {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96198d5d26f40557d986c1253bfe0e02d18c9d9b93cf389daf1a3c9f7c755fa"}, + {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddae504cfb556fe220efae65e35be63cd11e3c314b202723fc2119ce19f0ca2e"}, + {file = "yarl-1.12.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bce00f3b1f7f644faae89677ca68645ed5365f1c7f874fdd5ebf730a69640d38"}, + {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eee5ff934b0c9f4537ff9596169d56cab1890918004791a7a06b879b3ba2a7ef"}, + {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4ea99e64b2ad2635e0f0597b63f5ea6c374791ff2fa81cdd4bad8ed9f047f56f"}, + {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c667b383529520b8dd6bd496fc318678320cb2a6062fdfe6d3618da6b8790f6"}, + {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d920401941cb898ef089422e889759dd403309eb370d0e54f1bdf6ca07fef603"}, + {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:501a1576716032cc6d48c7c47bcdc42d682273415a8f2908e7e72cb4625801f3"}, + {file = "yarl-1.12.1-cp313-cp313-win32.whl", hash = "sha256:24416bb5e221e29ddf8aac5b97e94e635ca2c5be44a1617ad6fe32556df44294"}, + {file = "yarl-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:71af3766bb46738d12cc288d9b8de7ef6f79c31fd62757e2b8a505fe3680b27f"}, + {file = "yarl-1.12.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c924deab8105f86980983eced740433fb7554a7f66db73991affa4eda99d5402"}, + {file = "yarl-1.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5fb475a4cdde582c9528bb412b98f899680492daaba318231e96f1a0a1bb0d53"}, + {file = "yarl-1.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:36ee0115b9edca904153a66bb74a9ff1ce38caff015de94eadfb9ba8e6ecd317"}, + {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2631c9d7386bd2d4ce24ecc6ebf9ae90b3efd713d588d90504eaa77fec4dba01"}, + {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2376d8cf506dffd0e5f2391025ae8675b09711016656590cb03b55894161fcfa"}, + {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24197ba3114cc85ddd4091e19b2ddc62650f2e4a899e51b074dfd52d56cf8c72"}, + {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfdf419bf5d3644f94cd7052954fc233522f5a1b371fc0b00219ebd9c14d5798"}, + {file = "yarl-1.12.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8112f640a4f7e7bf59f7cabf0d47a29b8977528c521d73a64d5cc9e99e48a174"}, + {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:607d12f0901f6419a8adceb139847c42c83864b85371f58270e42753f9780fa6"}, + {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:664380c7ed524a280b6a2d5d9126389c3e96cd6e88986cdb42ca72baa27421d6"}, + {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0d0a5e87bc48d76dfcfc16295201e9812d5f33d55b4a0b7cad1025b92bf8b91b"}, + {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:eff6bac402719c14e17efe845d6b98593c56c843aca6def72080fbede755fd1f"}, + {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:22839d1d1eab9e4b427828a88a22beb86f67c14d8ff81175505f1cc8493f3500"}, + {file = "yarl-1.12.1-cp38-cp38-win32.whl", hash = "sha256:717f185086bb9d817d4537dd18d5df5d657598cd00e6fc22e4d54d84de266c1d"}, + {file = "yarl-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:71978ba778948760cff528235c951ea0ef7a4f9c84ac5a49975f8540f76c3f73"}, + {file = "yarl-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ffc046ebddccb3c4cac72c1a3e1bc343492336f3ca86d24672e90ccc5e788a"}, + {file = "yarl-1.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f10954b233d4df5cc3137ffa5ced97f8894152df817e5d149bf05a0ef2ab8134"}, + {file = "yarl-1.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2e912b282466444023610e4498e3795c10e7cfd641744524876239fcf01d538d"}, + {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af871f70cfd5b528bd322c65793b5fd5659858cdfaa35fbe563fb99b667ed1f"}, + {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3e4e1f7b08d1ec6b685ccd3e2d762219c550164fbf524498532e39f9413436e"}, + {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a7ee79183f0b17dcede8b6723e7da2ded529cf159a878214be9a5d3098f5b1e"}, + {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c8ff1e1dd680e38af0887927cab407a4e51d84a5f02ae3d6eb87233036c763"}, + {file = "yarl-1.12.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e9905fc2dc1319e4c39837b906a024cf71b1261cc66b0cd89678f779c0c61f5"}, + {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01549468858b87d36f967c97d02e6e54106f444aeb947ed76f8f71f85ed07cec"}, + {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:96b34830bd6825ca0220bf005ea99ac83eb9ce51301ddb882dcf613ae6cd95fb"}, + {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2aee7594d2c2221c717a8e394bbed4740029df4c0211ceb0f04815686e99c795"}, + {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:15871130439ad10abb25a4631120d60391aa762b85fcab971411e556247210a0"}, + {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:838dde2cb570cfbb4cab8a876a0974e8b90973ea40b3ac27a79b8a74c8a2db15"}, + {file = "yarl-1.12.1-cp39-cp39-win32.whl", hash = "sha256:eacbcf30efaca7dc5cb264228ffecdb95fdb1e715b1ec937c0ce6b734161e0c8"}, + {file = "yarl-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:76a59d1b63de859398bc7764c860a769499511463c1232155061fe0147f13e01"}, + {file = "yarl-1.12.1-py3-none-any.whl", hash = "sha256:dc3192a81ecd5ff954cecd690327badd5a84d00b877e1573f7c9097ce13e5bfb"}, + {file = "yarl-1.12.1.tar.gz", hash = "sha256:5b860055199aec8d6fe4dcee3c5196ce506ca198a50aab0059ffd26e8e815828"}, ] [package.dependencies] @@ -3398,4 +3398,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "e1084f068de60cabc884087eda4f1829fca3441772d1173fa569544646417ea4" +content-hash = "9dc53e6e824dcb3514416ef96f88f7dca93d7457b695c99fe22d881d2ef02226" diff --git a/pyproject.toml b/pyproject.toml index bcc25d2f5..d140bd0c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ textfsm = ">=1.1.3" ntc-templates = ">=3.1.0" pyserial = ">=3.3" cffi = ">=1.17.0rc1" +rich = ">=13.8" [tool.poetry.group.dev.dependencies] black = "24.8.0" From 610402c9004eeb32c321913a1b85fb9e8ee02705 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 1 Oct 2024 11:22:16 -0700 Subject: [PATCH 16/22] Working on colors --- netmiko/cli_tools/outputters.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 07ec22ed4..94f20d7ce 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -8,6 +8,23 @@ NAVY_BLUE = "#000080" BLUE = "#1E90FF" +LINEN_WHITE = "#FAF0E6" + +# "xcode"/"trac" alternate light themes +LIGHT_THEME = "trac" +LIGHT_BOX = NAVY_BLUE +# "rrt"/"vim" alternate dark themes +DARK_THEME = "rrt" +DARK_BOX = LINEN_WHITE + +DARK_MODE = False +if DARK_MODE: + DEFAULT_THEME = DARK_THEME + DEFAULT_BOX = DARK_BOX +else: + DEFAULT_THEME = LIGHT_THEME + DEFAULT_BOX = LIGHT_BOX + CUSTOM_THEME = Theme( { "device_name": "bold magenta", @@ -66,12 +83,11 @@ def output_json(results, raw=False): print(formatted_json) print() else: - # "xcode" alternate theme - theme = "trac" + theme = DEFAULT_THEME syntax = Syntax(formatted_json, "json", theme=theme) panel = Panel( syntax, - border_style=Style(color=NAVY_BLUE), + border_style=Style(color=DEFAULT_BOX), expand=False, padding=(1, 1), ) From 885d574793b642e16883a2628bb1459baa04ded3 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 1 Oct 2024 11:59:15 -0700 Subject: [PATCH 17/22] Consolidating argument parsing --- netmiko/cli_tools/argument_handling.py | 81 ++++++++++++++++++++++++++ netmiko/cli_tools/netmiko_cfg.py | 79 ++----------------------- netmiko/cli_tools/netmiko_show.py | 76 ++---------------------- 3 files changed, 92 insertions(+), 144 deletions(-) create mode 100644 netmiko/cli_tools/argument_handling.py diff --git a/netmiko/cli_tools/argument_handling.py b/netmiko/cli_tools/argument_handling.py new file mode 100644 index 000000000..8f4aa6d51 --- /dev/null +++ b/netmiko/cli_tools/argument_handling.py @@ -0,0 +1,81 @@ +import argparse + + +def common_args(parser): + """Add common arguments to the parser.""" + parser.add_argument( + "devices", + nargs="?", + help="Device or group to connect to", + action="store", + type=str, + ) + parser.add_argument( + "--cmd", + help="Command to execute", + action="store", + default=None, + type=str, + ) + parser.add_argument("--username", help="Username", action="store", type=str) + parser.add_argument("--password", help="Password", action="store_true") + parser.add_argument("--secret", help="Enable Secret", action="store_true") + parser.add_argument( + "--list-devices", help="List devices from inventory", action="store_true" + ) + parser.add_argument( + "--display-runtime", help="Display program runtime", action="store_true" + ) + parser.add_argument( + "--hide-failed", help="Hide failed devices", action="store_true" + ) + parser.add_argument( + "--json", help="Output results in JSON format", action="store_true" + ) + parser.add_argument("--raw", help="Display raw output", action="store_true") + parser.add_argument("--version", help="Display version", action="store_true") + + +def show_args(parser): + """Add arguments specific to netmiko_show.py.""" + pass + + +def cfg_args(parser): + """Add arguments specific to netmiko_cfg.py.""" + parser.add_argument( + "--infile", help="Read commands from file", type=argparse.FileType("r") + ) + + +def parse_arguments(args, command): + """Parse command-line arguments for both scripts.""" + + if command == "netmiko-cfg": + description = "Execute configurations command using Netmiko" + addl_args = cfg_args + elif command == "netmiko-show": + description = "Execute show command using Netmiko (defaults to 'show run')" + addl_args = show_args + elif command == "netmiko-grep": + # FIX + description = "" + # addl_args = grep_args + else: + # FIX: better message + raise ValueError() + + parser = argparse.ArgumentParser(description=description) + common_args(parser) + + # Add additional arguments based (addl_args references a function) + addl_args(parser) + + cli_args = parser.parse_args(args) + if not cli_args.list_devices and not cli_args.version: + if not cli_args.devices: + parser.error("Devices not specified.") + if command == "netmiko-cfg" and not cli_args.cmd and not cli_args.infile: + parser.error("No configuration commands provided.") + + return cli_args diff --git a/netmiko/cli_tools/netmiko_cfg.py b/netmiko/cli_tools/netmiko_cfg.py index 694e3396d..474e83db4 100755 --- a/netmiko/cli_tools/netmiko_cfg.py +++ b/netmiko/cli_tools/netmiko_cfg.py @@ -1,85 +1,18 @@ #!/usr/bin/env python """Return output from single show cmd using Netmiko.""" -import argparse import sys -import os -import subprocess from datetime import datetime from getpass import getpass from concurrent.futures import ThreadPoolExecutor, as_completed from netmiko.utilities import load_devices, display_inventory -from netmiko.utilities import find_netmiko_dir -from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ -from netmiko.cli_tools.cli_helpers import obtain_devices, update_device_params, ssh_conn +from netmiko.cli_tools import ERROR_PATTERN, MAX_WORKERS, __version__ +from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices +from netmiko.cli_tools.argument_handling import parse_arguments -def grepx(files, pattern, grep_options, use_colors=True): - """Call system grep""" - if not isinstance(files, (list, tuple)): - files = [files] - if use_colors: - grep_options += ["--color=auto"] - - # Make grep output look nicer by 'cd netmiko_full_dir' - _, netmiko_full_dir = find_netmiko_dir() - os.chdir(netmiko_full_dir) - # Convert files to strip off the directory - retrieve_file = lambda x: x.split("/")[-1] # noqa - files = [retrieve_file(a_file) for a_file in files] - files.sort() - grep_list = [GREP] + grep_options + [pattern] + files - proc = subprocess.Popen(grep_list, shell=False) - proc.communicate() - return "" - - -def parse_arguments(args): - """Parse command-line arguments.""" - description = "Execute single config cmd using Netmiko" - parser = argparse.ArgumentParser(description=description) - parser.add_argument( - "devices", - nargs="?", - help="Device or group to connect to", - action="store", - type=str, - ) - parser.add_argument( - "--infile", help="Read commands from file", type=argparse.FileType("r") - ) - parser.add_argument( - "--cmd", - help="Config command to execute", - action="store", - default=None, - type=str, - ) - parser.add_argument("--username", help="Username", action="store", type=str) - parser.add_argument("--password", help="Password", action="store_true") - parser.add_argument("--secret", help="Enable Secret", action="store_true") - parser.add_argument( - "--list-devices", help="List devices from inventory", action="store_true" - ) - parser.add_argument( - "--display-runtime", help="Display program runtime", action="store_true" - ) - parser.add_argument( - "--hide-failed", help="Hide failed devices", action="store_true" - ) - parser.add_argument( - "--json", help="Output results in JSON format", action="store_true" - ) - parser.add_argument("--raw", help="Display raw output", action="store_true") - parser.add_argument("--version", help="Display version", action="store_true") - cli_args = parser.parse_args(args) - if not cli_args.list_devices and not cli_args.version: - if not cli_args.devices: - parser.error("Devices not specified.") - if not cli_args.cmd and not cli_args.infile: - parser.error("No configuration commands provided.") - return cli_args +COMMAND = "netmiko-cfg" def main_ep(): @@ -88,7 +21,7 @@ def main_ep(): def main(args): start_time = datetime.now() - cli_args = parse_arguments(args) + cli_args = parse_arguments(args, COMMAND) cli_username = cli_args.username if cli_args.username else None cli_password = getpass() if cli_args.password else None @@ -96,7 +29,7 @@ def main(args): version = cli_args.version if version: - print("netmiko-cfg v{}".format(__version__)) + print(f"{COMMAND} v{__version__}") return 0 list_devices = cli_args.list_devices if list_devices: diff --git a/netmiko/cli_tools/netmiko_show.py b/netmiko/cli_tools/netmiko_show.py index 7bc978212..0369f7b7e 100755 --- a/netmiko/cli_tools/netmiko_show.py +++ b/netmiko/cli_tools/netmiko_show.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 """Return output from single show cmd using Netmiko.""" -import argparse import sys -import os -import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime @@ -11,76 +8,14 @@ from rich import print from netmiko.utilities import load_devices, display_inventory -from netmiko.utilities import find_netmiko_dir from netmiko.utilities import SHOW_RUN_MAPPER -from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ +from netmiko.cli_tools import ERROR_PATTERN, MAX_WORKERS, __version__ from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices +from netmiko.cli_tools.argument_handling import parse_arguments -def grepx(files, pattern, grep_options, use_colors=True): - """Call system grep""" - if not isinstance(files, (list, tuple)): - files = [files] - if use_colors: - grep_options += ["--color=auto"] - - # Make grep output look nicer by 'cd netmiko_full_dir' - _, netmiko_full_dir = find_netmiko_dir() - os.chdir(netmiko_full_dir) - # Convert files to strip off the directory - retrieve_file = lambda x: x.split("/")[-1] # noqa - files = [retrieve_file(a_file) for a_file in files] - files.sort() - grep_list = [GREP] + grep_options + [pattern] + files - proc = subprocess.Popen(grep_list, shell=False) - proc.communicate() - return "" - - -def parse_arguments(args): - """Parse command-line arguments.""" - description = ( - "Return output from single show cmd using Netmiko (defaults to running-config)" - ) - parser = argparse.ArgumentParser(description=description) - parser.add_argument( - "devices", - nargs="?", - help="Device or group to connect to", - action="store", - type=str, - ) - parser.add_argument( - "--cmd", - help="Remote command to execute", - action="store", - default=None, - type=str, - ) - parser.add_argument("--username", help="Username", action="store", type=str) - parser.add_argument("--password", help="Password", action="store_true") - parser.add_argument("--secret", help="Enable Secret", action="store_true") - parser.add_argument("--use-cache", help="Use cached files", action="store_true") - parser.add_argument( - "--list-devices", help="List devices from inventory", action="store_true" - ) - parser.add_argument( - "--display-runtime", help="Display program runtime", action="store_true" - ) - parser.add_argument( - "--hide-failed", help="Hide failed devices", action="store_true" - ) - parser.add_argument( - "--json", help="Output results in JSON format", action="store_true" - ) - parser.add_argument("--raw", help="Display raw output", action="store_true") - parser.add_argument("--version", help="Display version", action="store_true") - cli_args = parser.parse_args(args) - if not cli_args.list_devices and not cli_args.version: - if not cli_args.devices: - parser.error("Devices not specified.") - return cli_args +COMMAND = "netmiko-show" def main_ep(): @@ -91,7 +26,7 @@ def main(args): start_time = datetime.now() # CLI ARGS ##### - cli_args = parse_arguments(args) + cli_args = parse_arguments(args, COMMAND) cli_username = cli_args.username if cli_args.username else None cli_password = getpass() if cli_args.password else None @@ -99,7 +34,7 @@ def main(args): version = cli_args.version if version: - print("netmiko-show v{}".format(__version__)) + print(f"{COMMAND} v{__version__}") return 0 list_devices = cli_args.list_devices if list_devices: @@ -114,7 +49,6 @@ def main(args): if cli_command: cmd_arg = True device_or_group = cli_args.devices.strip() - use_cached_files = cli_args.use_cache # noqa hide_failed = cli_args.hide_failed # DEVICE LOADING ##### From 23fa0731142a4403bfcdb81714794cef74878b95 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 1 Oct 2024 12:25:14 -0700 Subject: [PATCH 18/22] Common argument handling for netmiko-grep --- netmiko/cli_tools/argument_handling.py | 47 +++++--- netmiko/cli_tools/netmiko_grep.py | 145 ++++++++----------------- 2 files changed, 78 insertions(+), 114 deletions(-) diff --git a/netmiko/cli_tools/argument_handling.py b/netmiko/cli_tools/argument_handling.py index 8f4aa6d51..c5ff42bc0 100644 --- a/netmiko/cli_tools/argument_handling.py +++ b/netmiko/cli_tools/argument_handling.py @@ -3,13 +3,6 @@ def common_args(parser): """Add common arguments to the parser.""" - parser.add_argument( - "devices", - nargs="?", - help="Device or group to connect to", - action="store", - type=str, - ) parser.add_argument( "--cmd", help="Command to execute", @@ -38,18 +31,42 @@ def common_args(parser): def show_args(parser): """Add arguments specific to netmiko_show.py.""" - pass + parser.add_argument( + "devices", + help="Device or group to connect to", + action="store", + type=str, + ) def cfg_args(parser): """Add arguments specific to netmiko_cfg.py.""" + parser.add_argument( + "devices", + help="Device or group to connect to", + action="store", + type=str, + ) parser.add_argument( "--infile", help="Read commands from file", type=argparse.FileType("r") ) +def grep_args(parser): + """Add arguments specific to netmiko_grep.py.""" + parser.add_argument( + "pattern", nargs="?", help="Pattern to search for", action="store", type=str + ) + parser.add_argument( + "devices", + help="Device or group to connect to", + action="store", + type=str, + ) + + def parse_arguments(args, command): - """Parse command-line arguments for both scripts.""" + """Parse command-line arguments for all scripts.""" if command == "netmiko-cfg": description = "Execute configurations command using Netmiko" @@ -58,12 +75,10 @@ def parse_arguments(args, command): description = "Execute show command using Netmiko (defaults to 'show run')" addl_args = show_args elif command == "netmiko-grep": - # FIX - description = "" - # addl_args = grep_args + description = "Grep pattern search on Netmiko output (defaults to 'show run')" + addl_args = grep_args else: - # FIX: better message - raise ValueError() + raise ValueError(f"Unknown Netmiko cli-tool: {command}") parser = argparse.ArgumentParser(description=description) common_args(parser) @@ -75,7 +90,9 @@ def parse_arguments(args, command): if not cli_args.list_devices and not cli_args.version: if not cli_args.devices: parser.error("Devices not specified.") - if command == "netmiko-cfg" and not cli_args.cmd and not cli_args.infile: + elif command == "netmiko-cfg" and not cli_args.cmd and not cli_args.infile: parser.error("No configuration commands provided.") + elif command == "netmiko-grep" and not cli_args.pattern: + parser.error("Grep pattern not specified.") return cli_args diff --git a/netmiko/cli_tools/netmiko_grep.py b/netmiko/cli_tools/netmiko_grep.py index 1efc7fd86..0234e068c 100755 --- a/netmiko/cli_tools/netmiko_grep.py +++ b/netmiko/cli_tools/netmiko_grep.py @@ -1,6 +1,5 @@ #!/usr/bin/env python """Create grep like remote behavior on show run or command output.""" -import argparse import sys import os import subprocess @@ -9,11 +8,15 @@ from getpass import getpass from netmiko.utilities import load_devices, display_inventory -from netmiko.utilities import obtain_netmiko_filename, write_tmp_file, ensure_dir_exists +from netmiko.utilities import write_tmp_file, ensure_dir_exists from netmiko.utilities import find_netmiko_dir from netmiko.utilities import SHOW_RUN_MAPPER from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ -from netmiko.cli_tools.cli_helpers import obtain_devices, update_device_params, ssh_conn +from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn +from netmiko.cli_tools.argument_handling import parse_arguments + + +COMMAND = "netmiko-grep" def grepx(files, pattern, grep_options, use_colors=True): @@ -36,55 +39,13 @@ def grepx(files, pattern, grep_options, use_colors=True): return "" -def parse_arguments(args): - """Parse command-line arguments.""" - description = "Grep pattern search on Netmiko output (defaults to running-config)" - parser = argparse.ArgumentParser(description=description) - parser.add_argument( - "pattern", nargs="?", help="Pattern to search for", action="store", type=str - ) - parser.add_argument( - "devices", - nargs="?", - help="Device or group to connect to", - action="store", - type=str, - ) - parser.add_argument( - "--cmd", - help="Remote command to execute", - action="store", - default=None, - type=str, - ) - parser.add_argument("--username", help="Username", action="store", type=str) - parser.add_argument("--password", help="Password", action="store_true") - parser.add_argument("--secret", help="Enable Secret", action="store_true") - parser.add_argument("--use-cache", help="Use cached files", action="store_true") - parser.add_argument( - "--list-devices", help="List devices from inventory", action="store_true" - ) - parser.add_argument( - "--display-runtime", help="Display program runtime", action="store_true" - ) - parser.add_argument( - "--hide-failed", help="Hide failed devices", action="store_true" - ) - parser.add_argument("--version", help="Display version", action="store_true") - cli_args = parser.parse_args(args) - if not cli_args.list_devices and not cli_args.version: - if not cli_args.devices or not cli_args.pattern: - parser.error("Grep pattern or devices not specified.") - return cli_args - - def main_ep(): sys.exit(main(sys.argv[1:])) def main(args): start_time = datetime.now() - cli_args = parse_arguments(args) + cli_args = parse_arguments(args, COMMAND) cli_username = cli_args.username if cli_args.username else None cli_password = getpass() if cli_args.password else None @@ -92,7 +53,7 @@ def main(args): version = cli_args.version if version: - print("netmiko-grep v{}".format(__version__)) + print(f"{COMMAND} v{__version__}") return 0 list_devices = cli_args.list_devices if list_devices: @@ -106,7 +67,6 @@ def main(args): cmd_arg = True device_or_group = cli_args.devices.strip() pattern = cli_args.pattern - use_cached_files = cli_args.use_cache hide_failed = cli_args.hide_failed # DEVICE LOADING ##### @@ -116,57 +76,44 @@ def main(args): my_files = [] failed_devices = [] results = {} - if not use_cached_files: - - # UPDATE DEVICE PARAMS (WITH CLI ARGS) ##### - device_tasks = [] - for device_name, device_params in devices.items(): - update_device_params( - device_params, - username=cli_username, - password=cli_password, - secret=cli_secret, - ) - if not cmd_arg: - device_type = device_params["device_type"] - cli_command = SHOW_RUN_MAPPER.get(device_type, "show run") - device_tasks.append( - { - "device_name": device_name, - "device_params": device_params, - "cli_command": cli_command, - } - ) - - # THREADING ##### - with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: - futures = [executor.submit(ssh_conn, **kwargs) for kwargs in device_tasks] - for future in as_completed(futures): - device_name, output = future.result() - results[device_name] = output - - netmiko_base_dir, netmiko_full_dir = find_netmiko_dir() - ensure_dir_exists(netmiko_base_dir) - ensure_dir_exists(netmiko_full_dir) - for device_name, output in results.items(): - - file_name = write_tmp_file(device_name, output) - if ERROR_PATTERN not in output: - my_files.append(file_name) - else: - failed_devices.append(device_name) - else: - for device_name in devices: - file_name = obtain_netmiko_filename(device_name) - try: - with open(file_name) as f: - output = f.read() - except IOError: - return "Some cache files are missing: unable to use --use-cache option." - if ERROR_PATTERN not in output: - my_files.append(file_name) - else: - failed_devices.append(device_name) + + # UPDATE DEVICE PARAMS (WITH CLI ARGS) ##### + device_tasks = [] + for device_name, device_params in devices.items(): + update_device_params( + device_params, + username=cli_username, + password=cli_password, + secret=cli_secret, + ) + if not cmd_arg: + device_type = device_params["device_type"] + cli_command = SHOW_RUN_MAPPER.get(device_type, "show run") + device_tasks.append( + { + "device_name": device_name, + "device_params": device_params, + "cli_command": cli_command, + } + ) + + # THREADING ##### + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + futures = [executor.submit(ssh_conn, **kwargs) for kwargs in device_tasks] + for future in as_completed(futures): + device_name, output = future.result() + results[device_name] = output + + netmiko_base_dir, netmiko_full_dir = find_netmiko_dir() + ensure_dir_exists(netmiko_base_dir) + ensure_dir_exists(netmiko_full_dir) + for device_name, output in results.items(): + + file_name = write_tmp_file(device_name, output) + if ERROR_PATTERN not in output: + my_files.append(file_name) + else: + failed_devices.append(device_name) grep_options = [] grepx(my_files, pattern, grep_options) From e53ef6dfb91a369ed96a3d98da081e1552c8b0bc Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 1 Oct 2024 12:58:47 -0700 Subject: [PATCH 19/22] Consolidating argument processing into standardized function --- netmiko/cli_tools/argument_handling.py | 21 ++++++++++++++ netmiko/cli_tools/netmiko_cfg.py | 40 +++++++++----------------- netmiko/cli_tools/netmiko_grep.py | 30 ++++++------------- netmiko/cli_tools/netmiko_show.py | 34 ++++++---------------- 4 files changed, 50 insertions(+), 75 deletions(-) diff --git a/netmiko/cli_tools/argument_handling.py b/netmiko/cli_tools/argument_handling.py index c5ff42bc0..60a9ca911 100644 --- a/netmiko/cli_tools/argument_handling.py +++ b/netmiko/cli_tools/argument_handling.py @@ -1,4 +1,6 @@ import argparse +from getpass import getpass +from netmiko.utilities import load_devices, display_inventory def common_args(parser): @@ -96,3 +98,22 @@ def parse_arguments(args, command): parser.error("Grep pattern not specified.") return cli_args + + +def extract_cli_vars(cli_args, command, __version__): + + return_vars = {} + return_vars["cli_username"] = cli_args.username if cli_args.username else None + return_vars["cli_password"] = getpass() if cli_args.password else None + return_vars["cli_secret"] = getpass("Enable secret: ") if cli_args.secret else None + version = cli_args.version + if version: + print(f"{command} v{__version__}") + return 0 + list_devices = cli_args.list_devices + if list_devices: + my_devices = load_devices() + display_inventory(my_devices) + return 0 + + return return_vars diff --git a/netmiko/cli_tools/netmiko_cfg.py b/netmiko/cli_tools/netmiko_cfg.py index 474e83db4..dfbaa465e 100755 --- a/netmiko/cli_tools/netmiko_cfg.py +++ b/netmiko/cli_tools/netmiko_cfg.py @@ -2,14 +2,12 @@ """Return output from single show cmd using Netmiko.""" import sys from datetime import datetime -from getpass import getpass from concurrent.futures import ThreadPoolExecutor, as_completed -from netmiko.utilities import load_devices, display_inventory from netmiko.cli_tools import ERROR_PATTERN, MAX_WORKERS, __version__ from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices -from netmiko.cli_tools.argument_handling import parse_arguments +from netmiko.cli_tools.argument_handling import parse_arguments, extract_cli_vars COMMAND = "netmiko-cfg" @@ -21,24 +19,14 @@ def main_ep(): def main(args): start_time = datetime.now() + + # CLI ARGS ##### cli_args = parse_arguments(args, COMMAND) + cli_vars = extract_cli_vars(cli_args, command=COMMAND, __version__=__version__) + device_or_group = cli_args.devices.strip() + hide_failed = cli_args.hide_failed - cli_username = cli_args.username if cli_args.username else None - cli_password = getpass() if cli_args.password else None - cli_secret = getpass("Enable secret: ") if cli_args.secret else None - - version = cli_args.version - if version: - print(f"{COMMAND} v{__version__}") - return 0 - list_devices = cli_args.list_devices - if list_devices: - my_devices = load_devices() - display_inventory(my_devices) - return 0 - - output_json = cli_args.json - output_raw = cli_args.raw + # CFG COMMAND HANDLER ##### cfg_command = cli_args.cmd if cfg_command: if r"\n" in cfg_command: @@ -50,8 +38,6 @@ def main(args): cfg_command = command_data.splitlines() else: raise ValueError("No configuration commands provided.") - device_or_group = cli_args.devices.strip() - hide_failed = cli_args.hide_failed # DEVICE LOADING ##### devices = obtain_devices(device_or_group) @@ -65,9 +51,9 @@ def main(args): for device_name, device_params in devices.items(): update_device_params( device_params, - username=cli_username, - password=cli_password, - secret=cli_secret, + username=cli_vars["cli_username"], + password=cli_vars["cli_password"], + secret=cli_vars["cli_secret"], ) device_tasks.append( { @@ -97,11 +83,11 @@ def main(args): # OUTPUT PROCESSING ##### out_format = "text" - if output_json and output_raw: + if cli_args.json and cli_args.raw: out_format = "json_raw" - elif output_json: + elif cli_args.json: out_format = "json" - elif output_raw: + elif cli_args.raw: out_format = "raw" # elif output_yaml: # out_format = "yaml" diff --git a/netmiko/cli_tools/netmiko_grep.py b/netmiko/cli_tools/netmiko_grep.py index 0234e068c..039484f72 100755 --- a/netmiko/cli_tools/netmiko_grep.py +++ b/netmiko/cli_tools/netmiko_grep.py @@ -5,15 +5,13 @@ import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime -from getpass import getpass -from netmiko.utilities import load_devices, display_inventory from netmiko.utilities import write_tmp_file, ensure_dir_exists from netmiko.utilities import find_netmiko_dir from netmiko.utilities import SHOW_RUN_MAPPER from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn -from netmiko.cli_tools.argument_handling import parse_arguments +from netmiko.cli_tools.argument_handling import parse_arguments, extract_cli_vars COMMAND = "netmiko-grep" @@ -45,29 +43,17 @@ def main_ep(): def main(args): start_time = datetime.now() - cli_args = parse_arguments(args, COMMAND) - - cli_username = cli_args.username if cli_args.username else None - cli_password = getpass() if cli_args.password else None - cli_secret = getpass("Enable secret: ") if cli_args.secret else None - - version = cli_args.version - if version: - print(f"{COMMAND} v{__version__}") - return 0 - list_devices = cli_args.list_devices - if list_devices: - my_devices = load_devices() - display_inventory(my_devices) - return 0 + # CLI ARGS ##### + cli_args = parse_arguments(args, COMMAND) + cli_vars = extract_cli_vars(cli_args, command=COMMAND, __version__=__version__) cli_command = cli_args.cmd cmd_arg = False if cli_command: cmd_arg = True device_or_group = cli_args.devices.strip() - pattern = cli_args.pattern hide_failed = cli_args.hide_failed + pattern = cli_args.pattern # DEVICE LOADING ##### devices = obtain_devices(device_or_group) @@ -82,9 +68,9 @@ def main(args): for device_name, device_params in devices.items(): update_device_params( device_params, - username=cli_username, - password=cli_password, - secret=cli_secret, + username=cli_vars["cli_username"], + password=cli_vars["cli_password"], + secret=cli_vars["cli_secret"], ) if not cmd_arg: device_type = device_params["device_type"] diff --git a/netmiko/cli_tools/netmiko_show.py b/netmiko/cli_tools/netmiko_show.py index 0369f7b7e..646529bdf 100755 --- a/netmiko/cli_tools/netmiko_show.py +++ b/netmiko/cli_tools/netmiko_show.py @@ -4,15 +4,13 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime -from getpass import getpass from rich import print -from netmiko.utilities import load_devices, display_inventory from netmiko.utilities import SHOW_RUN_MAPPER from netmiko.cli_tools import ERROR_PATTERN, MAX_WORKERS, __version__ from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices -from netmiko.cli_tools.argument_handling import parse_arguments +from netmiko.cli_tools.argument_handling import parse_arguments, extract_cli_vars COMMAND = "netmiko-show" @@ -27,23 +25,7 @@ def main(args): # CLI ARGS ##### cli_args = parse_arguments(args, COMMAND) - - cli_username = cli_args.username if cli_args.username else None - cli_password = getpass() if cli_args.password else None - cli_secret = getpass("Enable secret: ") if cli_args.secret else None - - version = cli_args.version - if version: - print(f"{COMMAND} v{__version__}") - return 0 - list_devices = cli_args.list_devices - if list_devices: - my_devices = load_devices() - display_inventory(my_devices) - return 0 - - output_json = cli_args.json - output_raw = cli_args.raw + cli_vars = extract_cli_vars(cli_args, command=COMMAND, __version__=__version__) cli_command = cli_args.cmd cmd_arg = False if cli_command: @@ -63,9 +45,9 @@ def main(args): for device_name, device_params in devices.items(): update_device_params( device_params, - username=cli_username, - password=cli_password, - secret=cli_secret, + username=cli_vars["cli_username"], + password=cli_vars["cli_password"], + secret=cli_vars["cli_secret"], ) if not cmd_arg: device_type = device_params["device_type"] @@ -98,11 +80,11 @@ def main(args): # OUTPUT PROCESSING ##### out_format = "text" - if output_json and output_raw: + if cli_args.json and cli_args.raw: out_format = "json_raw" - elif output_json: + elif cli_args.json: out_format = "json" - elif output_raw: + elif cli_args.raw: out_format = "raw" # elif output_yaml: # out_format = "yaml" From 5dc95ce6f4c0c7e1c4d43dc0986683195fddb382 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 1 Oct 2024 13:24:09 -0700 Subject: [PATCH 20/22] Working first pass on new grep using rich --- netmiko/cli_tools/netmiko_grep.py | 68 +++++++++++-------------------- netmiko/cli_tools/outputters.py | 68 +++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 66 deletions(-) diff --git a/netmiko/cli_tools/netmiko_grep.py b/netmiko/cli_tools/netmiko_grep.py index 039484f72..25819cad2 100755 --- a/netmiko/cli_tools/netmiko_grep.py +++ b/netmiko/cli_tools/netmiko_grep.py @@ -1,42 +1,19 @@ #!/usr/bin/env python """Create grep like remote behavior on show run or command output.""" import sys -import os -import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime -from netmiko.utilities import write_tmp_file, ensure_dir_exists -from netmiko.utilities import find_netmiko_dir from netmiko.utilities import SHOW_RUN_MAPPER -from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__ +from netmiko.cli_tools import ERROR_PATTERN, MAX_WORKERS, __version__ from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn +from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices from netmiko.cli_tools.argument_handling import parse_arguments, extract_cli_vars COMMAND = "netmiko-grep" -def grepx(files, pattern, grep_options, use_colors=True): - """Call system grep""" - if not isinstance(files, (list, tuple)): - files = [files] - if use_colors: - grep_options += ["--color=auto"] - - # Make grep output look nicer by 'cd netmiko_full_dir' - _, netmiko_full_dir = find_netmiko_dir() - os.chdir(netmiko_full_dir) - # Convert files to strip off the directory - retrieve_file = lambda x: x.split("/")[-1] # noqa - files = [retrieve_file(a_file) for a_file in files] - files.sort() - grep_list = [GREP] + grep_options + [pattern] + files - proc = subprocess.Popen(grep_list, shell=False) - proc.communicate() - return "" - - def main_ep(): sys.exit(main(sys.argv[1:])) @@ -59,7 +36,6 @@ def main(args): devices = obtain_devices(device_or_group) # Retrieve output from devices - my_files = [] failed_devices = [] results = {} @@ -90,31 +66,35 @@ def main(args): device_name, output = future.result() results[device_name] = output - netmiko_base_dir, netmiko_full_dir = find_netmiko_dir() - ensure_dir_exists(netmiko_base_dir) - ensure_dir_exists(netmiko_full_dir) + # FIND FAILED DEVICES ##### + # NEED NEW WAY TO CACHE AND RE-USE CACHED FILES + valid_results = {} for device_name, output in results.items(): - - file_name = write_tmp_file(device_name, output) - if ERROR_PATTERN not in output: - my_files.append(file_name) - else: + # Cache output(?) + # file_name = write_tmp_file(device_name, output) + if ERROR_PATTERN in output: failed_devices.append(device_name) + continue + valid_results[device_name] = output + + # OUTPUT PROCESSING ##### + out_format = "text_highlighted" + if cli_args.json and cli_args.raw: + out_format = "json_raw" + elif cli_args.json: + out_format = "json" + elif cli_args.raw: + out_format = "raw" + # elif output_yaml: + # out_format = "yaml" + output_dispatcher(out_format, valid_results, pattern=pattern) - grep_options = [] - grepx(my_files, pattern, grep_options) if cli_args.display_runtime: print("Total time: {0}".format(datetime.now() - start_time)) if not hide_failed: - if failed_devices: - print("\n") - print("-" * 20) - print("Failed devices:") - failed_devices.sort() - for device_name in failed_devices: - print(" {}".format(device_name)) - print() + output_failed_devices(failed_devices) + return 0 diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 94f20d7ce..6e6afb45d 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -1,10 +1,12 @@ import json +import re import rich from rich.console import Console from rich.panel import Panel from rich.style import Style from rich.theme import Theme from rich.syntax import Syntax +from rich.text import Text NAVY_BLUE = "#000080" BLUE = "#1E90FF" @@ -52,9 +54,15 @@ def output_raw(results): print() -def output_text(results): +def output_text(results, pattern=None): console = Console(theme=CUSTOM_THEME) + for device_name, output in results.items(): + if pattern: + output = highlight_regex(output, pattern) + else: + output = Text(output) + panel = Panel( output, title=device_name, @@ -115,27 +123,6 @@ def output_yaml(results): pass -def output_dispatcher(out_format, results): - - # Sort the results dictionary by device_name - results = dict(sorted(results.items())) - - output_functions = { - "text": output_text, - "json": output_json, - "yaml": output_yaml, - "raw": output_raw, - "json_raw": output_json, - } - kwargs = {} - if out_format == "json_raw": - func = output_functions.get(out_format, output_text) - kwargs["raw"] = True - else: - func = output_functions.get(out_format, output_text) - return func(results, **kwargs) - - def output_failed_devices(failed_devices): if failed_devices: console = Console(theme=CUSTOM_THEME) @@ -161,3 +148,40 @@ def output_failed_devices(failed_devices): console.print() console.print(panel) console.print() + + +def highlight_regex(text, pattern, highlight_color="red"): + """ + Highlight text matching a regex pattern using Rich. + """ + text_obj = Text(text) + for match in re.finditer(pattern, text): + start, end = match.span() + text_obj.stylize(highlight_color, start, end) + + return text_obj + + +def output_dispatcher(out_format, results, pattern=None): + + # Sort the results dictionary by device_name + results = dict(sorted(results.items())) + + output_functions = { + "text": output_text, + "text_highlighted": output_text, + "json": output_json, + "yaml": output_yaml, + "raw": output_raw, + "json_raw": output_json, + } + kwargs = {} + func = output_functions.get(out_format, output_text) + if out_format == "text_highlighted": + if pattern is None: + raise ValueError("Regex search pattern must be set for!") + kwargs["pattern"] = pattern + elif out_format == "json_raw": + kwargs["raw"] = True + + return func(results, **kwargs) From 9633c7f5443ad811617a413b18490b9d5c5caea9 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 1 Oct 2024 13:37:20 -0700 Subject: [PATCH 21/22] Working first pass at experimental 'grep' --- netmiko/cli_tools/outputters.py | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/netmiko/cli_tools/outputters.py b/netmiko/cli_tools/outputters.py index 6e6afb45d..e778d1a1c 100644 --- a/netmiko/cli_tools/outputters.py +++ b/netmiko/cli_tools/outputters.py @@ -59,7 +59,8 @@ def output_text(results, pattern=None): for device_name, output in results.items(): if pattern: - output = highlight_regex(output, pattern) + # output = highlight_regex(output, pattern) + output = highlight_regex_with_context(output, pattern) else: output = Text(output) @@ -162,6 +163,40 @@ def highlight_regex(text, pattern, highlight_color="red"): return text_obj +def highlight_regex_with_context(text, pattern, highlight_color="red", context_lines=2): + """ + Highlight text matching a regex pattern using Rich, showing only the matching lines + with a specified number of context lines before and after. + """ + lines = text.split("\n") + text_obj = Text() + pattern = re.compile(pattern) + + for i, line in enumerate(lines): + if pattern.search(line): + # Add context lines before + start = max(0, i - context_lines) + for j in range(start, i): + text_obj.append(lines[j] + "\n") + + # Add the matching line with highlighting + line_obj = Text(line + "\n") + for match in pattern.finditer(line): + line_obj.stylize(highlight_color, match.start(), match.end()) + text_obj.append(line_obj) + + # Add context lines after + end = min(len(lines), i + context_lines + 1) + for j in range(i + 1, end): + text_obj.append(lines[j] + "\n") + + # Add a separator if this isn't the last match + if i + context_lines + 1 < len(lines): + text_obj.append("...\n\n") + + return text_obj + + def output_dispatcher(out_format, results, pattern=None): # Sort the results dictionary by device_name From ef231e8beee5238fea7f5e91f911ce5e4899b78f Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 1 Oct 2024 13:39:44 -0700 Subject: [PATCH 22/22] Ignore cli_tools for mypy --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 1de3c3ce1..90d5e6c3d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,3 +52,6 @@ ignore_errors = True [mypy-netmiko.cli_tools.outputters] ignore_errors = True + +[mypy-netmiko.cli_tools.argument_handling] +ignore_errors = True