1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-01-17 08:48:05 +00:00

Migrate to typer for hyperglass CLI, implement new setup

This commit is contained in:
thatmattlove 2021-09-27 01:40:49 -07:00
parent fbe778a605
commit 2589c5fa06
18 changed files with 447 additions and 957 deletions

View file

@ -19,9 +19,12 @@ per-file-ignores=
hyperglass/models/*/__init__.py:F401
# Disable assertion and docstring checks on tests.
hyperglass/**/test_*.py:S101,D103
hyperglass/api/*.py:B008
hyperglass/state/hooks.py:F811
ignore=W503,C0330,R504,D202,S403,S301,S404,E731,D402,IF100
# Ignore whitespace in docstrings
hyperglass/cli/static.py:W293
# Ignore docstring standards
hyperglass/cli/main.py:D400,D403
ignore=W503,C0330,R504,D202,S403,S301,S404,E731,D402,IF100,B008
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
disable-noqa=False
hang-closing=False

View file

@ -1,6 +1,6 @@
"""hyperglass cli module."""
# Project
from hyperglass.cli.commands import hg
# Local
from .main import cli, run
CLI = hg
__all__ = ("cli", "run")

View file

@ -1,200 +0,0 @@
"""CLI Command definitions."""
# Standard Library
import sys
from pathlib import Path
# Third Party
from click import group, option, help_option
# Project
from hyperglass.util import cpu_count
# Local
from .echo import error, label, success, warning, cmd_help
from .util import build_ui
from .static import LABEL, CLI_HELP, E
from .installer import Installer
from .formatting import HelpColorsGroup, HelpColorsCommand, random_colors
# Define working directory
WORKING_DIR = Path(__file__).parent
supports_color = "utf" in sys.getfilesystemencoding().lower()
def _print_version(ctx, param, value):
# Project
from hyperglass import __version__
if not value or ctx.resilient_parsing:
return
label("hyperglass version: {v}", v=__version__)
ctx.exit()
@group(
cls=HelpColorsGroup,
help=CLI_HELP,
context_settings={"help_option_names": ["-h", "--help"], "color": supports_color},
help_headers_color=LABEL,
help_options_custom_colors=random_colors(
"build-ui", "start", "secret", "setup", "system-info", "clear-cache"
),
)
@option(
"-v",
"--version",
is_flag=True,
callback=_print_version,
expose_value=False,
is_eager=True,
help=cmd_help(E.NUMBERS, "hyperglass version", supports_color),
)
@help_option(
"-h", "--help", help=cmd_help(E.FOLDED_HANDS, "Show this help message", supports_color),
)
def hg():
"""Initialize Click Command Group."""
pass
@hg.command("build-ui", help=cmd_help(E.BUTTERFLY, "Create a new UI build", supports_color))
@option("-t", "--timeout", required=False, default=180, help="Timeout in seconds")
def build_frontend(timeout):
"""Create a new UI build."""
return build_ui(timeout)
@hg.command( # noqa: C901
"start",
help=cmd_help(E.ROCKET, "Start web server", supports_color),
cls=HelpColorsCommand,
help_options_custom_colors=random_colors("-b", "-d", "-w"),
)
@option("-b", "--build", is_flag=True, help="Render theme & build frontend assets")
@option(
"-d",
"--direct",
is_flag=True,
default=False,
help="Start hyperglass directly instead of through process manager",
)
@option(
"-w",
"--workers",
type=int,
required=False,
default=0,
help=f"Number of workers. By default, calculated from CPU cores [{cpu_count(2)}]",
)
def start(build, direct, workers): # noqa: C901
"""Start web server and optionally build frontend assets."""
# Project
from hyperglass.api import start as uvicorn_start
from hyperglass.main import start
kwargs = {}
if workers != 0:
kwargs["workers"] = workers
try:
if build:
build_complete = build_ui(timeout=180)
if build_complete and not direct:
start(**kwargs)
elif build_complete and direct:
uvicorn_start(**kwargs)
if not build and not direct:
start(**kwargs)
elif not build and direct:
uvicorn_start(**kwargs)
except (KeyboardInterrupt, SystemExit) as err:
error_message = str(err)
if (len(error_message)) > 1:
warning(str(err))
error("Stopping hyperglass due to keyboard interrupt.")
@hg.command(
"secret",
help=cmd_help(E.LOCK, "Generate agent secret", supports_color),
cls=HelpColorsCommand,
help_options_custom_colors=random_colors("-l"),
)
@option("-l", "--length", "length", default=32, help="Number of characters [default: 32]")
def generate_secret(length):
"""Generate secret for hyperglass-agent.
Arguments:
length {int} -- Length of secret
"""
# Standard Library
import secrets
gen_secret = secrets.token_urlsafe(length)
label("Secret: {s}", s=gen_secret)
@hg.command(
"setup",
help=cmd_help(E.TOOLBOX, "Run the setup wizard", supports_color),
cls=HelpColorsCommand,
help_options_custom_colors=random_colors("-d"),
)
@option(
"-d",
"--use-defaults",
"unattended",
default=False,
is_flag=True,
help="Use hyperglass defaults (requires no input)",
)
def setup(unattended):
"""Define application directory, move example files, generate systemd service."""
installer = Installer(unattended=unattended)
installer.install()
success(
"""Completed hyperglass installation.
After adding your {devices} file, you should run the {build_cmd} command.""", # noqa: E501
devices="devices.yaml",
build_cmd="hyperglass build-ui",
)
@hg.command(
"system-info",
help=cmd_help(E.THERMOMETER, " Get system information for a bug report", supports_color),
cls=HelpColorsCommand,
)
def get_system_info():
"""Get CPU, Memory, Disk, Python, & hyperglass version."""
# Project
from hyperglass.cli.util import system_info
system_info()
@hg.command(
"clear-cache",
help=cmd_help(E.SOAP, "Clear the Redis cache", supports_color),
cls=HelpColorsCommand,
)
def clear_cache():
"""Clear the Redis Cache."""
# Project
from hyperglass.state import use_state
state = use_state()
try:
state.clear()
success("Cleared Redis Cache")
except Exception as err:
error(str(err))

View file

@ -1,138 +1,42 @@
"""Helper functions for CLI message printing."""
# Standard Library
import re
# Third Party
from click import echo, style
import typing as t
# Project
from hyperglass.cli.static import CMD_HELP, Message
from hyperglass.cli.exceptions import CliError
from hyperglass.log import HyperglassConsole
def cmd_help(emoji="", help_text="", supports_color=False):
"""Print formatted command help."""
if supports_color:
help_str = emoji + style(help_text, **CMD_HELP)
else:
help_str = help_text
return help_str
class Echo:
"""Container for console-printing functions."""
_console = HyperglassConsole
def _fmt(self, message: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any:
if isinstance(message, str):
args = (f"[bold]{arg}[/bold]" for arg in args)
kwargs = {k: f"[bold]{v}[/bold]" for k, v in kwargs.items()}
return message.format(*args, **kwargs)
return message
def error(self, message: str, *args, **kwargs):
"""Print an error message."""
return self._console.print(self._fmt(message, *args, **kwargs), style="error")
def info(self, message: str, *args, **kwargs):
"""Print an informational message."""
return self._console.print(self._fmt(message, *args, **kwargs), style="info")
def warning(self, message: str, *args, **kwargs):
"""Print a warning message."""
return self._console.print(self._fmt(message, *args, **kwargs), style="info")
def success(self, message: str, *args, **kwargs):
"""Print a success message."""
return self._console.print(self._fmt(message, *args, **kwargs), style="success")
def plain(self, message: str, *args, **kwargs):
"""Print an unformatted message."""
return self._console.print(self._fmt(message, *args, **kwargs))
def _base_formatter(_text, _state, _callback, *args, **kwargs):
"""Format text block, replace template strings with keyword arguments.
Arguments:
state {dict} -- Text format attributes
label {dict} -- Keyword format attributes
text {[type]} -- Text to format
callback {function} -- Callback function
Returns:
{str|ClickException} -- Formatted output
"""
fmt = Message(_state)
if _callback is None:
_callback = style
nargs = ()
for i in args:
if not isinstance(i, str):
nargs += (str(i),)
else:
nargs += (i,)
for k, v in kwargs.items():
if not isinstance(v, str):
v = str(v)
kwargs[k] = style(v, **fmt.kw)
text_all = re.split(r"(\{\w+\})", _text)
text_all = [style(i, **fmt.msg) for i in text_all]
text_all = [i.format(*nargs, **kwargs) for i in text_all]
if fmt.emoji:
text_all.insert(0, fmt.emoji)
text_fmt = "".join(text_all)
return _callback(text_fmt)
def info(text, *args, **kwargs):
"""Generate formatted informational text.
Arguments:
text {str} -- Text to format
callback {callable} -- Callback function (default: {echo})
Returns:
{str} -- Informational output
"""
return _base_formatter(_state="info", _text=text, _callback=echo, *args, **kwargs)
def error(text, *args, **kwargs):
"""Generate formatted exception.
Arguments:
text {str} -- Text to format
callback {callable} -- Callback function (default: {echo})
Raises:
ClickException: Raised after formatting
"""
raise _base_formatter(text, "error", CliError, *args, **kwargs)
def success(text, *args, **kwargs):
"""Generate formatted success text.
Arguments:
text {str} -- Text to format
callback {callable} -- Callback function (default: {echo})
Returns:
{str} -- Success output
"""
return _base_formatter(_state="success", _text=text, _callback=echo, *args, **kwargs)
def warning(text, *args, **kwargs):
"""Generate formatted warning text.
Arguments:
text {str} -- Text to format
callback {callable} -- Callback function (default: {echo})
Returns:
{str} -- Warning output
"""
return _base_formatter(_state="warning", _text=text, _callback=echo, *args, **kwargs)
def label(text, *args, **kwargs):
"""Generate formatted info text with accented labels.
Arguments:
text {str} -- Text to format
callback {callable} -- Callback function (default: {echo})
Returns:
{str} -- Label output
"""
return _base_formatter(_state="label", _text=text, _callback=echo, *args, **kwargs)
def status(text, *args, **kwargs):
"""Generate formatted status text.
Arguments:
text {str} -- Text to format
callback {callable} -- Callback function (default: {echo})
Returns:
{str} -- Status output
"""
return _base_formatter(_state="status", _text=text, _callback=echo, *args, **kwargs)
echo = Echo()

View file

@ -1,15 +0,0 @@
"""hyperglass CLI custom exceptions."""
# Third Party
from click import ClickException, echo
from click._compat import get_text_stderr
class CliError(ClickException):
"""Custom exception to exclude the 'Error:' prefix from echos."""
def show(self, file=None):
"""Exclude 'Error:' prefix from raised exceptions."""
if file is None:
file = get_text_stderr()
echo(self.format_message())

View file

@ -1,171 +0,0 @@
"""Help formatting.
https://github.com/click-contrib/click-help-colors
MIT License
Copyright (c) 2016 Roman Tonkonozhko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
# Standard Library
import random
# Third Party
import click
def random_colors(*commands):
"""From tuple of commands, generate random but unique colors."""
colors = ["blue", "green", "red", "yellow", "magenta", "cyan", "white"]
num_colors = len(colors)
num_commands = len(commands)
if num_commands >= num_colors:
colors += colors
unique_colors = random.sample(colors, num_commands)
commands_fmt = {}
for i, cmd in enumerate(commands):
commands_fmt.update({cmd: {"fg": unique_colors[i], "bold": True}})
commands_fmt.update({"--help": {"fg": "white"}})
return commands_fmt
class HelpColorsFormatter(click.HelpFormatter):
"""Click help formatting plugin. See file docstring for license.
Modified from original copy to support click.style() instead of
direct ANSII string formatting.
"""
def __init__(
self, headers_color=None, options_color=None, options_custom_colors=None, *args, **kwargs
):
"""Initialize help formatter.
Keyword Arguments:
headers_color {dict} -- click.style() paramters for header
options_color {dict} -- click.style() paramters for options
options_custom_colors {dict} -- click.style() paramters for options by name
"""
self.headers_color = headers_color or {}
self.options_color = options_color or {}
self.options_custom_colors = options_custom_colors or {}
super().__init__(indent_increment=3, *args, **kwargs)
def _pick_color(self, option_name):
"""Filter options and pass relevant click.style() options for command."""
opt = option_name.split()[0].strip(",")
color = {}
if self.options_custom_colors and opt in self.options_custom_colors.keys():
color = self.options_custom_colors[opt]
else:
color = self.options_color
return color
def write_usage(self, prog, args="", prefix="Usage: "):
"""Write Usage: section."""
prefix_fmt = click.style(prefix, **self.headers_color)
super().write_usage(prog, args, prefix=prefix_fmt)
def write_heading(self, heading):
"""Write Heading section."""
heading_fmt = click.style(heading, **self.headers_color)
super().write_heading(heading_fmt)
def write_dl(self, rows, **kwargs):
"""Write Options section."""
colorized_rows = [(click.style(row[0], **self._pick_color(row[0])), row[1]) for row in rows]
super().write_dl(colorized_rows, **kwargs)
class HelpColorsMixin:
"""Click help formatting plugin. See file docstring for license.
Modified from original copy to support click.style() instead of
direct ANSII string formatting.
"""
def __init__(
self,
help_headers_color=None,
help_options_color=None,
help_options_custom_colors=None,
*args,
**kwargs
):
"""Initialize help mixin."""
self.help_headers_color = help_headers_color or {}
self.help_options_color = help_options_color or {}
self.help_options_custom_colors = help_options_custom_colors or {}
super().__init__(*args, **kwargs)
def get_help(self, ctx):
"""Format help."""
formatter = HelpColorsFormatter(
width=ctx.terminal_width,
max_width=ctx.max_content_width,
headers_color=self.help_headers_color,
options_color=self.help_options_color,
options_custom_colors=self.help_options_custom_colors,
)
self.format_help(ctx, formatter)
return formatter.getvalue().rstrip("\n")
class HelpColorsGroup(HelpColorsMixin, click.Group):
"""Click help formatting plugin. See file docstring for license.
Modified from original copy to support click.style() instead of
direct ANSII string formatting.
"""
def __init__(self, *args, **kwargs):
"""Initialize group formatter."""
super().__init__(*args, **kwargs)
def command(self, *args, **kwargs):
"""Set command values."""
kwargs.setdefault("cls", HelpColorsCommand)
kwargs.setdefault("help_headers_color", self.help_headers_color)
kwargs.setdefault("help_options_color", self.help_options_color)
kwargs.setdefault("help_options_custom_colors", self.help_options_custom_colors)
return super().command(*args, **kwargs)
def group(self, *args, **kwargs):
"""Set group values."""
kwargs.setdefault("cls", HelpColorsGroup)
kwargs.setdefault("help_headers_color", self.help_headers_color)
kwargs.setdefault("help_options_color", self.help_options_color)
kwargs.setdefault("help_options_custom_colors", self.help_options_custom_colors)
return super().group(*args, **kwargs)
class HelpColorsCommand(HelpColorsMixin, click.Command):
"""Click help formatting plugin. See file docstring for license.
Modified from original copy to support click.style() instead of
direct ANSII string formatting.
"""
def __init__(self, *args, **kwargs):
"""Initialize command formatter."""
super().__init__(*args, **kwargs)

View file

@ -2,89 +2,144 @@
# Standard Library
import os
import time
import shutil
import typing as t
import getpass
from types import TracebackType
from filecmp import dircmp
from pathlib import Path
# Third Party
import inquirer
import typer
from rich.progress import Progress
# Project
from hyperglass.util import compare_lists
from hyperglass.settings import Settings
from hyperglass.constants import __version__
# Local
from .echo import error, success, warning
from .util import create_dir
from .echo import echo
USER_PATH = Path.home() / "hyperglass"
ROOT_PATH = Path("/etc/hyperglass/")
ASSET_DIR = Path(__file__).parent.parent / "images"
IGNORED_FILES = [".DS_Store"]
INSTALL_PATHS = [
inquirer.List(
"install_path", message="Choose a directory for hyperglass", choices=[USER_PATH, ROOT_PATH],
)
]
def prompt_for_path() -> str:
"""Recursively prompt the user for an app path until one is provided."""
answer = inquirer.prompt(INSTALL_PATHS)
if answer is None:
warning("A directory for hyperglass is required")
answer = prompt_for_path()
return answer["install_path"]
class Installer:
"""Install hyperglass."""
def __init__(self, unattended: bool):
"""Initialize installer."""
app_path: Path
progress: Progress
user: str
assets: int
self.unattended = unattended
def __init__(self):
"""Start hyperglass installer."""
self.app_path = Settings.app_path
self.progress: Progress = Progress(console=echo._console)
self.user = getpass.getuser()
self.assets = len([p for p in ASSET_DIR.iterdir() if p.name not in IGNORED_FILES])
def install(self) -> None:
"""Complete the installation."""
"""Initialize tasks and start installer."""
permissions_task = self.progress.add_task("[bright purple]Checking System", total=2)
scaffold_task = self.progress.add_task(
"[bright blue]Creating Directory Structures", total=3
)
asset_task = self.progress.add_task(
"[bright cyan]Migrating Static Assets", total=self.assets
)
ui_task = self.progress.add_task("[bright teal]Initialzing UI", total=1, start=False)
self.app_path = self._get_app_path()
self._scaffold()
self._migrate_static_assets()
self.progress.start()
def _get_app_path(self) -> Path:
"""Find the app path from env variables or a prompt."""
self.check_permissions(task_id=permissions_task)
self.scaffold(task_id=scaffold_task)
self.migrate_static_assets(task_id=asset_task)
self.init_ui(task_id=ui_task)
if self.unattended:
return USER_PATH
def __enter__(self) -> t.Callable[[], None]:
"""Initialize tasks."""
self.progress.print(f"Starting hyperglass {__version__} setup")
return self.install
app_path = os.environ.get("HYPERGLASS_PATH", None)
def __exit__(
self,
exc_type: t.Optional[t.Type[BaseException]] = None,
exc_value: t.Optional[BaseException] = None,
exc_traceback: t.Optional[TracebackType] = None,
):
"""Print errors on exit."""
self.progress.stop()
if exc_type is not None:
echo._console.print_exception(show_locals=True)
raise typer.Exit(1)
raise typer.Exit(0)
if app_path is None:
app_path = prompt_for_path()
def check_permissions(self, task_id: int) -> None:
"""Ensure the executing user has permissions to the app path."""
read = os.access(self.app_path, os.R_OK)
if not read:
self.progress.print(
f"User {self.user!r} does not have read access to {self.app_path!s}", style="error"
)
raise typer.Exit(1)
return app_path
self.progress.advance(task_id)
time.sleep(0.4)
def _scaffold(self) -> None:
write = os.access(self.app_path, os.W_OK)
if not write:
self.progress.print(
f"User {self.user!r} does not have write access to {self.app_path!s}", style="error"
)
raise typer.Exit(1)
self.progress.advance(task_id)
def scaffold(self, task_id: int) -> None:
"""Create the file structure necessary for hyperglass to run."""
if not self.app_path.exists():
self.progress.print("Created {!s}".format(self.app_path), style="info")
self.app_path.mkdir(parents=True)
self.progress.print(f"hyperglass path is {self.app_path!s}", style="subtle")
self.progress.advance(task_id)
ui_dir = self.app_path / "static" / "ui"
images_dir = self.app_path / "static" / "images"
favicon_dir = images_dir / "favicons"
custom_dir = self.app_path / "static" / "custom"
favicon_dir = self.app_path / "static" / "images" / "favicons"
create_dir(self.app_path)
for path in (ui_dir, favicon_dir):
if not path.exists():
self.progress.print("Created {!s}".format(path), style="info")
path.mkdir(parents=True)
for path in (ui_dir, images_dir, favicon_dir, custom_dir):
create_dir(path, parents=True)
self.progress.advance(task_id)
time.sleep(0.4)
def _migrate_static_assets(self) -> bool:
def migrate_static_assets(self, task_id: int) -> None:
"""Synchronize the project assets with the installation assets."""
target_dir = self.app_path / "static" / "images"
def copy_func(src: str, dst: str):
time.sleep(self.assets / 10)
exists = Path(dst).exists()
if not exists:
copied = shutil.copy2(src, dst)
self.progress.print(f"Copied {copied!s}", style="info")
self.progress.advance(task_id)
return dst
if not target_dir.exists():
shutil.copytree(ASSET_DIR, target_dir)
shutil.copytree(
ASSET_DIR,
target_dir,
ignore=shutil.ignore_patterns(*IGNORED_FILES),
copy_function=copy_func,
)
# Compare the contents of the project's asset directory (considered
# the source of truth) with the installation directory. If they do
@ -92,19 +147,41 @@ class Installer:
# re-copy it.
compare_initial = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES)
if not compare_initial.left_list == compare_initial.right_list:
if not compare_lists(
compare_initial.left_list,
compare_initial.right_list,
ignore=["hyperglass-opengraph.jpg"],
):
shutil.rmtree(target_dir)
shutil.copytree(ASSET_DIR, target_dir)
shutil.copytree(
ASSET_DIR,
target_dir,
copy_function=copy_func,
ignore=shutil.ignore_patterns(*IGNORED_FILES),
)
# Re-compare the source and destination directory contents to
# ensure they match.
compare_post = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES)
if not compare_post.left_list == compare_post.right_list:
error(
"Files in {a} do not match files in {b}", a=str(ASSET_DIR), b=str(target_dir),
)
return False
if not compare_lists(
compare_post.left_list, compare_post.right_list, ignore=["hyperglass-opengraph.jpg"]
):
echo.error("Files in {!s} do not match files in {!s}", ASSET_DIR, target_dir)
raise typer.Exit(1)
else:
self.progress.update(task_id, completed=self.assets, refresh=True)
success("Migrated assets from {a} to {b}", a=str(ASSET_DIR), b=str(target_dir))
return True
def init_ui(self, task_id: int) -> None:
"""Initialize UI."""
# Project
from hyperglass.log import log
# Local
from .util import build_ui
with self.progress.console.capture():
log.disable("hyperglass")
build_ui(timeout=180)
log.enable("hyperglass")
self.progress.advance(task_id)

148
hyperglass/cli/main.py Normal file
View file

@ -0,0 +1,148 @@
"""hyperglass Command Line Interface."""
# Standard Library
import sys
import typing as t
# Third Party
import typer
# Local
from .echo import echo
def _version(value: bool) -> None:
# Project
from hyperglass import __version__
if value:
echo.info(__version__)
raise typer.Exit()
cli = typer.Typer(name="hyperglass", help="hyperglass Command Line Interface", no_args_is_help=True)
def run():
"""Run the hyperglass CLI."""
return typer.run(cli())
@cli.callback()
def version(
version: t.Optional[bool] = typer.Option(
None, "--version", help="hyperglass version", callback=_version
)
) -> None:
"""hyperglass"""
pass
@cli.command()
def start(build: bool = False, workers: t.Optional[int] = None) -> None:
"""Start hyperglass"""
# Project
from hyperglass.main import run
# Local
from .util import build_ui
kwargs = {}
if workers != 0:
kwargs["workers"] = workers
try:
if build:
build_complete = build_ui(timeout=180)
if build_complete:
run(workers)
else:
run(workers)
except (KeyboardInterrupt, SystemExit) as err:
error_message = str(err)
if (len(error_message)) > 1:
echo.warning(str(err))
echo.error("Stopping hyperglass due to keyboard interrupt.")
raise typer.Exit(0)
@cli.command()
def build_ui(timeout: int = typer.Option(180, help="Timeout in seconds")) -> None:
"""Create a new UI build."""
# Local
from .util import build_ui as _build_ui
with echo._console.status(
f"Starting new UI build with a {timeout} second timeout...", spinner="aesthetic"
):
_build_ui()
@cli.command()
def system_info():
"""Get system information for a bug report"""
# Third Party
from rich.table import Table
# Project
from hyperglass.util.system_info import get_system_info
# Local
from .static import MD_BOX
data = get_system_info()
rows = tuple(
(f"**{title}**", f"`{value!s}`" if mod == "code" else str(value))
for title, (value, mod) in data.items()
)
table = Table("Metric", "Value", box=MD_BOX)
for title, metric in rows:
table.add_row(title, metric)
echo.info("Please copy & paste this table in your bug report:\n")
echo.plain(table)
@cli.command()
def clear_cache():
"""Clear the Redis cache"""
# Project
from hyperglass.state import use_state
state = use_state()
try:
state.clear()
echo.success("Cleared Redis Cache")
except Exception as err:
if not sys.stdout.isatty():
echo._console.print_exception(show_locals=True)
raise typer.Exit(1)
echo.error("Error clearing cache: {!s}", err)
raise typer.Exit(1)
@cli.command()
def setup():
"""Initialize hyperglass setup."""
# Local
from .installer import Installer
with Installer() as start:
start()
@cli.command()
def settings():
"""Show hyperglass system settings (environment variables)"""
# Project
from hyperglass.settings import Settings
echo.plain(Settings)

View file

@ -1,6 +1,21 @@
"""Static string definitions."""
# Third Party
import click
from rich.box import Box
MD_BOX = Box(
"""\
| ||
|-||
| ||
| |
| |
| ||
""",
ascii=True,
)
class Char:
@ -27,99 +42,6 @@ class Char:
return str(self.char) + str(other)
class Emoji:
"""Helper class for unicode emoji."""
BUTTERFLY = "\U0001F98B "
CHECK = "\U00002705 "
INFO = "\U00002755 "
ERROR = "\U0000274C "
WARNING = "\U000026A0\U0000FE0F "
TOOLBOX = "\U0001F9F0 "
NUMBERS = "\U0001F522 "
FOLDED_HANDS = "\U0001F64F "
ROCKET = "\U0001F680 "
SPARKLES = "\U00002728 "
PAPERCLIP = "\U0001F4CE "
KEY = "\U0001F511 "
LOCK = "\U0001F512 "
CLAMP = "\U0001F5DC "
BOOKS = "\U0001F4DA "
THERMOMETER = "\U0001F321 "
SOAP = "\U0001F9FC "
WS = Char(" ")
NL = Char("\n")
CL = Char(":")
E = Emoji()
CLI_HELP = (
click.style("hyperglass", fg="magenta", bold=True)
+ WS[1]
+ click.style("Command Line Interface", fg="white")
)
# Click Style Helpers
SUCCESS = {"fg": "green", "bold": True}
WARNING = {"fg": "yellow"}
ERROR = {"fg": "red", "bold": True}
LABEL = {"fg": "white"}
INFO = {"fg": "blue", "bold": True}
STATUS = {"fg": "black"}
VALUE = {"fg": "magenta", "bold": True}
CMD_HELP = {"fg": "white"}
class Message:
"""Helper class for single-character strings."""
colors = {
"warning": "yellow",
"success": "green",
"error": "red",
"info": "blue",
"status": "black",
"label": "white",
}
label_colors = {
"warning": "yellow",
"success": "green",
"error": "red",
"info": "blue",
"status": "black",
"label": "magenta",
}
emojis = {
"warning": E.WARNING,
"success": E.CHECK,
"error": E.ERROR,
"info": E.INFO,
"status": "",
"label": "",
}
def __init__(self, state):
"""Set instance character."""
self.state = state
self.color = self.colors[self.state]
self.label_color = self.label_colors[self.state]
@property
def msg(self):
"""Click style attributes for message text."""
return {"fg": self.color}
@property
def kw(self):
"""Click style attributes for keywords."""
return {"fg": self.label_color, "bold": True, "underline": True}
@property
def emoji(self):
"""Match emoji from state."""
return self.emojis[self.state]
def __repr__(self):
"""Stringify the instance character for representation."""
return "Message(msg={m}, kw={k}, emoji={e})".format(m=self.msg, k=self.kw, e=self.emoji)

View file

@ -1,183 +1,47 @@
"""CLI utility functions."""
# Standard Library
import os
import sys
import asyncio
from pathlib import Path
# Third Party
from click import echo, style
import typer
# Project
from hyperglass.cli.echo import info, error, status, success
from hyperglass.cli.static import CL, NL, WS, E
PROJECT_ROOT = Path(__file__).parent.parent
def async_command(func) -> None:
"""Decororator for to make async functions runable from synchronous code."""
# Standard Library
import asyncio
from functools import update_wrapper
func = asyncio.coroutine(func)
def wrapper(*args, **kwargs):
loop = asyncio.get_event_loop()
return loop.run_until_complete(func(*args, **kwargs))
return update_wrapper(wrapper, func)
def start_web_server(start, params):
"""Start web server."""
msg_start = "Starting hyperglass web server on"
msg_uri = "http://"
msg_host = str(params["host"])
msg_port = str(params["port"])
msg_len = len("".join([msg_start, WS[1], msg_uri, msg_host, CL[1], msg_port]))
try:
echo(
NL[1]
+ WS[msg_len + 8]
+ E.ROCKET
+ NL[1]
+ E.CHECK
+ style(msg_start, fg="green", bold=True)
+ WS[1]
+ style(msg_uri, fg="white")
+ style(msg_host, fg="blue", bold=True)
+ style(CL[1], fg="white")
+ style(msg_port, fg="magenta", bold=True)
+ WS[1]
+ E.ROCKET
+ NL[1]
+ WS[1]
+ NL[1]
)
start()
except Exception as e:
error("Failed to start web server: {e}", e=e)
# Local
from .echo import echo
def build_ui(timeout: int) -> None:
"""Create a new UI build."""
try:
# Project
from hyperglass.state import use_state
from hyperglass.util.frontend import build_frontend
except ImportError as e:
error("Error importing UI builder: {e}", e=e)
# Project
from hyperglass.state import use_state
from hyperglass.util.frontend import build_frontend
state = use_state()
status("Starting new UI build with a {t} second timeout...", t=timeout)
if state.params.developer_mode:
dev_mode = "production"
if state.settings.dev_mode:
dev_mode = "development"
else:
dev_mode = "production"
try:
build_success = asyncio.run(
build_frontend(
app_path=state.settings.app_path,
dev_mode=state.settings.dev_mode,
dev_url=f"http://localhost:{state.settings.port!s}/",
prod_url="/api/",
params=state.ui_params,
force=True,
app_path=state.settings.app_path,
params=state.ui_params,
prod_url="/api/",
timeout=timeout,
)
)
if build_success:
success("Completed UI build in {m} mode", m=dev_mode)
echo.success("Completed UI build in {} mode", dev_mode)
except Exception as e:
error("Error building UI: {e}", e=e)
if not sys.stdout.isatty():
echo._console.print_exception(show_locals=True)
raise typer.Exit(1)
return True
def create_dir(path, **kwargs) -> bool:
"""Validate and attempt to create a directory, if it does not exist."""
# If input path is not a path object, try to make it one
if not isinstance(path, Path):
try:
path = Path(path)
except TypeError:
error("{p} is not a valid path", p=path)
# If path does not exist, try to create it
if not path.exists():
try:
path.mkdir(**kwargs)
except PermissionError:
error(
"{u} does not have permission to create {p}. Try running with sudo?",
u=os.getlogin(),
p=path,
)
# Verify the path was actually created
if path.exists():
success("Created {p}", p=path)
# If the path already exists, inform the user
elif path.exists():
info("{p} already exists", p=path)
return True
def write_to_file(file, data) -> bool:
"""Write string data to a file."""
try:
with file.open("w+") as f:
f.write(data.strip())
except PermissionError:
error(
"{u} does not have permission to write to {f}. Try running with sudo?",
u=os.getlogin(),
f=file,
)
if not file.exists():
error("Error writing file {f}", f=file)
elif file.exists():
success("Wrote systemd file {f}", f=file)
return True
def system_info() -> None:
"""Create a markdown table of various system information."""
# Project
from hyperglass.util.system_info import get_system_info
data = get_system_info()
def _code(val):
return f"`{str(val)}`"
def _bold(val):
return f"**{str(val)}**"
md_table_lines = ("| Metric | Value |", "| :----- | :---- |")
for title, metric in data.items():
value, mod = metric
title = _bold(title)
if mod == "code":
value = _code(value)
md_table_lines += (f"| {title} | {value} |",)
md_table = "\n".join(md_table_lines)
info("Please copy & paste this table in your bug report:\n")
echo(md_table + "\n")
return None
echo.error("Error building UI: {!s}", e)
raise typer.Exit(1)

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""hyperglass CLI management tool."""
# Project
from hyperglass.cli import CLI
# Local
from .cli import run
if __name__ == "__main__":
CLI()
run()

View file

@ -8,6 +8,8 @@ from datetime import datetime
# Third Party
from loguru import logger as _loguru_logger
from rich.theme import Theme
from rich.console import Console
from rich.logging import RichHandler
from gunicorn.glogging import Logger as GunicornLogger # type: ignore
@ -43,6 +45,18 @@ _LOG_LEVELS = [
{"name": "CRITICAL", "color": "<r>"},
]
HyperglassConsole = Console(
theme=Theme(
{
"info": "bold cyan",
"warning": "bold yellow",
"error": "bold red",
"success": "bold green",
"subtle": "rgb(128,128,128)",
}
)
)
class LibIntercentHandler(logging.Handler):
"""Custom log handler for integrating third party library logging with hyperglass's logger."""
@ -133,6 +147,7 @@ def init_logger(level: str = "INFO"):
# Use Rich for logging if hyperglass started from a TTY.
_loguru_logger.add(
sink=RichHandler(
console=HyperglassConsole,
rich_tracebacks=True,
level=level,
tracebacks_show_locals=True,

View file

@ -164,7 +164,8 @@ def start(*, log_level: str, workers: int, **kwargs) -> None:
).run()
if __name__ == "__main__":
def run(_workers: int = None):
"""Run hyperglass."""
try:
init_user_config()
@ -175,6 +176,9 @@ if __name__ == "__main__":
if Settings.debug is False:
workers, log_level = cpu_count(2), "WARNING"
if _workers is not None:
workers = _workers
setup_lib_logging(log_level)
start(log_level=log_level, workers=workers)
except Exception as error:
@ -188,3 +192,7 @@ if __name__ == "__main__":
except SystemExit:
# Handle Gunicorn exit.
sys.exit(4)
if __name__ == "__main__":
run()

View file

@ -18,6 +18,10 @@ from pydantic import (
# Project
from hyperglass.util import at_least, cpu_count
if t.TYPE_CHECKING:
# Third Party
from rich.console import Console, RenderResult, ConsoleOptions
ListenHost = t.Union[None, IPvAnyAddress, t.Literal["localhost"]]
@ -41,6 +45,34 @@ class HyperglassSettings(BaseSettings):
host: IPvAnyAddress = None
port: int = 8001
def __rich_console__(self, console: "Console", options: "ConsoleOptions") -> "RenderResult":
"""Render a Rich table representation of hyperglass settings."""
# Third Party
from rich.panel import Panel
from rich.style import Style
from rich.table import Table, box
from rich.pretty import Pretty
table = Table(box=box.MINIMAL, border_style="subtle")
table.add_column("Environment Variable", style=Style(color="#118ab2", bold=True))
table.add_column("Value")
params = sorted(
(
"debug",
"dev_mode",
"app_path",
"redis_host",
"redis_db",
"redis_dsn",
"host",
"port",
)
)
for attr in params:
table.add_row(f"hyperglass_{attr}".upper(), Pretty(getattr(self, attr)))
yield Panel.fit(table, title="hyperglass settings", border_style="subtle")
@validator("host", pre=True, always=True)
def validate_host(
cls: "HyperglassSettings", value: t.Any, values: t.Dict[str, t.Any]

View file

@ -385,3 +385,10 @@ def run_coroutine_in_new_thread(coroutine: t.Coroutine) -> t.Any:
thread.start()
thread.join()
return thread.result
def compare_lists(left: t.List[t.Any], right: t.List[t.Any], *, ignore: Series[t.Any] = ()) -> bool:
"""Determine if all items in left list exist in right list."""
left_ignored = [i for i in left if i not in ignore]
diff_ignored = [i for i in left if i in right and i not in ignore]
return len(left_ignored) == len(diff_ignored)

128
poetry.lock generated
View file

@ -1,18 +1,10 @@
[[package]]
name = "aiofiles"
version = "0.6.0"
version = "0.7.0"
description = "File support for asyncio."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "ansicon"
version = "1.89.0"
description = "Python wrapper for loading Jason Hood's ANSICON"
category = "main"
optional = false
python-versions = "*"
python-versions = ">=3.6,<4.0"
[[package]]
name = "appdirs"
@ -126,19 +118,6 @@ typed-ast = ">=1.4.0"
[package.extras]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "blessed"
version = "1.17.6"
description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
jinxed = {version = ">=0.5.4", markers = "platform_system == \"Windows\""}
six = ">=1.9.0"
wcwidth = ">=0.1.4"
[[package]]
name = "certifi"
version = "2020.6.20"
@ -273,7 +252,7 @@ test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<
[[package]]
name = "favicons"
version = "0.1.0"
version = "0.1.1"
description = "Favicon generator for Python 3 with strongly typed sync & async APIs, CLI, & HTML generation."
category = "main"
optional = false
@ -283,7 +262,7 @@ python-versions = ">=3.6.1,<4.0"
pillow = ">=7.2,<9.0"
rich = ">=6.0,<11.0"
svglib = ">=1.0.0,<2.0.0"
typer = ">=0.3.1,<0.4.0"
typer = ">=0.3.1,<1.0"
[[package]]
name = "filelock"
@ -606,19 +585,6 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "inquirer"
version = "2.7.0"
description = "Collection of common interactive command line user interfaces, based on Inquirer.js"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
blessed = "1.17.6"
python-editor = "1.0.4"
readchar = "2.0.1"
[[package]]
name = "isort"
version = "5.5.4"
@ -632,17 +598,6 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
[[package]]
name = "jinxed"
version = "1.0.1"
description = "Jinxed Terminal Library"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
ansicon = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "loguru"
version = "0.5.3"
@ -998,14 +953,6 @@ python-versions = "*"
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-editor"
version = "1.0.4"
description = "Programmatically open an editor, capture the result."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyyaml"
version = "5.4.1"
@ -1014,14 +961,6 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "readchar"
version = "2.0.1"
description = "Utilities to read single characters and key-strokes"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "redis"
version = "3.5.3"
@ -1276,20 +1215,20 @@ python-versions = "*"
[[package]]
name = "typer"
version = "0.3.2"
version = "0.4.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
click = ">=7.1.1,<7.2.0"
click = ">=7.1.1,<9.0.0"
[package.extras]
test = ["pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.782)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)", "shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)"]
all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"]
dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"]
doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"]
test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)"]
[[package]]
name = "typing-extensions"
@ -1355,14 +1294,6 @@ category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "wcwidth"
version = "0.2.5"
description = "Measures the displayed width of unicode strings in a terminal"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "webencodings"
version = "0.5.1"
@ -1401,16 +1332,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[metadata]
lock-version = "1.1"
python-versions = ">=3.8.1,<4.0"
content-hash = "1f1c9a87755507045ca8f1ec1132c48e637bb8f1d701caed3a48f280198e02e1"
content-hash = "d4a0600f54f56ba3641943af10908d325a459f301c073ec0f2f354ca7869d0ed"
[metadata.files]
aiofiles = [
{file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"},
{file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"},
]
ansicon = [
{file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
{file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"},
{file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
@ -1449,10 +1376,6 @@ black = [
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
]
blessed = [
{file = "blessed-1.17.6-py2.py3-none-any.whl", hash = "sha256:8371d69ac55558e4b1591964873d6721136e9ea17a730aeb3add7d27761b134b"},
{file = "blessed-1.17.6.tar.gz", hash = "sha256:a9a774fc6eda05248735b0d86e866d640ca2fef26038878f7e4d23f7749a1e40"},
]
certifi = [
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
@ -1552,8 +1475,8 @@ fastapi = [
{file = "fastapi-0.63.0.tar.gz", hash = "sha256:63c4592f5ef3edf30afa9a44fa7c6b7ccb20e0d3f68cd9eba07b44d552058dcb"},
]
favicons = [
{file = "favicons-0.1.0-py3-none-any.whl", hash = "sha256:1d8e9d6990c08a5e3dd5e00506278e30c7ee24eb43cc478f7ecd77685fd7ae2a"},
{file = "favicons-0.1.0.tar.gz", hash = "sha256:d70ccfdf6d8ae1315dbb83a9d62e792a60e968442fa23b8faa816d4b05771b9e"},
{file = "favicons-0.1.1-py3-none-any.whl", hash = "sha256:54b704c558414a67f43b5441869f4f82d8c5d88ddd1abcaba593977a57d90b47"},
{file = "favicons-0.1.1.tar.gz", hash = "sha256:76fe51870153c31ebe9ee8d88440919a7354e3194ae075e14275883e44917314"},
]
filelock = [
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
@ -1673,18 +1596,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
inquirer = [
{file = "inquirer-2.7.0-py2.py3-none-any.whl", hash = "sha256:d15e15de1ad5696f1967e7a23d8e2fce69d2e41a70b008948d676881ed94c3a5"},
{file = "inquirer-2.7.0.tar.gz", hash = "sha256:e819188de0ca7985a99c282176c6f50fb08b0d33867fd1965d3f3e97d6c8f83f"},
]
isort = [
{file = "isort-5.5.4-py3-none-any.whl", hash = "sha256:36f0c6659b9000597e92618d05b72d4181104cf59472b1c6a039e3783f930c95"},
{file = "isort-5.5.4.tar.gz", hash = "sha256:ba040c24d20aa302f78f4747df549573ae1eaf8e1084269199154da9c483f07f"},
]
jinxed = [
{file = "jinxed-1.0.1-py2.py3-none-any.whl", hash = "sha256:602f2cb3523c1045456f7b6d79ac19297fd8e933ae3bd9159845dc857f2d519c"},
{file = "jinxed-1.0.1.tar.gz", hash = "sha256:bc523c74fe676c99ccc69c68c2dcd7d4d2d7b2541f6dbef74ef211aedd8ad0d3"},
]
loguru = [
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
{file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"},
@ -1909,13 +1824,6 @@ python-dotenv = [
{file = "python-dotenv-0.17.0.tar.gz", hash = "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a"},
{file = "python_dotenv-0.17.0-py2.py3-none-any.whl", hash = "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"},
]
python-editor = [
{file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"},
{file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"},
{file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"},
{file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"},
{file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"},
]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@ -1947,10 +1855,6 @@ pyyaml = [
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
readchar = [
{file = "readchar-2.0.1-py2-none-any.whl", hash = "sha256:ed00b7a49bb12f345319d9fa393f289f03670310ada2beb55e8c3f017c648f1e"},
{file = "readchar-2.0.1-py3-none-any.whl", hash = "sha256:3ac34aab28563bc895f73233d5c08b28f951ca190d5850b8d4bec973132a8dca"},
]
redis = [
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
{file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
@ -2129,8 +2033,8 @@ typed-ast = [
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
typer = [
{file = "typer-0.3.2-py3-none-any.whl", hash = "sha256:ba58b920ce851b12a2d790143009fa00ac1d05b3ff3257061ff69dbdfc3d161b"},
{file = "typer-0.3.2.tar.gz", hash = "sha256:5455d750122cff96745b0dec87368f56d023725a7ebc9d2e54dd23dc86816303"},
{file = "typer-0.4.0-py3-none-any.whl", hash = "sha256:d81169725140423d072df464cad1ff25ee154ef381aaf5b8225352ea187ca338"},
{file = "typer-0.4.0.tar.gz", hash = "sha256:63c3aeab0549750ffe40da79a1b524f60e08a2cbc3126c520ebf2eeaf507f5dd"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
@ -2160,10 +2064,6 @@ watchgod = [
{file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"},
{file = "watchgod-0.7.tar.gz", hash = "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
webencodings = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},

View file

@ -32,15 +32,13 @@ hyperglass = "hyperglass.console:CLI"
Pillow = "^7.2"
PyJWT = "^2.0.1"
PyYAML = "^5.4.1"
aiofiles = "^0.6.0"
click = "^7.1.2"
aiofiles = "^0.7.0"
cryptography = "3.0.0"
distro = "^1.5.0"
fastapi = "^0.63.0"
favicons = ">=0.1.0,<1.0"
gunicorn = "^20.1.0"
httpx = "^0.17.1"
inquirer = "^2.6.3"
loguru = "^0.5.3"
netmiko = "^3.4.0"
paramiko = "^2.7.2"
@ -49,12 +47,13 @@ py-cpuinfo = "^7.0.0"
pydantic = {extras = ["dotenv"], version = "^1.8.2"}
python = ">=3.8.1,<4.0"
redis = "^3.5.3"
rich = "^10.11.0"
scrapli = {version = "2021.07.30", extras = ["asyncssh"]}
typer = "^0.4.0"
typing-extensions = "^3.7.4"
uvicorn = {extras = ["standard"], version = "^0.13.4"}
uvloop = "^0.14.0"
xmltodict = "^0.12.0"
rich = "^10.11.0"
[tool.poetry.dev-dependencies]
bandit = "^1.6.2"

View file

@ -7,7 +7,7 @@ from typing import Tuple, Union, Pattern
from pathlib import Path
# Third Party
import click
import typer
PACKAGE_JSON = Path(__file__).parent / "hyperglass" / "ui" / "package.json"
PACKAGE_JSON_PATTERN = re.compile(r"\s+\"version\"\:\s\"(.+)\"\,$")
@ -24,6 +24,8 @@ UPGRADES = (
("constants.py", CONSTANTS, CONSTANT_PATTERN),
)
cli = typer.Typer(name="version", no_args_is_help=True)
class Version:
"""Upgrade a file's version from one version to another."""
@ -115,20 +117,15 @@ class Version:
return (self.old_version, self.new_version)
@click.command(
name="Upgrade hyperglass Version",
help="Update pyproject.toml, constants.py, and package.json version statements",
)
@click.argument("new-version", nargs=1)
def update_versions(new_version: str) -> None:
"""Upgrade versions in pre-configured files to new version."""
"""Update hyperglass version in all package files."""
for name, file, pattern in UPGRADES:
with Version(
name=name, file=file, line_pattern=pattern, new_version=new_version,
) as version:
version.upgrade()
click.echo(str(version))
typer.echo(str(version))
if __name__ == "__main__":
update_versions()
typer.run(update_versions)