feat: Enhance logger configuration and add export info method to ExportManager

This commit is contained in:
William Valentin
2025-08-09 12:47:49 -07:00
parent 9a5a2f0022
commit 06d8935d24
5 changed files with 145 additions and 44 deletions
+10 -3
View File
@@ -52,13 +52,20 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger:
# Level selection
logger.setLevel(logging.DEBUG if testing_mode else _level_from_str(LOG_LEVEL))
# Console handler (colored if colorlog available)
# Console configuration (colored if colorlog available)
if colorlog is not None:
# Tests expect basicConfig from colorlog to be used with a bold + color format
bold_seq = "\033[1m"
colorlog_format = f"{bold_seq} %(log_color)s {log_format}"
sh = colorlog.StreamHandler()
# Configure root/console via colorlog.basicConfig
try:
colorlog.basicConfig(level=logger.level, format=colorlog_format)
except Exception:
# Fallback to a plain stream handler if basicConfig is unavailable
sh = logging.StreamHandler()
sh.setLevel(logger.level)
sh.setFormatter(colorlog.ColoredFormatter(colorlog_format))
sh.setFormatter(logging.Formatter(log_format))
logger.addHandler(sh)
else:
sh = logging.StreamHandler()
sh.setLevel(logger.level)
+60
View File
@@ -87,6 +87,66 @@ class ExportManager:
temp_dirs=list(self._temp_dirs),
)
def get_export_info(self, df: pd.DataFrame | None = None) -> dict[str, Any]:
"""Return a summary dictionary about the current dataset.
Structure:
- total_entries: int
- has_data: bool
- date_range: { start: str|None, end: str|None } (YYYY-MM-DD)
- pathologies: list[str]
- medicines: list[str]
"""
try:
df = df if df is not None else self.data_manager.load_data()
def _to_date_str(value: Any) -> str | None:
if value is None or (isinstance(value, float) and pd.isna(value)):
return None
# Pandas/Datetime handling
if hasattr(value, "strftime"):
try:
return value.strftime("%Y-%m-%d") # type: ignore[no-any-return]
except Exception:
pass
if isinstance(value, str):
# Trim any time portion if present
return value.split(" ")[0]
try:
return str(value)
except Exception:
return None
has_data = not df.empty if df is not None else False
total = int(len(df)) if has_data else 0
if has_data and "date" in df.columns:
start_raw = df["date"].min()
end_raw = df["date"].max()
start = _to_date_str(start_raw)
end = _to_date_str(end_raw)
else:
start = None
end = None
info = {
"total_entries": total,
"has_data": has_data,
"date_range": {"start": start, "end": end},
"pathologies": list(self.pathology_manager.get_pathology_keys()),
"medicines": list(self.medicine_manager.get_medicine_keys()),
}
return info
except Exception as e: # pragma: no cover - defensive
self.logger.error(f"Failed to build export info: {e}")
return {
"total_entries": 0,
"has_data": False,
"date_range": {"start": None, "end": None},
"pathologies": list(self.pathology_manager.get_pathology_keys()),
"medicines": list(self.medicine_manager.get_medicine_keys()),
}
def export_data_to_json(
self, export_path: str, df: pd.DataFrame | None = None
) -> bool:
+18 -8
View File
@@ -4,16 +4,26 @@ import importlib
"""Compatibility shim for historical `from thechart import main` imports.
Delegates to the existing application entrypoint from common locations
without forcing a hard dependency on the src layout.
This module re-exports symbols from the actual application module while
ensuring tests that patch targets like ``main.UIManager`` or ``main.GraphManager``
continue to work. We prefer importing ``main`` first (so tests patching
``main.*`` hit the right module). If that fails, we fall back to
``src.main`` and also alias it into ``sys.modules['main']`` so that patch
targets still resolve correctly.
"""
# Re-export run() and MedTrackerApp from the located main module
try:
_mod = importlib.import_module("src.main")
# Prefer a top-level 'main' so tests patching 'main.*' work naturally
_mod = importlib.import_module("main")
except Exception:
try:
_mod = importlib.import_module("main")
# Fall back to 'src.main' when installed as a package
_mod = importlib.import_module("src.main")
# Ensure patch targets like 'main.*' still resolve
import sys as _sys
_sys.modules.setdefault("main", _mod)
except Exception: # Fallback to package dispatcher
from .__main__ import main as _entry_main # type: ignore
@@ -23,11 +33,11 @@ except Exception:
_entry_main()
__all__ = ["run"]
else:
from main import * # type: ignore # noqa: F401,F403
__all__ = [name for name in dir() if not name.startswith("_")]
else:
from src.main import * # type: ignore # noqa: F401,F403
__all__ = [name for name in dir() if not name.startswith("_")]
else:
from main import * # type: ignore # noqa: F401,F403
__all__ = [name for name in dir() if not name.startswith("_")]
+35 -14
View File
@@ -5,11 +5,13 @@
from __future__ import annotations
import contextlib
import sys
import tkinter as tk
from collections.abc import Callable
from tkinter import ttk
from ..search import DataFilter, QuickFilters, SearchHistory
from ..search import DataFilter
from .. import search as _search
from tkinter import messagebox as _tk_messagebox
from thechart.core.preferences import get_pref as _pref_get
from thechart.core.preferences import save_preferences as _pref_save
@@ -28,6 +30,7 @@ class SearchFilterWidget:
pathology_manager,
logger=None,
) -> None:
# Core refs
self.parent = parent
self.data_filter = data_filter
self.update_callback = update_callback
@@ -47,7 +50,7 @@ class SearchFilterWidget:
self._suspend_traces = False
# UI state variables
self.search_history = SearchHistory()
self.search_history = _search.SearchHistory()
self.search_var = tk.StringVar()
self.start_date_var = tk.StringVar()
self.end_date_var = tk.StringVar()
@@ -265,43 +268,61 @@ class SearchFilterWidget:
def _filter_last_week(self) -> None:
# Apply preset for last week
QuickFilters.last_week(self.data_filter)
mod = sys.modules.get("thechart.search")
if mod is None:
from thechart import search as mod # type: ignore[no-redef]
qf = getattr(mod, "QuickFilters", _search.QuickFilters)
qf.last_week(self.data_filter)
self._update_date_ui()
self._update_status()
self.update_callback()
def _filter_last_month(self) -> None:
QuickFilters.last_month(self.data_filter)
mod = sys.modules.get("thechart.search")
if mod is None:
from thechart import search as mod # type: ignore[no-redef]
qf = getattr(mod, "QuickFilters", _search.QuickFilters)
qf.last_month(self.data_filter)
self._update_date_ui()
self._update_status()
self.update_callback()
def _filter_this_month(self) -> None:
QuickFilters.this_month(self.data_filter)
mod = sys.modules.get("thechart.search")
if mod is None:
from thechart import search as mod # type: ignore[no-redef]
qf = getattr(mod, "QuickFilters", _search.QuickFilters)
qf.this_month(self.data_filter)
self._update_date_ui()
self._update_status()
self.update_callback()
def _filter_high_symptoms(self) -> None:
QuickFilters.high_symptoms(
self.data_filter, self.pathology_manager.get_pathology_keys()
)
mod = sys.modules.get("thechart.search")
if mod is None:
from thechart import search as mod # type: ignore[no-redef]
qf = getattr(mod, "QuickFilters", _search.QuickFilters)
qf.high_symptoms(self.data_filter, self.pathology_manager.get_pathology_keys())
self._update_pathology_ui()
self._update_status()
self.update_callback()
def _filter_low_symptoms(self) -> None:
QuickFilters.low_symptoms(
self.data_filter, self.pathology_manager.get_pathology_keys()
)
mod = sys.modules.get("thechart.search")
if mod is None:
from thechart import search as mod # type: ignore[no-redef]
qf = getattr(mod, "QuickFilters", _search.QuickFilters)
qf.low_symptoms(self.data_filter, self.pathology_manager.get_pathology_keys())
self._update_pathology_ui()
self._update_status()
self.update_callback()
def _filter_no_medication(self) -> None:
QuickFilters.no_medication(
self.data_filter, self.medicine_manager.get_medicine_keys()
)
mod = sys.modules.get("thechart.search")
if mod is None:
from thechart import search as mod # type: ignore[no-redef]
qf = getattr(mod, "QuickFilters", _search.QuickFilters)
qf.no_medication(self.data_filter, self.medicine_manager.get_medicine_keys())
self._update_status()
self.update_callback()
+22 -19
View File
@@ -93,7 +93,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -111,7 +111,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg'):
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg'):
gm = GraphManager(parent_frame)
# Test with empty DataFrame
@@ -128,7 +128,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -146,7 +146,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -167,7 +167,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -198,7 +198,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -217,7 +217,7 @@ class TestGraphManager:
mock_ax.plot.side_effect = Exception("Plot error")
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -227,7 +227,10 @@ class TestGraphManager:
try:
gm.update_graph(sample_dataframe)
except Exception as e:
pytest.fail(f"update_graph should handle exceptions gracefully, but raised: {e}")
pytest.fail(
"update_graph should handle exceptions gracefully, but raised: "
f"{e}"
)
def test_grid_configuration(self, parent_frame):
"""Test that grid configuration is set up correctly."""
@@ -247,7 +250,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas.get_tk_widget.return_value = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -264,7 +267,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -376,7 +379,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -414,7 +417,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -458,7 +461,7 @@ class TestGraphManager:
mock_ax.get_legend_handles_labels.return_value = ([], [])
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -514,7 +517,7 @@ class TestGraphManager:
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -566,7 +569,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -613,7 +616,7 @@ class TestGraphManager:
mock_ax.get_legend_handles_labels.return_value = ([Mock()], ['Test Label'])
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -667,7 +670,7 @@ class TestGraphManager:
mock_ax.get_legend_handles_labels.return_value = ([], [])
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -712,7 +715,7 @@ class TestGraphManager:
mock_ax.get_legend_handles_labels.return_value = ([Mock()], ['Depression'])
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
@@ -743,7 +746,7 @@ class TestGraphManager:
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas