mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-18 09:53:40 -05:00
feat: initial application release
This commit is contained in:
27
src/obsidian_metadata/_utils/__init__.py
Normal file
27
src/obsidian_metadata/_utils/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Shared utilities."""
|
||||
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata._utils.alerts import LoggerManager
|
||||
from obsidian_metadata._utils.utilities import (
|
||||
clean_dictionary,
|
||||
clear_screen,
|
||||
dict_contains,
|
||||
dict_values_to_lists_strings,
|
||||
docstring_parameter,
|
||||
remove_markdown_sections,
|
||||
vault_validation,
|
||||
version_callback,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"alerts",
|
||||
"clean_dictionary",
|
||||
"clear_screen",
|
||||
"dict_values_to_lists_strings",
|
||||
"dict_contains",
|
||||
"docstring_parameter",
|
||||
"LoggerManager",
|
||||
"remove_markdown_sections",
|
||||
"vault_validation",
|
||||
"version_callback",
|
||||
]
|
||||
242
src/obsidian_metadata/_utils/alerts.py
Normal file
242
src/obsidian_metadata/_utils/alerts.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""Logging and alerts."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import rich.repr
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich import print
|
||||
|
||||
|
||||
def dryrun(msg: str) -> None:
|
||||
"""Print a message if the dry run flag is set.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[cyan]DRYRUN | {msg}[/cyan]")
|
||||
|
||||
|
||||
def success(msg: str) -> None:
|
||||
"""Print a success message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[green]SUCCESS | {msg}[/green]")
|
||||
|
||||
|
||||
def warning(msg: str) -> None:
|
||||
"""Print a warning message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[yellow]WARNING | {msg}[/yellow]")
|
||||
|
||||
|
||||
def error(msg: str) -> None:
|
||||
"""Print an error message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[red]ERROR | {msg}[/red]")
|
||||
|
||||
|
||||
def notice(msg: str) -> None:
|
||||
"""Print a notice message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[bold]NOTICE | {msg}[/bold]")
|
||||
|
||||
|
||||
def info(msg: str) -> None:
|
||||
"""Print a notice message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"INFO | {msg}")
|
||||
|
||||
|
||||
def dim(msg: str) -> None:
|
||||
"""Print a message in dimmed color.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[dim]{msg}[/dim]")
|
||||
|
||||
|
||||
def _log_formatter(record: dict) -> str:
|
||||
"""Create custom log formatter based on the log level. This effects the logs sent to stdout/stderr but not the log file."""
|
||||
if (
|
||||
record["level"].name == "INFO"
|
||||
or record["level"].name == "SUCCESS"
|
||||
or record["level"].name == "WARNING"
|
||||
):
|
||||
return "<level>{level: <8}</level> | <level>{message}</level>\n{exception}"
|
||||
|
||||
return "<level>{level: <8}</level> | <level>{message}</level> <fg #c5c5c5>({name}:{function}:{line})</fg #c5c5c5>\n{exception}"
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class LoggerManager:
|
||||
"""Instantiate the loguru logging system with the following levels.
|
||||
|
||||
- TRACE: Usage: log.trace("")
|
||||
- DEBUG: Usage: log.debug("")
|
||||
- INFO: Usage: log.info("")
|
||||
- WARNING: Usage: log.warning("")
|
||||
- ERROR: Usage: log.error("")
|
||||
- CRITICAL: Usage: log.critical("")
|
||||
- EXCEPTION: Usage: log.exception("")
|
||||
|
||||
Attributes:
|
||||
log_file (Path): Path to the log file.
|
||||
verbosity (int): Verbosity level.
|
||||
log_to_file (bool): Whether to log to a file.
|
||||
log_level (int): Default log level (verbosity overrides this)
|
||||
|
||||
Examples:
|
||||
Instantiate the logger:
|
||||
|
||||
logging = _utils.alerts.LoggerManager(
|
||||
verbosity,
|
||||
log_to_file,
|
||||
log_file,
|
||||
log_level)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log_file: Path = Path("/logs"),
|
||||
verbosity: int = 0,
|
||||
log_to_file: bool = False,
|
||||
log_level: int = 30,
|
||||
) -> None:
|
||||
self.verbosity = verbosity
|
||||
self.log_to_file = log_to_file
|
||||
self.log_file = log_file
|
||||
self.log_level = log_level
|
||||
|
||||
if self.log_file == Path("/logs") and self.log_to_file: # pragma: no cover
|
||||
print("No log file specified")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if self.verbosity >= 3:
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level="TRACE",
|
||||
format=_log_formatter, # type: ignore[arg-type]
|
||||
backtrace=False,
|
||||
diagnose=True,
|
||||
)
|
||||
self.log_level = 5
|
||||
elif self.verbosity == 2:
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level="DEBUG",
|
||||
format=_log_formatter, # type: ignore[arg-type]
|
||||
backtrace=False,
|
||||
diagnose=True,
|
||||
)
|
||||
self.log_level = 10
|
||||
elif self.verbosity == 1:
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level="INFO",
|
||||
format=_log_formatter, # type: ignore[arg-type]
|
||||
backtrace=False,
|
||||
diagnose=True,
|
||||
)
|
||||
self.log_level = 20
|
||||
else:
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
format=_log_formatter, # type: ignore[arg-type]
|
||||
level="WARNING",
|
||||
backtrace=False,
|
||||
diagnose=True,
|
||||
)
|
||||
self.log_level = 30
|
||||
|
||||
if self.log_to_file is True:
|
||||
logger.add(
|
||||
self.log_file,
|
||||
rotation="5 MB",
|
||||
level=self.log_level,
|
||||
backtrace=False,
|
||||
diagnose=True,
|
||||
delay=True,
|
||||
)
|
||||
logger.debug(f"Logging to file: {self.log_file}")
|
||||
|
||||
logger.debug("Logging instantiated")
|
||||
|
||||
def is_trace(self, msg: str | None = None) -> bool:
|
||||
"""Check if the current log level is TRACE.
|
||||
|
||||
Args:
|
||||
msg (optional): Message to print. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True if the current log level is TRACE or lower, False otherwise.
|
||||
"""
|
||||
if self.log_level <= 5:
|
||||
if msg:
|
||||
print(msg)
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_debug(self, msg: str | None = None) -> bool:
|
||||
"""Check if the current log level is DEBUG.
|
||||
|
||||
Args:
|
||||
msg (optional): Message to print. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True if the current log level is DEBUG or lower, False otherwise.
|
||||
"""
|
||||
if self.log_level <= 10:
|
||||
if msg:
|
||||
print(msg)
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_info(self, msg: str | None = None) -> bool:
|
||||
"""Check if the current log level is INFO.
|
||||
|
||||
Args:
|
||||
msg (optional): Message to print. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True if the current log level is INFO or lower, False otherwise.
|
||||
"""
|
||||
if self.log_level <= 20:
|
||||
if msg:
|
||||
print(msg)
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_default(self, msg: str | None = None) -> bool:
|
||||
"""Check if the current log level is default level (SUCCESS or WARNING).
|
||||
|
||||
Args:
|
||||
msg (optional): Message to print. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True if the current log level is default or lower, False otherwise.
|
||||
"""
|
||||
if self.log_level <= 30:
|
||||
if msg:
|
||||
print(msg)
|
||||
return True
|
||||
return False # pragma: no cover
|
||||
169
src/obsidian_metadata/_utils/utilities.py
Normal file
169
src/obsidian_metadata/_utils/utilities.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""Utility functions."""
|
||||
import re
|
||||
from os import name, system
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import typer
|
||||
|
||||
from obsidian_metadata.__version__ import __version__
|
||||
|
||||
|
||||
def dict_values_to_lists_strings(dictionary: dict, strip_null_values: bool = False) -> dict:
|
||||
"""Converts all values in a dictionary to lists of strings.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to convert
|
||||
strip_null (bool): Whether to strip null values
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with all values converted to lists of strings
|
||||
|
||||
{key: sorted(new_dict[key]) for key in sorted(new_dict)}
|
||||
"""
|
||||
new_dict = {}
|
||||
|
||||
if strip_null_values:
|
||||
for key, value in dictionary.items():
|
||||
if isinstance(value, list):
|
||||
new_dict[key] = sorted([str(item) for item in value if item is not None])
|
||||
elif isinstance(value, dict):
|
||||
new_dict[key] = dict_values_to_lists_strings(value) # type: ignore[assignment]
|
||||
elif value is None or value == "None" or value == "":
|
||||
new_dict[key] = []
|
||||
else:
|
||||
new_dict[key] = [str(value)]
|
||||
|
||||
return new_dict
|
||||
|
||||
for key, value in dictionary.items():
|
||||
if isinstance(value, list):
|
||||
new_dict[key] = sorted([str(item) for item in value])
|
||||
elif isinstance(value, dict):
|
||||
new_dict[key] = dict_values_to_lists_strings(value) # type: ignore[assignment]
|
||||
else:
|
||||
new_dict[key] = [str(value)]
|
||||
|
||||
return new_dict
|
||||
|
||||
|
||||
def remove_markdown_sections(
|
||||
text: str,
|
||||
strip_codeblocks: bool = False,
|
||||
strip_inlinecode: bool = False,
|
||||
strip_frontmatter: bool = False,
|
||||
) -> str:
|
||||
"""Strips markdown sections from text.
|
||||
|
||||
Args:
|
||||
text (str): Text to remove code blocks from
|
||||
strip_codeblocks (bool, optional): Strip code blocks. Defaults to False.
|
||||
strip_inlinecode (bool, optional): Strip inline code. Defaults to False.
|
||||
strip_frontmatter (bool, optional): Strip frontmatter. Defaults to False.
|
||||
|
||||
Returns:
|
||||
str: Text without code blocks
|
||||
"""
|
||||
if strip_codeblocks:
|
||||
text = re.sub(r"`{3}.*?`{3}", "", text, flags=re.DOTALL)
|
||||
|
||||
if strip_inlinecode:
|
||||
text = re.sub(r"`.*?`", "", text)
|
||||
|
||||
if strip_frontmatter:
|
||||
text = re.sub(r"^\s*---.*?---", "", text, flags=re.DOTALL)
|
||||
|
||||
return text # noqa: RET504
|
||||
|
||||
|
||||
def version_callback(value: bool) -> None:
|
||||
"""Print version and exit."""
|
||||
if value:
|
||||
print(f"{__package__.split('.')[0]}: v{__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
def vault_validation(path: str) -> bool | str:
|
||||
"""Validates the vault path."""
|
||||
path_to_validate: Path = Path(path).expanduser().resolve()
|
||||
if not path_to_validate.exists():
|
||||
return f"Path does not exist: {path_to_validate}"
|
||||
if not path_to_validate.is_dir():
|
||||
return f"Path is not a directory: {path_to_validate}"
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def docstring_parameter(*sub: Any) -> Any:
|
||||
"""Decorator to replace variables within docstrings.
|
||||
|
||||
Args:
|
||||
sub (Any): Replacement variables
|
||||
|
||||
Usage:
|
||||
@docstring_parameter("foo", "bar")
|
||||
def foo():
|
||||
'''This is a {0} docstring with {1} variables.'''
|
||||
|
||||
"""
|
||||
|
||||
def dec(obj: Any) -> Any:
|
||||
"""Format object."""
|
||||
obj.__doc__ = obj.__doc__.format(*sub)
|
||||
return obj
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
def clean_dictionary(dictionary: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Clean up a dictionary by markdown formatting from keys and values.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to clean
|
||||
|
||||
Returns:
|
||||
dict: Cleaned dictionary
|
||||
"""
|
||||
new_dict = {key.strip(): value for key, value in dictionary.items()}
|
||||
new_dict = {key.strip("*[]#"): value for key, value in new_dict.items()}
|
||||
for key, value in new_dict.items():
|
||||
new_dict[key] = [s.strip("*[]#") for s in value if isinstance(value, list)]
|
||||
|
||||
return new_dict
|
||||
|
||||
|
||||
def clear_screen() -> None:
|
||||
"""Clears the screen."""
|
||||
# for windows
|
||||
_ = system("cls") if name == "nt" else system("clear")
|
||||
|
||||
|
||||
def dict_contains(
|
||||
dictionary: dict[str, list[str]], key: str, value: str = None, is_regex: bool = False
|
||||
) -> bool:
|
||||
"""Checks if a dictionary contains a key.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to check
|
||||
key (str): Key to check for
|
||||
value (str, optional): Value to check for. Defaults to None.
|
||||
is_regex (bool, optional): Whether the key is a regex. Defaults to False.
|
||||
|
||||
Returns:
|
||||
bool: Whether the dictionary contains the key
|
||||
"""
|
||||
if value is None:
|
||||
if is_regex:
|
||||
return any(re.search(key, str(_key)) for _key in dictionary)
|
||||
return key in dictionary
|
||||
|
||||
if is_regex:
|
||||
found_keys = []
|
||||
for _key in dictionary:
|
||||
if re.search(key, str(_key)):
|
||||
found_keys.append(
|
||||
any(re.search(value, _v) for _v in dictionary[_key]),
|
||||
)
|
||||
return any(found_keys)
|
||||
|
||||
return key in dictionary and value in dictionary[key]
|
||||
Reference in New Issue
Block a user