Skip to content

Commit

Permalink
Refactored the code
Browse files Browse the repository at this point in the history
  • Loading branch information
sexfrance committed Nov 10, 2024
1 parent aab3ecd commit fb85664
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 127 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ ENV/
# Backup files
*.bak
*.swp

# Log
.log
logs/
51 changes: 38 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# LogMagix

**LogMagix** is a custom Python logging package that offers styled, colorful log messages for various logging levels such as success, warning, failure, and more. It also features an animated loader class for providing a visual indicator during long-running operations in the terminal. Additionally, the new `Home` class offers customizable ASCII art displays for greeting users with special messages, branding, or system information.
**LogMagix** is a custom Python logging package that offers styled, colorful log messages for various logging levels such as success, warning, failure, and more. It also features an animated loader class for providing a visual indicator during long-running operations in the terminal. Additionally, the `Home` class offers customizable ASCII art displays for greeting users with special messages, branding, or system information.

## 🔥 Features

- Log messages for success, warning, failure, and informational levels.
- Customize message colors using ANSI escape sequences.
- Log messages for various levels: success, warning, failure, debug, critical, info, and more.
- Color customization using ANSI escape sequences.
- Time-stamped log messages for better tracking.
- Built-in animated loader for visually appealing loading spinners.
- Log saving to file with optional log file paths.
- Customizable log and loader prefixes.
- ASCII art display for personalized greetings, system info, and branding.
- Simple and flexible API with multiple ways to use the `Loader` class.
Expand Down Expand Up @@ -57,13 +58,42 @@ log.info("Informational log message")
# Debug message
log.debug("Debugging log message")

# Customizable message
log.message("Dad", f"How are you? I'm gonna come soon!", start="", end="") # Start and end optional
# Critical message (also terminates the program with optional exit code)
log.critical("Critical failure encountered", exit_code=1)
```

### Log Levels

LogMagix provides several logging levels to help categorize the severity and type of log messages. You can configure the minimum log level to display based on your requirements:

- `DEBUG`: For detailed debug messages.
- `INFO`: For informational messages.
- `WARNING`: For warning messages.
- `SUCCESS`: For successful operations.
- `FAILURE`: For non-critical errors.
- `CRITICAL`: For critical errors; may terminate the program.

You can set the minimum logging level on initialization by passing a `LogLevel` value to the `Logger` constructor. For example:

# Question input
log.question("This is an input question!")
```python
from logmagix import Logger, LogLevel

log = Logger(level=LogLevel.WARNING)
```

With this setting, only `WARNING`, `SUCCESS`, `FAILURE`, and `CRITICAL` messages will display.

### Log File Saving

You can specify a log file path to save logs to a file for further review or debugging. The logger will automatically strip ANSI color codes from messages saved to the log file for readability. Log files are appended with each new logging session.

```python
log = Logger(log_file="logs/app.log")
log.success("This message will also be saved to app.log")
```

To view logs saved to the file, open the specified path and review the recorded entries, which include timestamped log messages for tracking system state over time.

### Loading Animation

The `Loader` class can be used in two ways:
Expand All @@ -89,7 +119,7 @@ loader.stop()

### Custom Log and Loader Prefix

Both the `Logger` and `Loader` classes allow for customizing the prefix that is shown before each message:
Both the `Logger` and `Loader` classes allow for customizing the prefix shown before each message:

#### Logger Prefix:

Expand Down Expand Up @@ -146,9 +176,6 @@ log.warning("Watch out, something might happen!")
log.failure("Critical error occurred!")
log.info("System is working properly")
log.debug(f"The system uuid is {uuid.getnode()}")
log.message("Dad", f"How are you? I'm gonna come soon!", start=start_time, end=time.time())
log.question("How old are you? ")


# Use loader with custom prefix and context manager
with Loader(prefix="custom/loader/prefix", desc="Processing data..."):
Expand All @@ -159,7 +186,6 @@ loader = Loader(prefix="custom/loader/prefix", desc="Saving files...", end="Done
time.sleep(2) # Simulate task
loader.stop()


home_screen = Home(
text="LogMagix",
align="center",
Expand All @@ -170,7 +196,6 @@ home_screen = Home(

home_screen.display()


log.success("Processing completed!")
```

Expand Down
196 changes: 102 additions & 94 deletions logmagix/logger.py
Original file line number Diff line number Diff line change
@@ -1,135 +1,151 @@
import datetime
import time
from threading import Thread, Lock
from threading import Thread
from itertools import cycle
from enum import Enum
from typing import Optional, List
import logging.handlers
from colorama import Fore, Style
import os
from sys import exit
import getpass
from .font import ascii_art
from pystyle import Write, Colors
from pystyle import Write, System, Colors
from enum import Enum
import re

class LogLevel(Enum):
"""Enumeration of available logging levels."""
DEBUG = 0
INFO = 1
WARNING = 2
SUCCESS = 3
ERROR = 4
CRITICAL = 5
DEBUG = 1
INFO = 2
WARNING = 3
SUCCESS = 4
FAILURE = 5
CRITICAL = 6

class Logger:
def __init__(self, prefix: str | None = "discord.cyberious.xyz"):
def __init__(self, prefix: str | None = "discord.cyberious.xyz", level: LogLevel = LogLevel.DEBUG, log_file: str | None = None):
self.WHITE = "\u001b[37m"
self.MAGENTA = "\033[38;5;97m"
self.MAGENTAA = "\033[38;2;157;38;255m"
self.BRIGHT_MAGENTA = "\033[38;2;157;38;255m"
self.LIGHT_CORAL = "\033[38;5;210m"
self.RED = "\033[38;5;196m"
self.GREEN = "\033[38;5;40m"
self.YELLOW = "\033[38;5;220m"
self.BLUE = "\033[38;5;21m"
self.PINK = "\033[38;5;176m"
self.CYAN = "\033[96m"
self.prefix = f"{self.PINK}[{self.MAGENTA}{prefix}{self.PINK}] " if prefix else f"{self.PINK}"
self.level = level
self.log_file = log_file
if log_file:
# Create log directory if it doesn't exist
os.makedirs(os.path.dirname(log_file), exist_ok=True)
# Add initial log entry
self._write_to_log(f"=== Logging started at {datetime.datetime.now()} ===\n")

def _write_to_log(self, message: str) -> None:
if self.log_file:
try:
with open(self.log_file, 'a', encoding='utf-8') as f:
# Strip ANSI color codes for file logging
clean_message = self._strip_ansi(message)
f.write(clean_message + '\n')
except Exception as e:
print(f"Error writing to log file: {e}")

def _strip_ansi(self, text: str) -> str:
"""Remove ANSI escape sequences from text"""
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
return ansi_escape.sub('', text)

def get_time(self) -> str:
return datetime.datetime.now().strftime("%H:%M:%S")

def message3(self, level: str, message: str, start: int = None, end: int = None) -> str:
time = self.get_time()
return f"{self.prefix}[{self.MAGENTAA}{time}{self.PINK}] {self.PINK}[{self.CYAN}{level}{self.PINK}] -> {self.CYAN}{message}{Fore.RESET}"
return f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}] {self.PINK}[{self.CYAN}{level}{self.PINK}] -> {self.CYAN}{message}{Fore.RESET}"

def _should_log(self, message_level: LogLevel) -> bool:
return message_level.value >= self.level.value

def success(self, message: str, start: int = None, end: int = None, level: str = "Success") -> None:
if self._should_log(LogLevel.SUCCESS):
self._log(LogLevel.SUCCESS, message, start, end)
log_message = self.message3(f"{self.GREEN}{level}", f"{self.GREEN}{message}", start, end)
print(log_message)
self._write_to_log(log_message)

def failure(self, message: str, start: int = None, end: int = None, level: str = "Failure") -> None:
if self._should_log(LogLevel.ERROR):
self._log(LogLevel.ERROR, message, start, end)

def critical(self, message: str, start: int = None, end: int = None, level: str = "CRITICAL", exit_code: int = 1) -> None:
input(self.message3(f"{self.LIGHT_CORAL}{level}", f"{self.LIGHT_CORAL}{message}", start, end))
self._log_to_file(LogLevel.CRITICAL, message)
exit(exit_code)
if self._should_log(LogLevel.FAILURE):
log_message = self.message3(f"{self.RED}{level}", f"{self.RED}{message}", start, end)
print(log_message)
self._write_to_log(log_message)

def warning(self, message: str, start: int = None, end: int = None, level: str = "Warning") -> None:
if self._should_log(LogLevel.WARNING):
self._log(LogLevel.WARNING, message, start, end)

def info(self, message: str, start: int = None, end: int = None) -> None:
if self._should_log(LogLevel.INFO):
self._log(LogLevel.INFO, message, start, end)

def debug(self, message: str, start: int = None, end: int = None) -> None:
if self._should_log(LogLevel.DEBUG):
self._log(LogLevel.DEBUG, message, start, end)

def _log_to_file(self, level: LogLevel, message: str) -> None:
"""Helper method to handle file logging"""
if not self.file_handler:
return

if level.value >= self._min_level.value:
stripped_message = self._strip_colors(message)
self.file_handler.emit(
logging.LogRecord(
name="logger",
level=level.value * 10,
pathname="",
lineno=0,
msg=stripped_message,
args=(),
exc_info=None
)
)

def get_time(self) -> str:
return datetime.datetime.now().strftime("%H:%M:%S")
log_message = self.message3(f"{self.YELLOW}{level}", f"{self.YELLOW}{message}", start, end)
print(log_message)
self._write_to_log(log_message)

def message(self, level: str, message: str, start: int = None, end: int = None) -> None:
time = self.get_time()
timer = f" {self.MAGENTA_BRIGHT}In{self.WHITE} -> {self.MAGENTA_BRIGHT}{str(end - start)[:5]} Seconds {Fore.RESET}" if start and end else ""
print(f"{self.prefix}[{self.MAGENTA_BRIGHT}{time}{self.PINK}] [{self.CYAN}{level}{self.PINK}] -> [{self.CYAN}{message}{self.PINK}]{timer}")
timer = f" {self.BRIGHT_MAGENTA}In{self.WHITE} -> {self.BRIGHT_MAGENTA}{str(end - start)[:5]} Seconds {Fore.RESET}" if start and end else ""
log_message = f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}] [{self.CYAN}{level}{self.PINK}] -> [{self.CYAN}{message}{self.PINK}]{timer}"
print(log_message)
self._write_to_log(log_message)

def message2(self, level: str, message: str, start: int = None, end: int = None) -> None:
time = self.get_time()
if start is not None and end is not None:
print(f"{self.prefix}[{self.MAGENTA_BRIGHT}{time}{self.PINK}] {self.PINK}[{self.CYAN}{level}{self.PINK}] -> {Fore.RESET} {self.CYAN}{message}{Fore.RESET} [{Fore.CYAN}{end - start}s{Style.RESET_ALL}]", end="\r")
print(f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}] {self.PINK}[{self.CYAN}{level}{self.PINK}] -> {Fore.RESET} {self.CYAN}{message}{Fore.RESET} [{Fore.CYAN}{end - start}s{Style.RESET_ALL}]", end="\r")
else:
print(f"{self.prefix}[{self.MAGENTA_BRIGHT}{time}{self.PINK}] {self.PINK}[{Fore.BLUE}{level}{self.PINK}] -> {Fore.RESET} {self.CYAN}{message}{Fore.RESET}", end="\r")

def message3(self, level: str, message: str, start: int = None, end: int = None) -> str:
time = self.get_time()
return f"{self.prefix}[{self.MAGENTA_BRIGHT}{time}{self.PINK}] {self.PINK}[{self.CYAN}{level}{self.PINK}] -> {self.CYAN}{message}{Fore.RESET}"
print(f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}] {self.PINK}[{Fore.BLUE}{level}{self.PINK}] -> {Fore.RESET} {self.CYAN}{message}{Fore.RESET}", end="\r")

def question(self, message: str, start: int = None, end: int = None) -> str:
def question(self, message: str, start: int = None, end: int = None) -> None:
time = self.get_time()
i = input(f"{self.prefix}[{self.MAGENTA_BRIGHT}{time}{self.PINK}]{Fore.RESET} {self.PINK}[{Fore.BLUE}?{self.PINK}] -> {Fore.RESET} {self.CYAN}{message}{Fore.RESET}")
question_message = f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}]{Fore.RESET} {self.PINK}[{Fore.BLUE}?{self.PINK}] -> {Fore.RESET} {self.CYAN}{message}{Fore.RESET}"
print(question_message, end='')
i = input()

# Log both the question and answer
if self.log_file:
self._write_to_log(f"{question_message}")
self._write_to_log(f"User Answer: {i}")

return i

log = Logger()
def critical(self, message: str, start: int = None, end: int = None, level: str = "CRITICAL", exit_code: int = 1) -> None:
if self._should_log(LogLevel.CRITICAL):
time = self.get_time()
log_message = f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}]{Fore.RESET} {self.PINK}[{self.LIGHT_CORAL}{level}{self.PINK}] -> {self.LIGHT_CORAL}{message}{Fore.RESET}"
print(log_message)
self._write_to_log(log_message)
input()
self._write_to_log(f"=== Program terminated with exit code {exit_code} at {datetime.datetime.now()} ===")
exit(exit_code)

class Loader:
"""Animated loading indicator with customizable appearance."""
def info(self, message: str, start: int = None, end: int = None) -> None:
if self._should_log(LogLevel.INFO):
time = self.get_time()
log_message = f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}]{Fore.RESET} {self.PINK}[{Fore.BLUE}!{self.PINK}] -> {Fore.RESET} {self.CYAN}{message}{Fore.RESET}"
print(log_message)
self._write_to_log(log_message)

def debug(self, message: str, start: int = None, end: int = None) -> None:
if self._should_log(LogLevel.DEBUG):
time = self.get_time()
log_message = f"{self.prefix}[{self.BRIGHT_MAGENTA}{time}{self.PINK}]{Fore.RESET} {self.PINK}[{Fore.YELLOW}DEBUG{self.PINK}] -> {Fore.RESET} {self.GREEN}{message}{Fore.RESET}"
print(log_message)
self._write_to_log(log_message)

SPINNER_CHARS = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"]
log = Logger()

def __init__(
self,
prefix: Optional[str] = "discord.cyberious.xyz",
desc: str = "Loading...",
end: str = "\r",
timeout: float = 0.1
) -> None:
class Loader:
def __init__(self, prefix: str = "discord.cyberious.xyz", desc="Loading...", end="\r", timeout=0.1):
self.desc = desc
self.end = end
self.prefix = prefix
self.timeout = timeout
self.time = datetime.datetime.now().strftime("%H:%M:%S")

self.start_time = datetime.datetime.now()

self._thread = Thread(target=self._animate, daemon=True)
self.steps = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"]
self.done = False

def __enter__(self):
Expand All @@ -144,33 +160,23 @@ def start(self):
return self

def _animate(self):
for c in cycle(self.SPINNER_CHARS):
for c in cycle(self.steps):
if self.done:
break
prefix_str = f"[{log.MAGENTA}{self.prefix}{log.PINK}] " if self.prefix is not None else ""
print(f"\r{log.PINK}{prefix_str}[{log.MAGENTA_BRIGHT}{self.time}{log.PINK}] [{log.GREEN}{self.desc}{log.PINK}]{Fore.RESET} {c}", flush=True, end="")
loader_message = f"\r{log.PINK}[{log.MAGENTA}{self.prefix}{log.PINK}] [{log.BRIGHT_MAGENTA}{self.time}{log.PINK}] [{log.GREEN}{self.desc}{log.PINK}]{Fore.RESET} {c}"
print(loader_message, flush=True, end="")
time.sleep(self.timeout)

def stop(self):
self.done = True
if self.end != "\r":
prefix_str = f"[{log.MAGENTA}{self.prefix}{log.PINK}] " if self.prefix is not None else ""
print(f"\n{log.PINK}{prefix_str}[{log.MAGENTA_BRIGHT}{self.time}{log.PINK}] {log.GREEN} {self.end} {Fore.RESET}", flush=True)
end_message = f"\n{log.PINK}[{log.MAGENTA}{self.prefix}{log.PINK}] [{log.BRIGHT_MAGENTA}{self.time}{log.PINK}] {log.GREEN} {self.end} {Fore.RESET}"
print(end_message, flush=True)
else:
print(self.end, flush=True)

class Home:
"""ASCII art text display with customizable formatting and layout."""

def __init__(
self,
text: str,
align: str = "left",
adinfo1: Optional[str] = None,
adinfo2: Optional[str] = None,
credits: Optional[str] = None,
clear: bool = True
) -> None:
def __init__(self, text, align="left", adinfo1=None, adinfo2=None, credits=None, clear=True):
self.text = text
self.align = align
self.adinfo1 = adinfo1
Expand Down Expand Up @@ -254,6 +260,8 @@ def _construct_adinfo_text(self, ascii_art_width):
remaining_space = ascii_art_width - total_adinfo_length
if remaining_space > 0:
padding_between = ' ' * (remaining_space // 3)
return self.adinfo1 + padding_between + self.adinfo2
else:
return self.adinfo1 + ' ' + self.adinfo2
return self.adinfo1 or self.adinfo2 or ''

Expand All @@ -275,4 +283,4 @@ def _display_welcome(self, terminal_width, block_width):
Write.Print(f"{tilde_line_aligned}\n", Colors.red_to_blue, interval=0.000)

equals_line = "═" * terminal_width
Write.Print(f"{equals_line}\n", Colors.red_to_blue, interval=0.000)
Write.Print(f"{equals_line}\n", Colors.red_to_blue, interval=0.000)
Loading

0 comments on commit fb85664

Please sign in to comment.