Skip to content

Commit

Permalink
Merge pull request #37 from MEHRSHAD-MIRSHEKARY/refactor/names_and_types
Browse files Browse the repository at this point in the history
🔨 Refactor/names and types
  • Loading branch information
ARYAN-NIKNEZHAD authored Aug 24, 2024
2 parents ff531ed + 43a80a7 commit a385eb9
Show file tree
Hide file tree
Showing 35 changed files with 2,913 additions and 46 deletions.
35 changes: 21 additions & 14 deletions django_logging/constants/default_settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from typing import cast
from dataclasses import dataclass, field

from django_logging.constants.config_types import (
Expand All @@ -22,23 +23,29 @@ class DefaultLoggingSettings:
auto_initialization_enable: bool = True
initialization_message_enable: bool = True
log_file_formats: LogFileFormatsType = field(
default_factory=lambda: {
"DEBUG": 1,
"INFO": 1,
"WARNING": 1,
"ERROR": 1,
"CRITICAL": 1,
}
default_factory=lambda: cast(
LogFileFormatsType,
{
"DEBUG": 1,
"INFO": 1,
"WARNING": 1,
"ERROR": 1,
"CRITICAL": 1,
},
)
)
log_console_level: LogLevel = "DEBUG"
log_console_format: FormatOption = 1
log_console_colorize: bool = True
log_email_notifier: LogEmailNotifierType = field(
default_factory=lambda: {
"ENABLE": False,
"NOTIFY_ERROR": False,
"NOTIFY_CRITICAL": False,
"LOG_FORMAT": 1,
"USE_TEMPLATE": True,
}
default_factory=lambda: cast(
LogEmailNotifierType,
{
"ENABLE": False,
"NOTIFY_ERROR": False,
"NOTIFY_CRITICAL": False,
"LOG_FORMAT": 1,
"USE_TEMPLATE": True,
},
)
)
2 changes: 1 addition & 1 deletion django_logging/formatters/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .colored_formatter import ColorizedFormatter
from .colored_formatter import ColoredFormatter
6 changes: 3 additions & 3 deletions django_logging/formatters/colored_formatter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import logging
from logging import LogRecord, Formatter
from django_logging.settings.conf import LogConfig
from django_logging.utils.console_colorizer import colorize_log_format


class ColorizedFormatter(logging.Formatter):
def format(self, record):
class ColoredFormatter(Formatter):
def format(self, record: LogRecord) -> str:
original_format = self._style._fmt

# checks that the format does not have any color it's self
Expand Down
18 changes: 8 additions & 10 deletions django_logging/handlers/email_handler.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import logging
from logging import Handler, LogRecord
from typing import Optional

from django.conf import settings
from django.http import HttpRequest
from django.template import engines
from django.utils.timezone import now
from django_logging.utils.log_email_notifier.notifier import send_email_async
from django_logging.utils.get_conf import use_email_notifier_template
from django_logging.middleware import RequestLogMiddleware


class EmailHandler(logging.Handler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.include_html = use_email_notifier_template()

def emit(self, record):
class EmailHandler(Handler):
def emit(self, record: LogRecord) -> None:
try:
request = getattr(record, "request", None)
log_entry = self.format(record)

if self.include_html:
if use_email_notifier_template():
email_body = self.render_template(log_entry, request)
else:
email_body = log_entry
Expand All @@ -31,8 +29,8 @@ def emit(self, record):

@staticmethod
def render_template(
log_entry, request=None, template_path="email_notifier_template.html"
):
log_entry: str, request: Optional[HttpRequest] = None, template_path: str = "email_notifier_template.html"
) -> str:
django_engine = engines["django"]
template = django_engine.get_template(template_path)

Expand Down
8 changes: 5 additions & 3 deletions django_logging/management/commands/send_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import shutil
import tempfile
import logging
from argparse import ArgumentParser
from typing import Dict, Tuple

from django.core.exceptions import ImproperlyConfigured
from django.core.mail import EmailMessage
Expand All @@ -27,7 +29,7 @@ class Command(BaseCommand):

help = "Send log folder to the specified email address"

def add_arguments(self, parser):
def add_arguments(self, parser: ArgumentParser) -> None:
"""
Add custom command arguments.
Expand All @@ -38,7 +40,7 @@ def add_arguments(self, parser):
"email", type=str, help="The email address to send the logs to"
)

def handle(self, *args, **kwargs):
def handle(self, *args: Tuple, **kwargs: Dict) -> None:
"""
The main entry point for the command.
Expand Down Expand Up @@ -95,7 +97,7 @@ def handle(self, *args, **kwargs):
os.remove(zip_path)
logger.info("Temporary zip file cleaned up successfully.")

def validate_email_settings(self):
def validate_email_settings(self) -> None:
"""
Check if all required email settings are present in the settings file.
Expand Down
2 changes: 1 addition & 1 deletion django_logging/settings/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def set_conf(self) -> None:
}
if self.log_config.colorize_console:
formatters["console"].update(
{"()": "django_logging.formatters.ColorizedFormatter"}
{"()": "django_logging.formatters.ColoredFormatter"}
)

formatters["email"] = {
Expand Down
Empty file.
Empty file.
203 changes: 203 additions & 0 deletions django_logging/tests/commands/test_send_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import os
import tempfile
import shutil

from unittest.mock import patch, ANY, Mock

from django.core.exceptions import ImproperlyConfigured
from django.core.management import call_command
from django.test import TestCase


class SendLogsCommandTests(TestCase):
"""
Test suite for the `send_logs` management command in the django_logging package.
"""

@patch(
"django_logging.management.commands.send_logs.Command.validate_email_settings"
)
@patch("django_logging.management.commands.send_logs.shutil.make_archive")
@patch("django_logging.management.commands.send_logs.EmailMessage")
def test_handle_success(
self,
mock_email_message: Mock,
mock_make_archive: Mock,
mock_validate_email_settings: Mock,
) -> None:
"""
Test that the `send_logs` command successfully creates an archive of the logs
and sends an email when executed with valid settings.
Args:
----
mock_email_message: Mock for the `EmailMessage` class used to send the email.
mock_make_archive: Mock for the `shutil.make_archive` function that creates the log archive.
mock_validate_email_settings: Mock for the `validate_email_settings` method in the command.
Asserts:
-------
- The `validate_email_settings` method is called exactly once.
- The `shutil.make_archive` function is called with the correct arguments.
- The `EmailMessage` is instantiated and sent.
"""
temp_log_dir = tempfile.mkdtemp()
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
mock_make_archive.return_value = temp_file.name

with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}):
call_command("send_logs", "[email protected]")

mock_validate_email_settings.assert_called_once()
mock_make_archive.assert_called_once_with(ANY, "zip", temp_log_dir)
mock_email_message.assert_called_once()

shutil.rmtree(temp_log_dir)
(
os.remove(temp_file.name + ".zip")
if os.path.exists(temp_file.name + ".zip")
else None
)

@patch(
"django_logging.management.commands.send_logs.Command.validate_email_settings"
)
@patch("django_logging.management.commands.send_logs.EmailMessage.send")
def test_handle_email_send_failure(
self, mock_email_send: Mock, mock_validate_email_settings: Mock
) -> None:
"""
Test that the `send_logs` command handles email sending failures correctly
and logs an appropriate error message.
Args:
----
mock_email_send: Mock for the `EmailMessage.send` method, simulating a failure.
mock_validate_email_settings: Mock for the `validate_email_settings` method in the command.
Asserts:
-------
- An error message is logged when the email sending fails.
"""
temp_log_dir = tempfile.mkdtemp()
mock_email_send.side_effect = Exception("Email send failed")

with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}):
with self.assertLogs(
"django_logging.management.commands.send_logs", level="ERROR"
) as cm:
call_command("send_logs", "[email protected]")

self.assertIn(
"ERROR:django_logging.management.commands.send_logs:Failed to send logs: Email send failed",
cm.output,
)

shutil.rmtree(temp_log_dir)

@patch(
"django_logging.management.commands.send_logs.Command.validate_email_settings"
)
def test_handle_missing_log_dir(self, mock_validate_email_settings: Mock) -> None:
"""
Test that the `send_logs` command logs an error when the specified log directory does not exist
and skips the email validation step.
Args:
----
mock_validate_email_settings: Mock for the `validate_email_settings` method in the command.
Asserts:
-------
- An error message is logged if the log directory does not exist.
- The `validate_email_settings` method is not called.
"""
non_existent_dir = "/non/existent/directory"
with self.settings(DJANGO_LOGGING={"LOG_DIR": non_existent_dir}):
with self.assertLogs(
"django_logging.management.commands.send_logs", level="ERROR"
) as cm:
call_command("send_logs", "[email protected]")

self.assertIn(
f'ERROR:django_logging.management.commands.send_logs:Log directory "{non_existent_dir}" does not exist.',
cm.output,
)
mock_validate_email_settings.assert_not_called()

@patch(
"django_logging.management.commands.send_logs.check_email_settings",
return_value=None,
)
def test_validate_email_settings_success(
self, mock_check_email_settings: Mock
) -> None:
"""
Test that the `validate_email_settings` method successfully validates the email settings
without raising any exceptions.
Args:
----
mock_check_email_settings: Mock for the `check_email_settings` function, simulating a successful check.
Asserts:
-------
- The `check_email_settings` function is called exactly once.
"""
call_command("send_logs", "[email protected]")
mock_check_email_settings.assert_called_once()

@patch(
"django_logging.management.commands.send_logs.check_email_settings",
return_value="Missing config",
)
def test_validate_email_settings_failure(
self, mock_check_email_settings: Mock
) -> None:
"""
Test that the `validate_email_settings` method raises an `ImproperlyConfigured` exception
when the email settings are invalid.
Args:
----
mock_check_email_settings: Mock for the `check_email_settings` function, simulating a failure.
Asserts:
-------
- An `ImproperlyConfigured` exception is raised when email settings are invalid.
"""
with self.assertRaises(ImproperlyConfigured):
call_command("send_logs", "[email protected]")

@patch(
"django_logging.management.commands.send_logs.Command.validate_email_settings"
)
@patch("django_logging.management.commands.send_logs.shutil.make_archive")
def test_cleanup_on_failure(
self, mock_make_archive: Mock, mock_validate_email_settings: Mock
) -> None:
"""
Test that the `send_logs` command cleans up any partially created files when an error occurs
during the log archiving process.
Args:
----
mock_make_archive: Mock for the `shutil.make_archive` function, simulating a failure.
mock_validate_email_settings: Mock for the `validate_email_settings` method in the command.
Asserts:
-------
- The zip file is not left behind if an error occurs during the archiving process.
"""
temp_log_dir = tempfile.mkdtemp()
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
mock_make_archive.side_effect = Exception("Archive failed")

with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}):
with self.assertRaises(Exception):
call_command("send_logs", "[email protected]")

self.assertFalse(os.path.exists(temp_file.name + ".zip"))
shutil.rmtree(temp_log_dir)
Empty file.
Loading

0 comments on commit a385eb9

Please sign in to comment.