feat: Enhance logger configuration and add export info method to ExportManager
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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("_")]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user