1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

Improve logging

This commit is contained in:
thatmattlove
2021-09-26 16:50:25 -07:00
parent e73de24904
commit fbe778a605
10 changed files with 122 additions and 127 deletions

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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 = (
"<lvl><b>[{level}]</b> {time:YYYYMMDD} {time:HH:mm:ss} <lw>|</lw> {name}<lw>:</lw>"
"<b>{line}</b> <lw>|</lw> {function}</lvl> <lvl><b>→</b></lvl> {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

View File

@@ -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.

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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

37
poetry.lock generated
View File

@@ -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 = [

View File

@@ -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