Skip to content

Commit

Permalink
add f_lib.logging
Browse files Browse the repository at this point in the history
  • Loading branch information
ITProKyle committed Mar 4, 2024
1 parent e7523b7 commit 946d9b6
Show file tree
Hide file tree
Showing 17 changed files with 781 additions and 5 deletions.
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[{*.{json,py},Makefile}]
indent_size = 4

[Makefile]
indent_style = tab

[{cdk,package,package-lock,tsconfig,tslint}.json]
indent_size = 2
4 changes: 1 addition & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,5 @@
],
"restructuredtext.linter.doc8.extraArgs": [
"--config ${workspaceFolder}/pyproject.toml"
],
"restructuredtext.preview.scrollEditorWithPreview": false,
"restructuredtext.preview.scrollPreviewWithEditor": false
]
}
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
highlight_language = "default"
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None), # link to python docs
"rich": ("https://rich.readthedocs.io/en/stable", None), # link to rich docs
}
language = "en"
master_doc = "index"
Expand Down
7 changes: 6 additions & 1 deletion f_lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Finley library."""

import logging as __logging
from importlib.metadata import PackageNotFoundError, version

from . import aws, constants, mixins, utils
from . import aws, constants, logging, mixins, utils
from ._environment import Environment
from ._os_info import OsInfo
from ._system_info import SystemInfo, UnknownPlatformArchitectureError

# when creating loggers, always use instances of `f_lib.logging.Logger`
__logging.setLoggerClass(logging.Logger)

try:
__version__ = version(__name__)
except PackageNotFoundError: # cov: ignore
Expand All @@ -20,6 +24,7 @@
"SystemInfo",
"aws",
"constants",
"logging",
"mixins",
"utils",
"UnknownPlatformArchitectureError",
Expand Down
17 changes: 17 additions & 0 deletions f_lib/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Logging utilities."""

from ._constants import DEFAULT_LOG_FORMAT, DEFAULT_LOG_FORMAT_VERBOSE
from ._highlighters import ExtendableHighlighter, HighlightTypedDict
from ._log_level import LogLevel
from ._logger import Logger
from ._prefix_adaptor import PrefixAdaptor

__all__ = [
"DEFAULT_LOG_FORMAT",
"DEFAULT_LOG_FORMAT_VERBOSE",
"ExtendableHighlighter",
"HighlightTypedDict",
"LogLevel",
"Logger",
"PrefixAdaptor",
]
9 changes: 9 additions & 0 deletions f_lib/logging/_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Logging constants."""

from __future__ import annotations

DEFAULT_LOG_FORMAT = "%(message)s"
"""Default log format."""

DEFAULT_LOG_FORMAT_VERBOSE = "%(name)s:%(message)s"
"""Default log format when a verbose log level is used."""
61 changes: 61 additions & 0 deletions f_lib/logging/_highlighters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Custom :class:`rich.highlighting.Highlighter`."""

from __future__ import annotations

from functools import cached_property
from typing import TYPE_CHECKING, ClassVar, TypedDict

from rich.highlighter import Highlighter, ReprHighlighter

if TYPE_CHECKING:
from rich.text import Text


class HighlightTypedDict(TypedDict):
""":class:`typing.TypedDict` for highlights.
Used with :class:`f_lib.logging.ExtendableHighlighter`.
"""

base_style: str
"""Base name used for applying styles."""

highlights: list[str] | tuple[str, ...]
"""Regex patterns to highlight."""


class ExtendableHighlighter(Highlighter):
"""Extendable :class:`rich.highlighting.Highlighter`."""

__slots__ = ()

DEFAULT_HIGHLIGHTS: ClassVar[tuple[HighlightTypedDict, ...]] = (
HighlightTypedDict(
base_style=ReprHighlighter.base_style, highlights=ReprHighlighter.highlights
),
HighlightTypedDict(
base_style="aws.",
highlights=(
r"(?P<region>(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d)",
),
),
)

HIGHLIGHTS: ClassVar[tuple[HighlightTypedDict, ...]] = ()

@cached_property
def highlights(self) -> tuple[HighlightTypedDict, ...]:
"""All highlights of this highlighter."""
return self.DEFAULT_HIGHLIGHTS + self.HIGHLIGHTS

def highlight(self, text: Text) -> None:
"""Highlight :class:`rich.text.Text` using regular expressions.
Args:
text: Text to highlighted.
"""
for highlight in self.highlights:
for pattern in highlight["highlights"]:
text.highlight_regex(pattern, style_prefix=highlight["base_style"])
99 changes: 99 additions & 0 deletions f_lib/logging/_log_level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Log level enum."""

from __future__ import annotations

from enum import IntEnum


class LogLevel(IntEnum):
"""Log level enum."""

NOTSET = 0 # same as logging.NOTSET
"""When set on a logger, indicates that ancestor loggers are to be consulted
to determine the effective level.
If that still resolves to NOTSET, then all events are logged. When set on a
handler, all events are handled.
"""

SPAM = 5
"""Custom level for spam messages."""

DEBUG = 10 # same as logging.DEBUG
"""Detailed information, typically only of interest to a developer trying to diagnose a problem."""

VERBOSE = 15
"""Custom level between INFO and DEBUG.
Useful where some additional information might be desirable but does not cause
full information dumps everywhere.
"""

INFO = 20 # same as logging.INFO
"""Confirmation that things are working as expected.
This is the *default* level most things will want to set at.
"""

NOTICE = 25
"""Custom level situated between INFO and WARNING to draw attention without raising concern."""

WARNING = 30 # same as logging.WARNING
"""An indication that something unexpected happened, or that a problem might
occur in the near future (e.g. disk space low).
The software is still working as expected.
"""

SUCCESS = 35
"""Custom log level used when something good happens."""

ERROR = 40 # same as logging.ERROR
"""Due to a more serious problem, the software has not been able to perform some function."""

CRITICAL = 50 # same as logging.CRITICAL | logging.FATAL
"""A serious error, indicating that the program itself may be unable to continue running."""

FATAL = 50 # same as logging.CRITICAL | logging.FATAL
"""A serious error, indicating that the program itself may be unable to continue running."""

@classmethod
def from_verbosity(cls, verbosity: int) -> LogLevel:
"""Determine appropriate log level from verbosity.
+-----------+----------------------------------------+
| Verbosity | Log Level |
+===========+========================================+
| ``0`` | :attr:`f_lib.logging.LogLevel.FATAL` |
| ``1`` | :attr:`f_lib.logging.LogLevel.INFO` |
| ``2`` | :attr:`f_lib.logging.LogLevel.VERBOSE` |
| ``3`` | :attr:`f_lib.logging.LogLevel.DEBUG` |
| ``4`` | :attr:`f_lib.logging.LogLevel.DEBUG` |
| ``5``+ | :attr:`f_lib.logging.LogLevel.NOTSET` |
+-----------+----------------------------------------+
Args:
verbosity: Requested level of verbosity.
Returns:
A log level based on the table above.
"""
if not verbosity:
return cls.FATAL
if verbosity == 1:
return cls.INFO
if verbosity == 2:
return cls.VERBOSE
if verbosity < 5:
return cls.DEBUG
return cls.NOTSET

@classmethod
def has_value(cls, value: int) -> bool:
"""Check if :class:`f_lib.logging.LogLevel` has a value."""
return value in cls._value2member_map_
Loading

0 comments on commit 946d9b6

Please sign in to comment.