diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29868ee..88a9fbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,39 +6,30 @@ jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.8, 3.9, 3.10, 3.11, 3.12] - django-version: [4.2, 5.0, 5.1] - steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v4 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install poetry tox - poetry install - pip install coverage codecov pytest + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' - - name: Run tests with coverage - run: coverage run -m pytest + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install coverage codecov pytest poetry + pip install -r packages/requirements-dev.txt - - name: Generate coverage report - run: coverage xml + - name: Run tests with coverage + run: pytest --cov=django_logging --cov-report=xml - - name: Run Tox tests - run: tox + - name: Run Tox tests + run: tox - - name: Run pre-commit hooks - run: tox -e pre-commit + - name: Run pre-commit hooks + run: tox -e pre-commit - - name: Upload coverage to Codecov - run: codecov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Upload coverage to Codecov + run: codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1227b2c..9b55220 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,9 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: + - id: check-toml + - id: check-yaml + files: \.yaml$ - id: trailing-whitespace exclude: (migrations/|tests/|docs/).* - id: end-of-file-fixer @@ -15,6 +18,21 @@ repos: - id: check-docstring-first exclude: (migrations/|tests/|docs/).* + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 2.2.1 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 1.3.1 + hooks: + - id: tox-ini-fmt + + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + - repo: https://github.com/pre-commit/mirrors-isort rev: v5.10.1 hooks: @@ -49,6 +67,14 @@ repos: args: ["--in-place", "--recursive", "--blank"] exclude: (migrations/|tests/|docs/).* + - repo: https://github.com/adamchainz/blacken-docs + rev: 1.18.0 + hooks: + - id: blacken-docs + additional_dependencies: + - black==24.4.2 + files: '\.rst$' + - repo: https://github.com/rstcheck/rstcheck rev: "v6.2.4" hooks: @@ -68,18 +94,15 @@ repos: pass_filenames: false always_run: true - - id: pylint - name: pylint - entry: pylint - language: system - types: [python] - require_serial: true - args: - - "-rn" - - "-sn" - - "--rcfile=pyproject.toml" - files: ^django_logging/ - exclude: (migrations/|tests/|docs/).* - -ci: - skip: [pylint] +# - id: pylint +# name: pylint +# entry: pylint +# language: system +# types: [python] +# require_serial: true +# args: +# - "-rn" +# - "-sn" +# - "--rcfile=pyproject.toml" +# files: ^django_logging/ +# exclude: (migrations/|tests/|docs/).* diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 9dbd03e..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,29 +0,0 @@ -# .readthedocs.yml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Set the OS, Python version, and other tools -build: - os: ubuntu-22.04 - tools: - python: "3.8" - -# Build documentation with Sphinx -sphinx: - configuration: docs/conf.py - -# Optionally build your docs in additional formats such as PDF and ePub -# formats: -# - pdf -# - epub - - -# Optional but recommended, declare the Python requirements required -# to build your documentation -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -python: - install: - - requirements: packages/requirements-dev.txt diff --git a/django_logging/filters/log_level_filter.py b/django_logging/filters/log_level_filter.py index b147322..e252616 100644 --- a/django_logging/filters/log_level_filter.py +++ b/django_logging/filters/log_level_filter.py @@ -2,37 +2,37 @@ class LoggingLevelFilter(logging.Filter): - """ - Filters log records based on their logging level. + """Filters log records based on their logging level. + + This filter is used to prevent log records from being written to log + files intended for lower log levels. For example, if we have + separate log files for DEBUG, INFO, WARNING, and ERROR levels, this + filter ensures that a log record with level ERROR is only written to + the ERROR log file, and not to the DEBUG, INFO or WARNING log files. - This filter is used to prevent log records from being written to log files - intended for lower log levels. For example, if we have separate log - files for DEBUG, INFO, WARNING, and ERROR levels, this filter ensures that - a log record with level ERROR is only written to the ERROR log file, and not - to the DEBUG, INFO or WARNING log files. """ def __init__(self, logging_level: int): - """ - Initializes a LoggingLevelFilter instance. + """Initializes a LoggingLevelFilter instance. Args: logging_level: The logging level to filter on (e.g. logging.DEBUG, logging.INFO, etc.). Returns: None + """ super().__init__() self.logging_level = logging_level def filter(self, record: logging.LogRecord) -> bool: - """ - Filters a log record based on its level. + """Filters a log record based on its level. Args: record: The log record to filter. Returns: True if the log record's level matches the specified logging level, False otherwise. + """ return record.levelno == self.logging_level diff --git a/django_logging/management/commands/send_logs.py b/django_logging/management/commands/send_logs.py index fb768be..d051e49 100644 --- a/django_logging/management/commands/send_logs.py +++ b/django_logging/management/commands/send_logs.py @@ -18,35 +18,35 @@ class Command(BaseCommand): - """ - A Django management command that zips the log directory and sends it to + """A Django management command that zips the log directory and sends it to the specified email address. - This command is used to send the log files to a specified email address. - It zips the log directory, creates an email with the zipped file as an attachment, - and sends it to the specified email address. + This command is used to send the log files to a specified email + address. It zips the log directory, creates an email with the zipped + file as an attachment, and sends it to the specified email address. + """ help = "Send log folder to the specified email address" def add_arguments(self, parser: ArgumentParser) -> None: - """ - Add custom command arguments. + """Add custom command arguments. Parameters: parser (ArgumentParser): The argument parser to add arguments to. + """ parser.add_argument( "email", type=str, help="The email address to send the logs to" ) def handle(self, *args: Tuple, **kwargs: Dict) -> None: - """ - The main entry point for the command. + """The main entry point for the command. Parameters: args (tuple): Positional arguments. kwargs (dict): Keyword arguments. + """ email = kwargs["email"] @@ -98,10 +98,12 @@ def handle(self, *args: Tuple, **kwargs: Dict) -> None: logger.info("Temporary zip file cleaned up successfully.") def validate_email_settings(self) -> None: - """ - Check if all required email settings are present in the settings file. + """Check if all required email settings are present in the settings + file. + + Raises ImproperlyConfigured if any of the required email + settings are missing. - Raises ImproperlyConfigured if any of the required email settings are missing. """ errors = check_email_settings(require_admin_email=False) if errors: diff --git a/django_logging/middleware/request_middleware.py b/django_logging/middleware/request_middleware.py index cd5e8ed..acf2293 100644 --- a/django_logging/middleware/request_middleware.py +++ b/django_logging/middleware/request_middleware.py @@ -8,33 +8,33 @@ class RequestLogMiddleware: - """ - Middleware to log information about each incoming request. + """Middleware to log information about each incoming request. + + This middleware logs the request path, the user making the request + (if authenticated), and the user's IP address. - This middleware logs the request path, the user making the request (if authenticated), - and the user's IP address. """ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: - """ - Initializes the RequestLogMiddleware instance. + """Initializes the RequestLogMiddleware instance. Args: get_response: A callable that returns an HttpResponse object. + """ self.get_response = get_response user_model = get_user_model() self.username_field = user_model.USERNAME_FIELD def __call__(self, request: HttpRequest) -> HttpResponse: - """ - Processes an incoming request and logs relevant information. + """Processes an incoming request and logs relevant information. Args: request: The incoming request object. Returns: The response object returned by the view function. + """ # Before view (and later middleware) are called. response = self.get_response(request) @@ -67,9 +67,7 @@ def __call__(self, request: HttpRequest) -> HttpResponse: @staticmethod def get_ip_address(request: HttpRequest) -> str: - """ - Retrieves the client's IP address from the request object. - """ + """Retrieves the client's IP address from the request object.""" ip_address = request.META.get("HTTP_X_FORWARDED_FOR") if ip_address: ip_address = ip_address.split(",")[0] @@ -82,7 +80,5 @@ def get_ip_address(request: HttpRequest) -> str: @staticmethod def get_user_agent(request: HttpRequest) -> str: - """ - Retrieves the client's user agent from the request object. - """ + """Retrieves the client's user agent from the request object.""" return request.META.get("HTTP_USER_AGENT", "Unknown User Agent") diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index 38344e2..05185cc 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -18,12 +18,12 @@ # pylint: disable=too-many-instance-attributes, too-many-arguments class LogConfig: - """ - Configuration class for django_logging. + """Configuration class for django_logging. Attributes: log_levels (List[str]): A list of log levels to be used in logging. log_dir (str): The directory where log files will be stored. + """ def __init__( @@ -39,7 +39,6 @@ def __init__( log_email_notifier_log_levels: NotifierLogLevels, log_email_notifier_log_format: FormatOption, ) -> None: - self.log_levels = log_levels self.log_dir = log_dir self.log_file_formats = self._resolve_file_formats(log_file_formats) @@ -76,9 +75,7 @@ def _resolve_file_formats(self, log_file_formats: LogFileFormatsType) -> Dict: @staticmethod def remove_ansi_escape_sequences(log_message: str) -> str: - """ - Remove ANSI escape sequences from log messages. - """ + """Remove ANSI escape sequences from log messages.""" import re ansi_escape = re.compile(r"(?:\x1B[@-_][0-?]*[ -/]*[@-~])") @@ -104,16 +101,15 @@ def resolve_format(_format: FormatOption, use_colors: bool = False) -> str: class LogManager: - """ - Manages the creation and configuration of log files. + """Manages the creation and configuration of log files. Attributes: log_config (LogConfig): The logging configuration. log_files (Dict[str, str]): A dictionary mapping log levels to file paths. + """ def __init__(self, log_config: LogConfig) -> None: - self.log_config = log_config self.log_files: Dict[str, str] = {} @@ -130,14 +126,14 @@ def create_log_files(self) -> None: self.log_files[log_level] = log_file_path def get_log_file(self, log_level: LogLevel) -> Optional[str]: - """ - Retrieves the file path for a given log level. + """Retrieves the file path for a given log level. Args: log_level (str): The log level to retrieve the file for. Returns: Optional[str]: The file path associated with the log level, or None if not found. + """ return self.log_files.get(log_level) diff --git a/django_logging/utils/context_manager.py b/django_logging/utils/context_manager.py index 278dc16..62cca92 100644 --- a/django_logging/utils/context_manager.py +++ b/django_logging/utils/context_manager.py @@ -8,14 +8,14 @@ @contextmanager def config_setup() -> Iterator[LogManager]: - """ - Context manager to temporarily apply a custom logging configuration. + """Context manager to temporarily apply a custom logging configuration. Raises: ValueError: If 'AUTO_INITIALIZATION_ENABLE' in DJNAGO_LOGGING is set to True. Yields: LogManager: The log manager instance with the custom configuration. + """ if is_auto_initialization_enabled(): raise ValueError( @@ -47,14 +47,14 @@ def _restore_logging_config( original_level: int, original_handlers: list, ) -> None: - """ - Restore the original logging configuration. + """Restore the original logging configuration. Args: logger (Logger): The root logger instance. original_config (Dict[str, Logger | PlaceHolder]): The original logger dictionary. original_level (int): The original root logger level. original_handlers (list): The original root logger handlers. + """ logger.manager.loggerDict.clear() logger.manager.loggerDict.update(original_config) diff --git a/django_logging/utils/get_conf.py b/django_logging/utils/get_conf.py index 754944c..0b962ce 100644 --- a/django_logging/utils/get_conf.py +++ b/django_logging/utils/get_conf.py @@ -8,11 +8,11 @@ # pylint: disable=too-many-locals def get_config(extra_info: bool = False) -> Dict: - """ - Retrieve logging configuration from Django settings. + """Retrieve logging configuration from Django settings. Returns: A Dict containing all necessary configurations for logging. + """ log_settings = getattr(settings, "DJANGO_LOGGING", {}) logging_defaults = DefaultLoggingSettings() @@ -67,11 +67,12 @@ def get_config(extra_info: bool = False) -> Dict: def use_email_notifier_template() -> bool: - """ - Check whether the email notifier should use a template based on Django settings. + """Check whether the email notifier should use a template based on Django + settings. Returns: bool: True if the email notifier should use a template, False otherwise. + """ log_settings = getattr(settings, "DJANGO_LOGGING", {}) defaults = DefaultLoggingSettings() @@ -83,12 +84,13 @@ def use_email_notifier_template() -> bool: def is_auto_initialization_enabled() -> bool: - """ - Check if the AUTO_INITIALIZATION_ENABLE for the logging system is set to True in Django settings. + """Check if the AUTO_INITIALIZATION_ENABLE for the logging system is set to + True in Django settings. Returns: bool: True if AUTO_INITIALIZATION_ENABLE, False otherwise. Defaults to True if not specified. + """ log_settings = getattr(settings, "DJANGO_LOGGING", {}) defaults = DefaultLoggingSettings() @@ -99,12 +101,13 @@ def is_auto_initialization_enabled() -> bool: def is_initialization_message_enabled() -> bool: - """ - Check if the INITIALIZATION_MESSAGE_ENABLE is set to True in Django settings. + """Check if the INITIALIZATION_MESSAGE_ENABLE is set to True in Django + settings. Returns: bool: True if INITIALIZATION_MESSAGE_ENABLE is True, False otherwise. Defaults to True if not specified. + """ log_settings = getattr(settings, "DJANGO_LOGGING", {}) defaults = DefaultLoggingSettings() diff --git a/django_logging/utils/set_conf.py b/django_logging/utils/set_conf.py index 34c6d06..6cffa65 100644 --- a/django_logging/utils/set_conf.py +++ b/django_logging/utils/set_conf.py @@ -32,8 +32,7 @@ def set_config( log_email_notifier_log_levels: NotifierLogLevels, log_email_notifier_log_format: FormatOption, ) -> None: - """ - Sets up the logging configuration based on the provided parameters. + """Sets up the logging configuration based on the provided parameters. This function initializes and configures logging for the application, including file-based logging, console output, and optional email notifications. @@ -74,6 +73,7 @@ def set_config( Notes: - The function performs system checks and logs warnings if configuration issues are detected. - It also logs the current logging setup upon successful initialization. + """ if not is_auto_initialization_enabled(): return diff --git a/packages/requirements-dev.txt b/packages/requirements-dev.txt index 3cddcf5..09d5cc2 100644 --- a/packages/requirements-dev.txt +++ b/packages/requirements-dev.txt @@ -12,8 +12,10 @@ cfgv==3.4.0 ; python_version >= "3.8" and python_version < "4.0" chardet==5.2.0 ; python_version >= "3.8" and python_version < "4.0" charset-normalizer==3.3.2 ; python_version >= "3.8" and python_version < "4.0" click==8.1.7 ; python_version >= "3.8" and python_version < "4.0" +codecov==2.1.13 ; python_version >= "3.8" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.8" and python_version < "4.0" commitizen==3.29.0 ; python_version >= "3.8" and python_version < "4.0" +coverage==7.6.1 ; python_version >= "3.8" and python_version < "4.0" coverage[toml]==7.6.1 ; python_version >= "3.8" and python_version < "4.0" decli==0.6.2 ; python_version >= "3.8" and python_version < "4.0" dill==0.3.8 ; python_version >= "3.8" and python_version < "4.0" diff --git a/poetry.lock b/poetry.lock index e0973bd..30fed7a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -185,13 +185,13 @@ files = [ [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -329,6 +329,21 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "codecov" +version = "2.1.13" +description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, + {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, +] + +[package.dependencies] +coverage = "*" +requests = ">=2.7.9" + [[package]] name = "colorama" version = "0.4.6" @@ -1020,13 +1035,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.2.6" +version = "3.2.7" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, - {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, + {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, + {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, ] [package.dependencies] @@ -1657,4 +1672,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "5920aefc57e8a2dc2b8fa514a65ab174f716e598d99d54e3a02e83245e338374" +content-hash = "0fe240ee8025de3e00f6f3dbe0a27bf6a5c193d78fb22d86d617d16bdb4d0e28" diff --git a/tox.ini b/tox.ini index 0bc06f5..98474b8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,43 +1,52 @@ [tox] -envlist = - py38-django40, py39-django40, py310-django40, py311-django40, py312-django40, - py310-django50, py311-django50, py312-django50, - py310-django51, py311-django51, py312-django51, - - -[gh-actions] -python = - 3.8: py38 - 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 +requires = + tox>=4.2 +env_list = + py312-django40 + py312-django50 + py312-django51 + py311-django40 + py311-django50 + py311-django51 + py310-django40 + py310-django50 + py310-django51 + py39-django40 + py38-django40 [testenv] description = Run Pytest tests with multiple django versions -develop = True deps = - django40: django>=4.2,<5.0 - django50: django>=5.0,<5.1 - django51: django>=5.1,<5.2 pytest - pytest-django pytest-cov -commands = pytest --cov=django_logging --cov-report=html - -setenv = - DJANGO_SETTINGS_MODULE = kernel.settings - + pytest-django + django40: django<5.0,>=4.2 + django50: django<5.1,>=5 + django51: django<5.2,>=5.1 +commands = + pytest --cov=django_logging --cov-report=html +develop = True [testenv:bandit] description = Run security checks skip_install = true -deps = bandit -commands = bandit -r django_logging +deps = + bandit +commands = + bandit -r django_logging [testenv:pre-commit] description = Run pre-commit hooks skip_install = true -deps = pre-commit +deps = + pre-commit commands = pre-commit run --all-files + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312