From 6fc57249c10acca3be3476884e9018b604bf3347 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Johnny=20Miller=20=28=E9=94=BA=E4=BF=8A=29?=
 <johnnysviva@outlook.com>
Date: Sat, 20 May 2023 21:17:21 +0800
Subject: [PATCH] feat($loguru): retain 7 days log files

---
 .../configuration/loguru_configuration.py     | 23 +++++++++++++++++--
 .../demo/{async_demo.py => async_usage.py}    |  0
 tests/common/test_profiling.py                | 10 +++++++-
 .../test_loguru_configuration.py              | 21 ++++++++++++++++-
 ...test_async_demo.py => test_async_usage.py} | 11 ++++++++-
 5 files changed, 60 insertions(+), 5 deletions(-)
 rename python_boilerplate/demo/{async_demo.py => async_usage.py} (100%)
 rename tests/demo/{test_async_demo.py => test_async_usage.py} (92%)

diff --git a/python_boilerplate/configuration/loguru_configuration.py b/python_boilerplate/configuration/loguru_configuration.py
index 3c5d3ce..9699b8e 100644
--- a/python_boilerplate/configuration/loguru_configuration.py
+++ b/python_boilerplate/configuration/loguru_configuration.py
@@ -3,6 +3,8 @@
 import sys
 from logging import LogRecord
 
+import arrow
+from arrow import Arrow
 from loguru import logger
 
 from python_boilerplate.common.common_function import get_data_dir, get_module_name
@@ -19,8 +21,9 @@
 # Remove a previously added handler and stop sending logs to its sink.
 logger.remove(handler_id=None)
 # Set up logging for log file
+_logs_directory_path = get_data_dir("logs")
 _log_file = (
-    str(get_data_dir("logs"))
+    str(_logs_directory_path)
     + f"/{get_module_name()}.{platform.node()}."
     + "{time}.log"
 )
@@ -34,7 +37,7 @@
     backtrace=True,
     diagnose=True,
     rotation="00:00",
-    retention="7 Days",
+    retention="7 days",
     compression="gz",
     serialize=False,
 )
@@ -78,8 +81,24 @@ def emit(self, record: LogRecord) -> None:
     logger.info(f"Configured logger[{key}]'s level to {value}")
 
 
+def retain_log_files() -> None:
+    now = arrow.get()
+    dates = {
+        date.format("YYYY-MM-DD")
+        for date in Arrow.range("day", now.shift(days=-7), end=now)
+    }
+    for file_path in _logs_directory_path.glob("*.log"):
+        split_log_file_name = file_path.name.split(".")
+        split_log_file_name.reverse()
+        date_in_file_name = split_log_file_name[1][0:10]
+        if date_in_file_name not in dates:
+            logger.debug(f"Deleting log: {file_path}")
+            file_path.unlink(missing_ok=True)
+
+
 def configure() -> None:
     """
     Configure logging.
     """
+    retain_log_files()
     logger.warning(f"Loguru logging configured, log_level: {log_level}")
diff --git a/python_boilerplate/demo/async_demo.py b/python_boilerplate/demo/async_usage.py
similarity index 100%
rename from python_boilerplate/demo/async_demo.py
rename to python_boilerplate/demo/async_usage.py
diff --git a/tests/common/test_profiling.py b/tests/common/test_profiling.py
index 501e4e6..cbcf9d3 100644
--- a/tests/common/test_profiling.py
+++ b/tests/common/test_profiling.py
@@ -3,15 +3,23 @@
 
 import pytest
 
-from python_boilerplate.common.profiling import async_elapsed_time, elapsed_time
+from python_boilerplate.common.profiling import (
+    async_elapsed_time,
+    cpu_profile,
+    elapsed_time,
+    mem_profile,
+)
 
 
+@mem_profile()
 @elapsed_time(level="WARNING")
 def time_consuming_function() -> str:
     sleep(3)
     return "done execution"
 
 
+@cpu_profile()
+@mem_profile()
 @elapsed_time(level="INFO")
 def time_consuming_function_raising_error() -> None:
     sleep(1)
diff --git a/tests/configuration/test_loguru_configuration.py b/tests/configuration/test_loguru_configuration.py
index 71b2e83..6b38c79 100644
--- a/tests/configuration/test_loguru_configuration.py
+++ b/tests/configuration/test_loguru_configuration.py
@@ -1,6 +1,12 @@
+from pathlib import Path
+
 from loguru import logger
+from pytest_mock import MockerFixture
 
-from python_boilerplate.configuration.loguru_configuration import configure
+from python_boilerplate.configuration.loguru_configuration import (
+    configure,
+    retain_log_files,
+)
 
 
 def test_configure() -> None:
@@ -16,3 +22,16 @@ def test_log_exception() -> None:
         logger.info(f"divided_by_zero = {divided_by_zero}")
     except Exception as ex:
         logger.exception(f"Oops! Exception occurred. {ex}")
+
+
+def test_retain_log_files(mocker: MockerFixture) -> None:
+    path_list = [
+        Path(
+            "python_boilerplate.johnnys-macbook-pro-2017.local.2023-05-01_08-42-03_086829.log"
+        )
+    ]
+    patch = mocker.patch(
+        "pathlib.Path.glob", return_value=(element for element in path_list)
+    )
+    retain_log_files()
+    patch.assert_called()
diff --git a/tests/demo/test_async_demo.py b/tests/demo/test_async_usage.py
similarity index 92%
rename from tests/demo/test_async_demo.py
rename to tests/demo/test_async_usage.py
index 0304a62..ca9884b 100644
--- a/tests/demo/test_async_demo.py
+++ b/tests/demo/test_async_usage.py
@@ -4,10 +4,11 @@
 import pytest
 from loguru import logger
 
-from python_boilerplate.demo.async_demo import (
+from python_boilerplate.demo.async_usage import (
     coroutine1,
     coroutine2,
     coroutine3,
+    main,
     non_coroutine,
 )
 
@@ -92,3 +93,11 @@ async def test_running_coroutine1_2_3_concurrently() -> None:
     assert type(gathered_results[0]) == int
     assert type(gathered_results[1]) == str
     assert type(gathered_results[2]) is ValueError
+
+
+@pytest.mark.asyncio
+async def test_main() -> None:
+    try:
+        await main()
+    except Exception as e:
+        assert False, f"Unexpected exception: {e}"