diff --git a/hyperglass/configuration/validate.py b/hyperglass/configuration/validate.py index c19293e..42e68df 100644 --- a/hyperglass/configuration/validate.py +++ b/hyperglass/configuration/validate.py @@ -6,6 +6,7 @@ from pydantic import ValidationError # Project from hyperglass.log import log, enable_file_logging, enable_syslog_logging +from hyperglass.settings import Settings from hyperglass.models.ui import UIParameters from hyperglass.models.directive import Directive, Directives from hyperglass.exceptions.private import ConfigError, ConfigInvalid @@ -32,18 +33,16 @@ def init_params() -> "Params": # Set up file logging once configuration parameters are initialized. enable_file_logging( - logger=log, log_directory=params.logging.directory, log_format=params.logging.format, log_max_size=params.logging.max_size, + debug=Settings.debug, ) # Set up syslog logging if enabled. if params.logging.syslog is not None and params.logging.syslog.enable: enable_syslog_logging( - logger=log, - syslog_host=params.logging.syslog.host, - syslog_port=params.logging.syslog.port, + syslog_host=params.logging.syslog.host, syslog_port=params.logging.syslog.port, ) if params.logging.http is not None and params.logging.http.enable: diff --git a/hyperglass/exceptions/_common.py b/hyperglass/exceptions/_common.py index c36c9e9..0bec60f 100644 --- a/hyperglass/exceptions/_common.py +++ b/hyperglass/exceptions/_common.py @@ -9,7 +9,7 @@ from pydantic import ValidationError # Project from hyperglass.log import log -from hyperglass.util import get_fmt_keys +from hyperglass.util import get_fmt_keys, repr_from_attrs from hyperglass.constants import STATUS_CODE_MAP ErrorLevel = Literal["danger", "warning"] @@ -29,11 +29,11 @@ class HyperglassError(Exception): self._level = level self._keywords = keywords or [] if self._level == "warning": - log.error(repr(self)) + log.error(str(self)) elif self._level == "danger": - log.critical(repr(self)) + log.critical(str(self)) else: - log.info(repr(self)) + log.info(str(self)) def __str__(self) -> str: """Return the instance's error message.""" @@ -41,7 +41,7 @@ class HyperglassError(Exception): def __repr__(self) -> str: """Return the instance's severity & error message in a string.""" - return f"[{self.level.upper()}] {self._message}" + return repr_from_attrs(self, ("_message", "level", "keywords"), strip="_") def dict(self) -> Dict[str, Union[str, List[str]]]: """Return the instance's attributes as a dictionary.""" diff --git a/hyperglass/log.py b/hyperglass/log.py index 36b650b..bddb6e9 100644 --- a/hyperglass/log.py +++ b/hyperglass/log.py @@ -1,7 +1,6 @@ """Logging instance setup & configuration.""" # Standard Library -import os import sys import typing as t import logging @@ -9,12 +8,29 @@ from datetime import datetime # Third Party from loguru import logger as _loguru_logger -from gunicorn.glogging import Logger # type: ignore +from rich.logging import RichHandler +from gunicorn.glogging import Logger as GunicornLogger # type: ignore + +# Local +from .constants import __version__ + +if t.TYPE_CHECKING: + # Standard Library + from pathlib import Path + + # Third Party + from loguru import Logger as LoguruLogger + from pydantic import ByteSize + + # Project + from hyperglass.models.fields import LogFormat _FMT = ( "[{level}] {time:YYYYMMDD} {time:HH:mm:ss} | {name}:" "{line} | {function} {message}" ) + +_FMT_FILE = "[{time:YYYYMMDD} {time:HH:mm:ss}] {message}" _DATE_FMT = "%Y%m%d %H:%M:%S" _FMT_BASIC = "{message}" _LOG_LEVELS = [ @@ -51,7 +67,7 @@ class LibIntercentHandler(logging.Handler): _loguru_logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) -class GunicornLogger(Logger): +class CustomGunicornLogger(GunicornLogger): """Custom logger to direct Gunicorn/Uvicorn logs to Loguru. See: https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/ @@ -95,8 +111,6 @@ def setup_lib_logging(log_level: str) -> None: seen.add(name.split(".")[0]) logging.getLogger(name).handlers = [intercept_handler] - _loguru_logger.configure(handlers=[{"sink": sys.stdout, "format": _FMT}]) - def _log_patcher(record): """Patch for exception handling in logger. @@ -109,20 +123,40 @@ def _log_patcher(record): record["exception"] = exception._replace(value=fixed) -def base_logger(level: str = "INFO"): +def init_logger(level: str = "INFO"): """Initialize hyperglass logging instance.""" + + # Reset built-in Loguru configurations. _loguru_logger.remove() - _loguru_logger.add(sys.stdout, format=_FMT, level=level, enqueue=True) + + if sys.stdout.isatty(): + # Use Rich for logging if hyperglass started from a TTY. + _loguru_logger.add( + sink=RichHandler( + rich_tracebacks=True, + level=level, + tracebacks_show_locals=True, + log_time_format="[%Y%m%d %H:%M:%S]", + ), + format=_FMT_BASIC, + level=level, + enqueue=True, + ) + else: + # Otherwise, use regular format. + _loguru_logger.add(sys.stdout, format=_FMT, level=level, enqueue=True) + _loguru_logger.configure(levels=_LOG_LEVELS, patcher=_log_patcher) + return _loguru_logger -log = base_logger() +log = init_logger() logging.addLevelName(25, "SUCCESS") -def _log_success(self, message, *a, **kw): +def _log_success(self: "LoguruLogger", message: str, *a: t.Any, **kw: t.Any) -> None: """Add custom builtin logging handler for the success level.""" if self.isEnabledFor(25): self._log(25, message, a, **kw) @@ -131,20 +165,13 @@ def _log_success(self, message, *a, **kw): logging.Logger.success = _log_success -def set_log_level(logger, debug): - """Set log level based on debug state.""" - if debug: - os.environ["HYPERGLASS_LOG_LEVEL"] = "DEBUG" - base_logger("DEBUG") - - if debug: - logger.debug("Debugging enabled") - return True - - -def enable_file_logging(logger, log_directory, log_format, log_max_size): +def enable_file_logging( + log_directory: "Path", log_format: "LogFormat", log_max_size: "ByteSize", debug: bool +) -> None: """Set up file-based logging from configuration parameters.""" + log_level = "DEBUG" if debug else "INFO" + if log_format == "json": log_file_name = "hyperglass.log.json" structured = True @@ -155,43 +182,41 @@ def enable_file_logging(logger, log_directory, log_format, log_max_size): log_file = log_directory / log_file_name if log_format == "text": - now_str = "hyperglass logs for " + datetime.utcnow().strftime( - "%B %d, %Y beginning at %H:%M:%S UTC" - ) - now_str_y = len(now_str) + 6 - now_str_x = len(now_str) + 4 - log_break = ( - "#" * now_str_y, - "\n#" + " " * now_str_x + "#\n", - "# ", - now_str, - " #", - "\n#" + " " * now_str_x + "#\n", - "#" * now_str_y, + now_str = datetime.utcnow().strftime("%B %d, %Y beginning at %H:%M:%S UTC") + header_lines = ( + f"# {line}" + for line in ( + f"hyperglass {__version__}", + f"Logs for {now_str}", + f"Log Level: {log_level}", + ) ) + header = "\n" + "\n".join(header_lines) + "\n" with log_file.open("a+") as lf: - lf.write(f'\n\n{"".join(log_break)}\n\n') + lf.write(header) - logger.add( - log_file, format=_FMT, rotation=log_max_size, serialize=structured, enqueue=True, + _loguru_logger.add( + enqueue=True, + sink=log_file, + format=_FMT_FILE, + serialize=structured, + level=log_level, + encoding="utf8", + rotation=log_max_size.human_readable(), ) - - logger.debug("Logging to {} enabled", str(log_file)) - - return True + log.debug("Logging to file {!s}", log_file) -def enable_syslog_logging(logger, syslog_host, syslog_port): +def enable_syslog_logging(syslog_host: str, syslog_port: int) -> None: """Set up syslog logging from configuration parameters.""" # Standard Library from logging.handlers import SysLogHandler - logger.add( + _loguru_logger.add( SysLogHandler(address=(str(syslog_host), syslog_port)), format=_FMT_BASIC, enqueue=True, ) - logger.debug( + log.debug( "Logging to syslog target {}:{} enabled", str(syslog_host), str(syslog_port), ) - return True diff --git a/hyperglass/main.py b/hyperglass/main.py index 8d280af..9da0bf3 100644 --- a/hyperglass/main.py +++ b/hyperglass/main.py @@ -12,7 +12,7 @@ from gunicorn.arbiter import Arbiter # type: ignore from gunicorn.app.base import BaseApplication # type: ignore # Local -from .log import GunicornLogger, log, set_log_level, setup_lib_logging +from .log import CustomGunicornLogger, log, setup_lib_logging from .plugins import ( InputPluginManager, OutputPluginManager, @@ -156,7 +156,7 @@ def start(*, log_level: str, workers: int, **kwargs) -> None: "bind": Settings.bind(), "on_starting": on_starting, "command": shutil.which("gunicorn"), - "logger_class": GunicornLogger, + "logger_class": CustomGunicornLogger, "worker_class": "uvicorn.workers.UvicornWorker", "logconfig_dict": {"formatters": {"generic": {"format": "%(message)s"}}}, **kwargs, @@ -167,7 +167,6 @@ def start(*, log_level: str, workers: int, **kwargs) -> None: if __name__ == "__main__": try: init_user_config() - set_log_level(log, Settings.debug) log.debug("System settings: {!r}", Settings) @@ -177,7 +176,6 @@ if __name__ == "__main__": workers, log_level = cpu_count(2), "WARNING" setup_lib_logging(log_level) - start(log_level=log_level, workers=workers) except Exception as error: # Handle app exceptions. diff --git a/hyperglass/models/api/cert_import.py b/hyperglass/models/api/cert_import.py index 7558411..6690fe1 100644 --- a/hyperglass/models/api/cert_import.py +++ b/hyperglass/models/api/cert_import.py @@ -3,10 +3,7 @@ from typing import Union # Third Party -from pydantic import BaseModel, StrictStr - -# Local -from ..fields import StrictBytes +from pydantic import BaseModel, StrictStr, StrictBytes class EncodedRequest(BaseModel): diff --git a/hyperglass/models/config/logging.py b/hyperglass/models/config/logging.py index 0c1198e..a2caf19 100644 --- a/hyperglass/models/config/logging.py +++ b/hyperglass/models/config/logging.py @@ -16,7 +16,6 @@ from pydantic import ( StrictBool, StrictFloat, DirectoryPath, - constr, validator, ) @@ -25,10 +24,7 @@ from hyperglass.constants import __version__ # Local from ..main import HyperglassModel - -HttpAuthMode = constr(regex=r"(basic|api_key)") -HttpProvider = constr(regex=r"(msteams|slack|generic)") -LogFormat = constr(regex=r"(text|json)") +from ..fields import LogFormat, HttpAuthMode, HttpProvider class Syslog(HyperglassModel): diff --git a/hyperglass/models/fields.py b/hyperglass/models/fields.py index fdb5367..bb57589 100644 --- a/hyperglass/models/fields.py +++ b/hyperglass/models/fields.py @@ -2,57 +2,17 @@ # Standard Library import re -from typing import TypeVar +import typing as t # Third Party -from pydantic import StrictInt, StrictFloat, constr +from pydantic import StrictInt, StrictFloat -IntFloat = TypeVar("IntFloat", StrictInt, StrictFloat) +IntFloat = t.TypeVar("IntFloat", StrictInt, StrictFloat) -SupportedDriver = constr(regex=r"(scrapli|netmiko|hyperglass_agent)") - - -class StrictBytes(bytes): - """Custom data type for a strict byte string. - - Used for validating the encoded JWT request payload. - """ - - @classmethod - def __get_validators__(cls): - """Yield Pydantic validator function. - - See: https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types - - Yields: - {function} -- Validator - """ - yield cls.validate - - @classmethod - def validate(cls, value): - """Validate type. - - Arguments: - value {Any} -- Pre-validated input - - Raises: - TypeError: Raised if value is not bytes - - Returns: - {object} -- Instantiated class - """ - if not isinstance(value, bytes): - raise TypeError("bytes required") - return cls() - - def __repr__(self): - """Return representation of object. - - Returns: - {str} -- Representation - """ - return f"StrictBytes({super().__repr__()})" +SupportedDriver = t.Literal["scrapli", "netmiko", "hyperglass_agent"] +HttpAuthMode = t.Literal["basic", "api_key"] +HttpProvider = t.Literal["msteams", "slack", "generic"] +LogFormat = t.Literal["text", "json"] class AnyUri(str): diff --git a/hyperglass/util/__init__.py b/hyperglass/util/__init__.py index ae3b087..c9b78be 100644 --- a/hyperglass/util/__init__.py +++ b/hyperglass/util/__init__.py @@ -16,7 +16,6 @@ from loguru._logger import Logger as LoguruLogger from netmiko.ssh_dispatcher import CLASS_MAPPER # type: ignore # Project -from hyperglass.log import log from hyperglass.types import Series from hyperglass.constants import DRIVER_MAP @@ -270,6 +269,9 @@ def resolve_hostname(hostname: str) -> t.Generator[t.Union[IPv4Address, IPv6Addr # Standard Library from socket import gaierror, getaddrinfo + # Project + from hyperglass.log import log + log.debug("Ensuring '{}' is resolvable...", hostname) ip4 = None diff --git a/poetry.lock b/poetry.lock index 67fbfa6..da7ebbf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -273,15 +273,15 @@ test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,< [[package]] name = "favicons" -version = "0.0.9" +version = "0.1.0" description = "Favicon generator for Python 3 with strongly typed sync & async APIs, CLI, & HTML generation." category = "main" optional = false python-versions = ">=3.6.1,<4.0" [package.dependencies] -pillow = ">=7.2,<8.0" -rich = ">=6.0,<9.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" @@ -1068,7 +1068,7 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "8.0.0" +version = "10.11.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -1078,7 +1078,6 @@ python-versions = ">=3.6,<4.0" colorama = ">=0.4.0,<0.5.0" commonmark = ">=0.9.0,<0.10.0" pygments = ">=2.6.0,<3.0.0" -typing-extensions = ">=3.7.4,<4.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] @@ -1402,7 +1401,7 @@ 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 = "c439e39b6aee8009b444a98905e88c1d16388c9026cf780ee3ca5ffde07434b1" +content-hash = "1f1c9a87755507045ca8f1ec1132c48e637bb8f1d701caed3a48f280198e02e1" [metadata.files] aiofiles = [ @@ -1553,8 +1552,8 @@ fastapi = [ {file = "fastapi-0.63.0.tar.gz", hash = "sha256:63c4592f5ef3edf30afa9a44fa7c6b7ccb20e0d3f68cd9eba07b44d552058dcb"}, ] favicons = [ - {file = "favicons-0.0.9-py3-none-any.whl", hash = "sha256:03b9e036ce8573ae03c7a9608af5b6ed6a8d60c5187fe8eb17130321b2b96f4e"}, - {file = "favicons-0.0.9.tar.gz", hash = "sha256:a3ca51f9ff95ec3d3d5e9a4da9b6ce9c461de5e680c15de6ed7eb84651187c3e"}, + {file = "favicons-0.1.0-py3-none-any.whl", hash = "sha256:1d8e9d6990c08a5e3dd5e00506278e30c7ee24eb43cc478f7ecd77685fd7ae2a"}, + {file = "favicons-0.1.0.tar.gz", hash = "sha256:d70ccfdf6d8ae1315dbb83a9d62e792a60e968442fa23b8faa816d4b05771b9e"}, ] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, @@ -1633,6 +1632,7 @@ gitpython = [ {file = "GitPython-3.1.9.tar.gz", hash = "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"}, ] gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] h11 = [ @@ -1720,6 +1720,8 @@ lxml = [ {file = "lxml-4.5.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293"}, {file = "lxml-4.5.2-cp38-cp38-win32.whl", hash = "sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f"}, {file = "lxml-4.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"}, + {file = "lxml-4.5.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6f767d11803dbd1274e43c8c0b2ff0a8db941e6ed0f5d44f852fb61b9d544b54"}, + {file = "lxml-4.5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d15a801d9037d7512edb2f1e196acebb16ab17bef4b25a91ea2e9a455ca353af"}, {file = "lxml-4.5.2.tar.gz", hash = "sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6"}, ] mccabe = [ @@ -1974,6 +1976,12 @@ regex = [ {file = "regex-2020.9.27-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637"}, {file = "regex-2020.9.27-cp38-cp38-win32.whl", hash = "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"}, {file = "regex-2020.9.27-cp38-cp38-win_amd64.whl", hash = "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c"}, + {file = "regex-2020.9.27-cp39-cp39-manylinux1_i686.whl", hash = "sha256:84cada8effefe9a9f53f9b0d2ba9b7b6f5edf8d2155f9fdbe34616e06ececf81"}, + {file = "regex-2020.9.27-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:816064fc915796ea1f26966163f6845de5af78923dfcecf6551e095f00983650"}, + {file = "regex-2020.9.27-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:5d892a4f1c999834eaa3c32bc9e8b976c5825116cde553928c4c8e7e48ebda67"}, + {file = "regex-2020.9.27-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c9443124c67b1515e4fe0bb0aa18df640965e1030f468a2a5dc2589b26d130ad"}, + {file = "regex-2020.9.27-cp39-cp39-win32.whl", hash = "sha256:49f23ebd5ac073765ecbcf046edc10d63dcab2f4ae2bce160982cb30df0c0302"}, + {file = "regex-2020.9.27-cp39-cp39-win_amd64.whl", hash = "sha256:3d20024a70b97b4f9546696cbf2fd30bae5f42229fbddf8661261b1eaff0deb7"}, {file = "regex-2020.9.27.tar.gz", hash = "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d"}, ] reportlab = [ @@ -2023,8 +2031,8 @@ rfc3986 = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] rich = [ - {file = "rich-8.0.0-py3-none-any.whl", hash = "sha256:3c5e4bb1e48c647bc75bc4ae7c125d399bec5b6ed2a319f0d447361635f02a9a"}, - {file = "rich-8.0.0.tar.gz", hash = "sha256:1b5023d2241e6552a24ddfe830a853fc8e53da4e6a6ed6c7105bb262593edf97"}, + {file = "rich-10.11.0-py3-none-any.whl", hash = "sha256:44bb3f9553d00b3c8938abf89828df870322b9ba43caf3b12bb7758debdc6dec"}, + {file = "rich-10.11.0.tar.gz", hash = "sha256:016fa105f34b69c434e7f908bb5bd7fefa9616efdb218a2917117683a6394ce5"}, ] scp = [ {file = "scp-0.13.3-py2.py3-none-any.whl", hash = "sha256:f2fa9fb269ead0f09b4e2ceb47621beb7000c135f272f6b70d3d9d29928d7bf0"}, @@ -2096,19 +2104,28 @@ typed-ast = [ {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, + {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, + {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typer = [ diff --git a/pyproject.toml b/pyproject.toml index 75ff815..d91e61d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ click = "^7.1.2" cryptography = "3.0.0" distro = "^1.5.0" fastapi = "^0.63.0" -favicons = "^0.0.9" +favicons = ">=0.1.0,<1.0" gunicorn = "^20.1.0" httpx = "^0.17.1" inquirer = "^2.6.3" @@ -54,6 +54,7 @@ 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" @@ -79,9 +80,9 @@ mccabe = "^0.6.1" pep8-naming = "^0.9.1" pre-commit = "^1.21.0" pytest = "^6.2.5" +pytest-dependency = "^0.5.1" stackprinter = "^0.2.3" taskipy = "^1.8.2" -pytest-dependency = "^0.5.1" [tool.black] line-length = 100