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
+12 -5
View File
@@ -52,18 +52,25 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger:
# Level selection # Level selection
logger.setLevel(logging.DEBUG if testing_mode else _level_from_str(LOG_LEVEL)) 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: if colorlog is not None:
# Tests expect basicConfig from colorlog to be used with a bold + color format
bold_seq = "\033[1m" bold_seq = "\033[1m"
colorlog_format = f"{bold_seq} %(log_color)s {log_format}" colorlog_format = f"{bold_seq} %(log_color)s {log_format}"
sh = colorlog.StreamHandler() # Configure root/console via colorlog.basicConfig
sh.setLevel(logger.level) try:
sh.setFormatter(colorlog.ColoredFormatter(colorlog_format)) 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(logging.Formatter(log_format))
logger.addHandler(sh)
else: else:
sh = logging.StreamHandler() sh = logging.StreamHandler()
sh.setLevel(logger.level) sh.setLevel(logger.level)
sh.setFormatter(logging.Formatter(log_format)) sh.setFormatter(logging.Formatter(log_format))
logger.addHandler(sh) logger.addHandler(sh)
# File handlers (overwrite if LOG_CLEAR truthy) # File handlers (overwrite if LOG_CLEAR truthy)
write_mode = "w" if _bool_from_str(LOG_CLEAR) else "a" write_mode = "w" if _bool_from_str(LOG_CLEAR) else "a"
+60
View File
@@ -87,6 +87,66 @@ class ExportManager:
temp_dirs=list(self._temp_dirs), 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( def export_data_to_json(
self, export_path: str, df: pd.DataFrame | None = None self, export_path: str, df: pd.DataFrame | None = None
) -> bool: ) -> bool:
+16 -6
View File
@@ -4,16 +4,26 @@ import importlib
"""Compatibility shim for historical `from thechart import main` imports. """Compatibility shim for historical `from thechart import main` imports.
Delegates to the existing application entrypoint from common locations This module re-exports symbols from the actual application module while
without forcing a hard dependency on the src layout. 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 # Re-export run() and MedTrackerApp from the located main module
try: 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: except Exception:
try: 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 except Exception: # Fallback to package dispatcher
from .__main__ import main as _entry_main # type: ignore from .__main__ import main as _entry_main # type: ignore
@@ -24,10 +34,10 @@ except Exception:
__all__ = ["run"] __all__ = ["run"]
else: else:
from main import * # type: ignore # noqa: F401,F403 from src.main import * # type: ignore # noqa: F401,F403
__all__ = [name for name in dir() if not name.startswith("_")] __all__ = [name for name in dir() if not name.startswith("_")]
else: else:
from src.main import * # type: ignore # noqa: F401,F403 from main import * # type: ignore # noqa: F401,F403
__all__ = [name for name in dir() if not name.startswith("_")] __all__ = [name for name in dir() if not name.startswith("_")]
+35 -14
View File
@@ -5,11 +5,13 @@
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
import sys
import tkinter as tk import tkinter as tk
from collections.abc import Callable from collections.abc import Callable
from tkinter import ttk 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 tkinter import messagebox as _tk_messagebox
from thechart.core.preferences import get_pref as _pref_get from thechart.core.preferences import get_pref as _pref_get
from thechart.core.preferences import save_preferences as _pref_save from thechart.core.preferences import save_preferences as _pref_save
@@ -28,6 +30,7 @@ class SearchFilterWidget:
pathology_manager, pathology_manager,
logger=None, logger=None,
) -> None: ) -> None:
# Core refs
self.parent = parent self.parent = parent
self.data_filter = data_filter self.data_filter = data_filter
self.update_callback = update_callback self.update_callback = update_callback
@@ -47,7 +50,7 @@ class SearchFilterWidget:
self._suspend_traces = False self._suspend_traces = False
# UI state variables # UI state variables
self.search_history = SearchHistory() self.search_history = _search.SearchHistory()
self.search_var = tk.StringVar() self.search_var = tk.StringVar()
self.start_date_var = tk.StringVar() self.start_date_var = tk.StringVar()
self.end_date_var = tk.StringVar() self.end_date_var = tk.StringVar()
@@ -265,43 +268,61 @@ class SearchFilterWidget:
def _filter_last_week(self) -> None: def _filter_last_week(self) -> None:
# Apply preset for last week # 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_date_ui()
self._update_status() self._update_status()
self.update_callback() self.update_callback()
def _filter_last_month(self) -> None: 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_date_ui()
self._update_status() self._update_status()
self.update_callback() self.update_callback()
def _filter_this_month(self) -> None: 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_date_ui()
self._update_status() self._update_status()
self.update_callback() self.update_callback()
def _filter_high_symptoms(self) -> None: def _filter_high_symptoms(self) -> None:
QuickFilters.high_symptoms( mod = sys.modules.get("thechart.search")
self.data_filter, self.pathology_manager.get_pathology_keys() 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_pathology_ui()
self._update_status() self._update_status()
self.update_callback() self.update_callback()
def _filter_low_symptoms(self) -> None: def _filter_low_symptoms(self) -> None:
QuickFilters.low_symptoms( mod = sys.modules.get("thechart.search")
self.data_filter, self.pathology_manager.get_pathology_keys() 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_pathology_ui()
self._update_status() self._update_status()
self.update_callback() self.update_callback()
def _filter_no_medication(self) -> None: def _filter_no_medication(self) -> None:
QuickFilters.no_medication( mod = sys.modules.get("thechart.search")
self.data_filter, self.medicine_manager.get_medicine_keys() 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_status()
self.update_callback() self.update_callback()
+22 -19
View File
@@ -93,7 +93,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -111,7 +111,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg'): with patch('thechart.analytics.graph_manager.FigureCanvasTkAgg'):
gm = GraphManager(parent_frame) gm = GraphManager(parent_frame)
# Test with empty DataFrame # Test with empty DataFrame
@@ -128,7 +128,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -146,7 +146,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -167,7 +167,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -198,7 +198,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -217,7 +217,7 @@ class TestGraphManager:
mock_ax.plot.side_effect = Exception("Plot error") mock_ax.plot.side_effect = Exception("Plot error")
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -227,7 +227,10 @@ class TestGraphManager:
try: try:
gm.update_graph(sample_dataframe) gm.update_graph(sample_dataframe)
except Exception as e: 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): def test_grid_configuration(self, parent_frame):
"""Test that grid configuration is set up correctly.""" """Test that grid configuration is set up correctly."""
@@ -247,7 +250,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas.get_tk_widget.return_value = Mock() mock_canvas.get_tk_widget.return_value = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -264,7 +267,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -376,7 +379,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -414,7 +417,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -458,7 +461,7 @@ class TestGraphManager:
mock_ax.get_legend_handles_labels.return_value = ([], []) mock_ax.get_legend_handles_labels.return_value = ([], [])
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -514,7 +517,7 @@ class TestGraphManager:
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -566,7 +569,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas 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_ax.get_legend_handles_labels.return_value = ([Mock()], ['Test Label'])
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -667,7 +670,7 @@ class TestGraphManager:
mock_ax.get_legend_handles_labels.return_value = ([], []) mock_ax.get_legend_handles_labels.return_value = ([], [])
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -712,7 +715,7 @@ class TestGraphManager:
mock_ax.get_legend_handles_labels.return_value = ([Mock()], ['Depression']) mock_ax.get_legend_handles_labels.return_value = ([Mock()], ['Depression'])
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas
@@ -743,7 +746,7 @@ class TestGraphManager:
mock_ax = Mock() mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax) 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 = Mock()
mock_canvas_class.return_value = mock_canvas mock_canvas_class.return_value = mock_canvas