forked from mirrors/thatmattlove-hyperglass
Migrate to typer for hyperglass CLI, implement new setup
This commit is contained in:
parent
fbe778a605
commit
2589c5fa06
18 changed files with 447 additions and 957 deletions
7
.flake8
7
.flake8
|
|
@ -19,9 +19,12 @@ per-file-ignores=
|
||||||
hyperglass/models/*/__init__.py:F401
|
hyperglass/models/*/__init__.py:F401
|
||||||
# Disable assertion and docstring checks on tests.
|
# Disable assertion and docstring checks on tests.
|
||||||
hyperglass/**/test_*.py:S101,D103
|
hyperglass/**/test_*.py:S101,D103
|
||||||
hyperglass/api/*.py:B008
|
|
||||||
hyperglass/state/hooks.py:F811
|
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
|
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
|
||||||
disable-noqa=False
|
disable-noqa=False
|
||||||
hang-closing=False
|
hang-closing=False
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""hyperglass cli module."""
|
"""hyperglass cli module."""
|
||||||
|
|
||||||
# Project
|
# Local
|
||||||
from hyperglass.cli.commands import hg
|
from .main import cli, run
|
||||||
|
|
||||||
CLI = hg
|
__all__ = ("cli", "run")
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -1,138 +1,42 @@
|
||||||
"""Helper functions for CLI message printing."""
|
"""Helper functions for CLI message printing."""
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import re
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
|
||||||
from click import echo, style
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.cli.static import CMD_HELP, Message
|
from hyperglass.log import HyperglassConsole
|
||||||
from hyperglass.cli.exceptions import CliError
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_help(emoji="", help_text="", supports_color=False):
|
class Echo:
|
||||||
"""Print formatted command help."""
|
"""Container for console-printing functions."""
|
||||||
if supports_color:
|
|
||||||
help_str = emoji + style(help_text, **CMD_HELP)
|
_console = HyperglassConsole
|
||||||
else:
|
|
||||||
help_str = help_text
|
def _fmt(self, message: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
return help_str
|
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):
|
echo = Echo()
|
||||||
"""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)
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -2,89 +2,144 @@
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import shutil
|
import shutil
|
||||||
|
import typing as t
|
||||||
|
import getpass
|
||||||
|
from types import TracebackType
|
||||||
from filecmp import dircmp
|
from filecmp import dircmp
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# 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
|
# Local
|
||||||
from .echo import error, success, warning
|
from .echo import echo
|
||||||
from .util import create_dir
|
|
||||||
|
|
||||||
USER_PATH = Path.home() / "hyperglass"
|
|
||||||
ROOT_PATH = Path("/etc/hyperglass/")
|
|
||||||
ASSET_DIR = Path(__file__).parent.parent / "images"
|
ASSET_DIR = Path(__file__).parent.parent / "images"
|
||||||
IGNORED_FILES = [".DS_Store"]
|
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:
|
class Installer:
|
||||||
"""Install hyperglass."""
|
"""Install hyperglass."""
|
||||||
|
|
||||||
def __init__(self, unattended: bool):
|
app_path: Path
|
||||||
"""Initialize installer."""
|
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:
|
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.progress.start()
|
||||||
self._scaffold()
|
|
||||||
self._migrate_static_assets()
|
|
||||||
|
|
||||||
def _get_app_path(self) -> Path:
|
self.check_permissions(task_id=permissions_task)
|
||||||
"""Find the app path from env variables or a prompt."""
|
self.scaffold(task_id=scaffold_task)
|
||||||
|
self.migrate_static_assets(task_id=asset_task)
|
||||||
|
self.init_ui(task_id=ui_task)
|
||||||
|
|
||||||
if self.unattended:
|
def __enter__(self) -> t.Callable[[], None]:
|
||||||
return USER_PATH
|
"""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:
|
def check_permissions(self, task_id: int) -> None:
|
||||||
app_path = prompt_for_path()
|
"""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."""
|
"""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"
|
ui_dir = self.app_path / "static" / "ui"
|
||||||
images_dir = self.app_path / "static" / "images"
|
favicon_dir = self.app_path / "static" / "images" / "favicons"
|
||||||
favicon_dir = images_dir / "favicons"
|
|
||||||
custom_dir = self.app_path / "static" / "custom"
|
|
||||||
|
|
||||||
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):
|
self.progress.advance(task_id)
|
||||||
create_dir(path, parents=True)
|
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."""
|
"""Synchronize the project assets with the installation assets."""
|
||||||
|
|
||||||
target_dir = self.app_path / "static" / "images"
|
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():
|
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
|
# Compare the contents of the project's asset directory (considered
|
||||||
# the source of truth) with the installation directory. If they do
|
# the source of truth) with the installation directory. If they do
|
||||||
|
|
@ -92,19 +147,41 @@ class Installer:
|
||||||
# re-copy it.
|
# re-copy it.
|
||||||
compare_initial = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES)
|
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.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
|
# Re-compare the source and destination directory contents to
|
||||||
# ensure they match.
|
# ensure they match.
|
||||||
compare_post = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES)
|
compare_post = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES)
|
||||||
|
|
||||||
if not compare_post.left_list == compare_post.right_list:
|
if not compare_lists(
|
||||||
error(
|
compare_post.left_list, compare_post.right_list, ignore=["hyperglass-opengraph.jpg"]
|
||||||
"Files in {a} do not match files in {b}", a=str(ASSET_DIR), b=str(target_dir),
|
):
|
||||||
)
|
echo.error("Files in {!s} do not match files in {!s}", ASSET_DIR, target_dir)
|
||||||
return False
|
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))
|
def init_ui(self, task_id: int) -> None:
|
||||||
return True
|
"""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
148
hyperglass/cli/main.py
Normal 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)
|
||||||
|
|
@ -1,6 +1,21 @@
|
||||||
"""Static string definitions."""
|
"""Static string definitions."""
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
import click
|
from rich.box import Box
|
||||||
|
|
||||||
|
MD_BOX = Box(
|
||||||
|
"""\
|
||||||
|
|
||||||
|
| ||
|
||||||
|
|-||
|
||||||
|
| ||
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| ||
|
||||||
|
|
||||||
|
""",
|
||||||
|
ascii=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Char:
|
class Char:
|
||||||
|
|
@ -27,99 +42,6 @@ class Char:
|
||||||
return str(self.char) + str(other)
|
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(" ")
|
WS = Char(" ")
|
||||||
NL = Char("\n")
|
NL = Char("\n")
|
||||||
CL = Char(":")
|
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)
|
|
||||||
|
|
|
||||||
|
|
@ -1,183 +1,47 @@
|
||||||
"""CLI utility functions."""
|
"""CLI utility functions."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import os
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from click import echo, style
|
import typer
|
||||||
|
|
||||||
# Project
|
# Local
|
||||||
from hyperglass.cli.echo import info, error, status, success
|
from .echo import echo
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def build_ui(timeout: int) -> None:
|
def build_ui(timeout: int) -> None:
|
||||||
"""Create a new UI build."""
|
"""Create a new UI build."""
|
||||||
try:
|
# Project
|
||||||
# Project
|
from hyperglass.state import use_state
|
||||||
from hyperglass.state import use_state
|
from hyperglass.util.frontend import build_frontend
|
||||||
from hyperglass.util.frontend import build_frontend
|
|
||||||
except ImportError as e:
|
|
||||||
error("Error importing UI builder: {e}", e=e)
|
|
||||||
|
|
||||||
state = use_state()
|
state = use_state()
|
||||||
|
|
||||||
status("Starting new UI build with a {t} second timeout...", t=timeout)
|
dev_mode = "production"
|
||||||
|
if state.settings.dev_mode:
|
||||||
if state.params.developer_mode:
|
|
||||||
dev_mode = "development"
|
dev_mode = "development"
|
||||||
else:
|
|
||||||
dev_mode = "production"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
build_success = asyncio.run(
|
build_success = asyncio.run(
|
||||||
build_frontend(
|
build_frontend(
|
||||||
|
app_path=state.settings.app_path,
|
||||||
dev_mode=state.settings.dev_mode,
|
dev_mode=state.settings.dev_mode,
|
||||||
dev_url=f"http://localhost:{state.settings.port!s}/",
|
dev_url=f"http://localhost:{state.settings.port!s}/",
|
||||||
prod_url="/api/",
|
|
||||||
params=state.ui_params,
|
|
||||||
force=True,
|
force=True,
|
||||||
app_path=state.settings.app_path,
|
params=state.ui_params,
|
||||||
|
prod_url="/api/",
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if build_success:
|
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:
|
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
|
echo.error("Error building UI: {!s}", e)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""hyperglass CLI management tool."""
|
"""hyperglass CLI management tool."""
|
||||||
|
|
||||||
# Project
|
# Local
|
||||||
from hyperglass.cli import CLI
|
from .cli import run
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
CLI()
|
run()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from loguru import logger as _loguru_logger
|
from loguru import logger as _loguru_logger
|
||||||
|
from rich.theme import Theme
|
||||||
|
from rich.console import Console
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
from gunicorn.glogging import Logger as GunicornLogger # type: ignore
|
from gunicorn.glogging import Logger as GunicornLogger # type: ignore
|
||||||
|
|
||||||
|
|
@ -43,6 +45,18 @@ _LOG_LEVELS = [
|
||||||
{"name": "CRITICAL", "color": "<r>"},
|
{"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):
|
class LibIntercentHandler(logging.Handler):
|
||||||
"""Custom log handler for integrating third party library logging with hyperglass's logger."""
|
"""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.
|
# Use Rich for logging if hyperglass started from a TTY.
|
||||||
_loguru_logger.add(
|
_loguru_logger.add(
|
||||||
sink=RichHandler(
|
sink=RichHandler(
|
||||||
|
console=HyperglassConsole,
|
||||||
rich_tracebacks=True,
|
rich_tracebacks=True,
|
||||||
level=level,
|
level=level,
|
||||||
tracebacks_show_locals=True,
|
tracebacks_show_locals=True,
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,8 @@ def start(*, log_level: str, workers: int, **kwargs) -> None:
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def run(_workers: int = None):
|
||||||
|
"""Run hyperglass."""
|
||||||
try:
|
try:
|
||||||
init_user_config()
|
init_user_config()
|
||||||
|
|
||||||
|
|
@ -175,6 +176,9 @@ if __name__ == "__main__":
|
||||||
if Settings.debug is False:
|
if Settings.debug is False:
|
||||||
workers, log_level = cpu_count(2), "WARNING"
|
workers, log_level = cpu_count(2), "WARNING"
|
||||||
|
|
||||||
|
if _workers is not None:
|
||||||
|
workers = _workers
|
||||||
|
|
||||||
setup_lib_logging(log_level)
|
setup_lib_logging(log_level)
|
||||||
start(log_level=log_level, workers=workers)
|
start(log_level=log_level, workers=workers)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
|
@ -188,3 +192,7 @@ if __name__ == "__main__":
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
# Handle Gunicorn exit.
|
# Handle Gunicorn exit.
|
||||||
sys.exit(4)
|
sys.exit(4)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ from pydantic import (
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.util import at_least, cpu_count
|
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"]]
|
ListenHost = t.Union[None, IPvAnyAddress, t.Literal["localhost"]]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,6 +45,34 @@ class HyperglassSettings(BaseSettings):
|
||||||
host: IPvAnyAddress = None
|
host: IPvAnyAddress = None
|
||||||
port: int = 8001
|
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)
|
@validator("host", pre=True, always=True)
|
||||||
def validate_host(
|
def validate_host(
|
||||||
cls: "HyperglassSettings", value: t.Any, values: t.Dict[str, t.Any]
|
cls: "HyperglassSettings", value: t.Any, values: t.Dict[str, t.Any]
|
||||||
|
|
|
||||||
|
|
@ -385,3 +385,10 @@ def run_coroutine_in_new_thread(coroutine: t.Coroutine) -> t.Any:
|
||||||
thread.start()
|
thread.start()
|
||||||
thread.join()
|
thread.join()
|
||||||
return thread.result
|
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
128
poetry.lock
generated
|
|
@ -1,18 +1,10 @@
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiofiles"
|
name = "aiofiles"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
description = "File support for asyncio."
|
description = "File support for asyncio."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = ">=3.6,<4.0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ansicon"
|
|
||||||
version = "1.89.0"
|
|
||||||
description = "Python wrapper for loading Jason Hood's ANSICON"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "appdirs"
|
name = "appdirs"
|
||||||
|
|
@ -126,19 +118,6 @@ typed-ast = ">=1.4.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
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]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2020.6.20"
|
version = "2020.6.20"
|
||||||
|
|
@ -273,7 +252,7 @@ test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "favicons"
|
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."
|
description = "Favicon generator for Python 3 with strongly typed sync & async APIs, CLI, & HTML generation."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
|
@ -283,7 +262,7 @@ python-versions = ">=3.6.1,<4.0"
|
||||||
pillow = ">=7.2,<9.0"
|
pillow = ">=7.2,<9.0"
|
||||||
rich = ">=6.0,<11.0"
|
rich = ">=6.0,<11.0"
|
||||||
svglib = ">=1.0.0,<2.0.0"
|
svglib = ">=1.0.0,<2.0.0"
|
||||||
typer = ">=0.3.1,<0.4.0"
|
typer = ">=0.3.1,<1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
|
|
@ -606,19 +585,6 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
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]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "5.5.4"
|
version = "5.5.4"
|
||||||
|
|
@ -632,17 +598,6 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
|
||||||
requirements_deprecated_finder = ["pipreqs", "pip-api"]
|
requirements_deprecated_finder = ["pipreqs", "pip-api"]
|
||||||
colors = ["colorama (>=0.4.3,<0.5.0)"]
|
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]]
|
[[package]]
|
||||||
name = "loguru"
|
name = "loguru"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -998,14 +953,6 @@ python-versions = "*"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
cli = ["click (>=5.0)"]
|
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]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
|
|
@ -1014,14 +961,6 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
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]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "3.5.3"
|
version = "3.5.3"
|
||||||
|
|
@ -1276,20 +1215,20 @@ python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.3.2"
|
version = "0.4.0"
|
||||||
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
click = ">=7.1.1,<7.2.0"
|
click = ">=7.1.1,<9.0.0"
|
||||||
|
|
||||||
[package.extras]
|
[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)"]
|
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)"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
|
|
@ -1355,14 +1294,6 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
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]]
|
[[package]]
|
||||||
name = "webencodings"
|
name = "webencodings"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -1401,16 +1332,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = ">=3.8.1,<4.0"
|
python-versions = ">=3.8.1,<4.0"
|
||||||
content-hash = "1f1c9a87755507045ca8f1ec1132c48e637bb8f1d701caed3a48f280198e02e1"
|
content-hash = "d4a0600f54f56ba3641943af10908d325a459f301c073ec0f2f354ca7869d0ed"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
{file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"},
|
{file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"},
|
||||||
{file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"},
|
{file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"},
|
||||||
]
|
|
||||||
ansicon = [
|
|
||||||
{file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
|
|
||||||
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
|
|
||||||
]
|
]
|
||||||
appdirs = [
|
appdirs = [
|
||||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
{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-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
||||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
{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 = [
|
certifi = [
|
||||||
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
||||||
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
|
{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"},
|
{file = "fastapi-0.63.0.tar.gz", hash = "sha256:63c4592f5ef3edf30afa9a44fa7c6b7ccb20e0d3f68cd9eba07b44d552058dcb"},
|
||||||
]
|
]
|
||||||
favicons = [
|
favicons = [
|
||||||
{file = "favicons-0.1.0-py3-none-any.whl", hash = "sha256:1d8e9d6990c08a5e3dd5e00506278e30c7ee24eb43cc478f7ecd77685fd7ae2a"},
|
{file = "favicons-0.1.1-py3-none-any.whl", hash = "sha256:54b704c558414a67f43b5441869f4f82d8c5d88ddd1abcaba593977a57d90b47"},
|
||||||
{file = "favicons-0.1.0.tar.gz", hash = "sha256:d70ccfdf6d8ae1315dbb83a9d62e792a60e968442fa23b8faa816d4b05771b9e"},
|
{file = "favicons-0.1.1.tar.gz", hash = "sha256:76fe51870153c31ebe9ee8d88440919a7354e3194ae075e14275883e44917314"},
|
||||||
]
|
]
|
||||||
filelock = [
|
filelock = [
|
||||||
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
|
{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-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
{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 = [
|
isort = [
|
||||||
{file = "isort-5.5.4-py3-none-any.whl", hash = "sha256:36f0c6659b9000597e92618d05b72d4181104cf59472b1c6a039e3783f930c95"},
|
{file = "isort-5.5.4-py3-none-any.whl", hash = "sha256:36f0c6659b9000597e92618d05b72d4181104cf59472b1c6a039e3783f930c95"},
|
||||||
{file = "isort-5.5.4.tar.gz", hash = "sha256:ba040c24d20aa302f78f4747df549573ae1eaf8e1084269199154da9c483f07f"},
|
{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 = [
|
loguru = [
|
||||||
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
|
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
|
||||||
{file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"},
|
{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.tar.gz", hash = "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a"},
|
||||||
{file = "python_dotenv-0.17.0-py2.py3-none-any.whl", hash = "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"},
|
{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 = [
|
pyyaml = [
|
||||||
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
|
{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"},
|
{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-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
|
||||||
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
|
{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 = [
|
redis = [
|
||||||
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
||||||
{file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
|
{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"},
|
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
|
||||||
]
|
]
|
||||||
typer = [
|
typer = [
|
||||||
{file = "typer-0.3.2-py3-none-any.whl", hash = "sha256:ba58b920ce851b12a2d790143009fa00ac1d05b3ff3257061ff69dbdfc3d161b"},
|
{file = "typer-0.4.0-py3-none-any.whl", hash = "sha256:d81169725140423d072df464cad1ff25ee154ef381aaf5b8225352ea187ca338"},
|
||||||
{file = "typer-0.3.2.tar.gz", hash = "sha256:5455d750122cff96745b0dec87368f56d023725a7ebc9d2e54dd23dc86816303"},
|
{file = "typer-0.4.0.tar.gz", hash = "sha256:63c3aeab0549750ffe40da79a1b524f60e08a2cbc3126c520ebf2eeaf507f5dd"},
|
||||||
]
|
]
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
|
{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-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"},
|
||||||
{file = "watchgod-0.7.tar.gz", hash = "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29"},
|
{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 = [
|
webencodings = [
|
||||||
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
|
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
|
||||||
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
|
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,13 @@ hyperglass = "hyperglass.console:CLI"
|
||||||
Pillow = "^7.2"
|
Pillow = "^7.2"
|
||||||
PyJWT = "^2.0.1"
|
PyJWT = "^2.0.1"
|
||||||
PyYAML = "^5.4.1"
|
PyYAML = "^5.4.1"
|
||||||
aiofiles = "^0.6.0"
|
aiofiles = "^0.7.0"
|
||||||
click = "^7.1.2"
|
|
||||||
cryptography = "3.0.0"
|
cryptography = "3.0.0"
|
||||||
distro = "^1.5.0"
|
distro = "^1.5.0"
|
||||||
fastapi = "^0.63.0"
|
fastapi = "^0.63.0"
|
||||||
favicons = ">=0.1.0,<1.0"
|
favicons = ">=0.1.0,<1.0"
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
httpx = "^0.17.1"
|
httpx = "^0.17.1"
|
||||||
inquirer = "^2.6.3"
|
|
||||||
loguru = "^0.5.3"
|
loguru = "^0.5.3"
|
||||||
netmiko = "^3.4.0"
|
netmiko = "^3.4.0"
|
||||||
paramiko = "^2.7.2"
|
paramiko = "^2.7.2"
|
||||||
|
|
@ -49,12 +47,13 @@ py-cpuinfo = "^7.0.0"
|
||||||
pydantic = {extras = ["dotenv"], version = "^1.8.2"}
|
pydantic = {extras = ["dotenv"], version = "^1.8.2"}
|
||||||
python = ">=3.8.1,<4.0"
|
python = ">=3.8.1,<4.0"
|
||||||
redis = "^3.5.3"
|
redis = "^3.5.3"
|
||||||
|
rich = "^10.11.0"
|
||||||
scrapli = {version = "2021.07.30", extras = ["asyncssh"]}
|
scrapli = {version = "2021.07.30", extras = ["asyncssh"]}
|
||||||
|
typer = "^0.4.0"
|
||||||
typing-extensions = "^3.7.4"
|
typing-extensions = "^3.7.4"
|
||||||
uvicorn = {extras = ["standard"], version = "^0.13.4"}
|
uvicorn = {extras = ["standard"], version = "^0.13.4"}
|
||||||
uvloop = "^0.14.0"
|
uvloop = "^0.14.0"
|
||||||
xmltodict = "^0.12.0"
|
xmltodict = "^0.12.0"
|
||||||
rich = "^10.11.0"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
bandit = "^1.6.2"
|
bandit = "^1.6.2"
|
||||||
|
|
|
||||||
15
version.py
15
version.py
|
|
@ -7,7 +7,7 @@ from typing import Tuple, Union, Pattern
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
import click
|
import typer
|
||||||
|
|
||||||
PACKAGE_JSON = Path(__file__).parent / "hyperglass" / "ui" / "package.json"
|
PACKAGE_JSON = Path(__file__).parent / "hyperglass" / "ui" / "package.json"
|
||||||
PACKAGE_JSON_PATTERN = re.compile(r"\s+\"version\"\:\s\"(.+)\"\,$")
|
PACKAGE_JSON_PATTERN = re.compile(r"\s+\"version\"\:\s\"(.+)\"\,$")
|
||||||
|
|
@ -24,6 +24,8 @@ UPGRADES = (
|
||||||
("constants.py", CONSTANTS, CONSTANT_PATTERN),
|
("constants.py", CONSTANTS, CONSTANT_PATTERN),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cli = typer.Typer(name="version", no_args_is_help=True)
|
||||||
|
|
||||||
|
|
||||||
class Version:
|
class Version:
|
||||||
"""Upgrade a file's version from one version to another."""
|
"""Upgrade a file's version from one version to another."""
|
||||||
|
|
@ -115,20 +117,15 @@ class Version:
|
||||||
return (self.old_version, self.new_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:
|
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:
|
for name, file, pattern in UPGRADES:
|
||||||
with Version(
|
with Version(
|
||||||
name=name, file=file, line_pattern=pattern, new_version=new_version,
|
name=name, file=file, line_pattern=pattern, new_version=new_version,
|
||||||
) as version:
|
) as version:
|
||||||
version.upgrade()
|
version.upgrade()
|
||||||
click.echo(str(version))
|
typer.echo(str(version))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
update_versions()
|
typer.run(update_versions)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue