"""DataTools — Data Cleaning Mastery Suite. Launch: streamlit run src/gui/app.py This module is the navigation manager for the full GUI: it registers every tool page with ``st.navigation`` so the sidebar can render section headers ("Data Review", "Data Cleaners", "Transformations", "Automations") instead of the flat numbered list Streamlit's auto-page-discovery would produce. The Home page itself is registered as a callable defined below so the entry script remains the single file users invoke. """ from __future__ import annotations import sys from pathlib import Path import streamlit as st # Ensure project root is on sys.path so `src.core` imports work _project_root = Path(__file__).resolve().parent.parent.parent if str(_project_root) not in sys.path: sys.path.insert(0, str(_project_root)) # Quiet the loguru stderr sink. The CLIs (``src/cli*.py``) reconfigure # it per-script to ``WARNING``; the GUI's ``python -m src.gui`` # entrypoint never did, so internal ``logger.debug`` / # ``logger.warning`` breadcrumbs bled into the terminal where users # launched the app from. Drop the default ``DEBUG``-level stderr sink # and add a clean ``ERROR``-only one so only genuinely actionable # problems surface — diagnostic warnings still land in the audit log # via its own sink. Runs once, before any module-level ``logger`` use # in the GUI tree. from loguru import logger as _logger # noqa: E402 _logger.remove() _logger.add(sys.stderr, level="ERROR", format="{level: <8} | {message}") # --------------------------------------------------------------------------- # Home page (rendered when the user selects the default nav entry) # --------------------------------------------------------------------------- # # The renderer lives in ``src/gui/_home.py`` so the ``back_to_home_link`` # helper on tool pages can ``import _home_page`` and pass it into # ``st.switch_page`` without re-executing this entry script's # navigation setup (which would crash because tool pages have a # different "main script" context and the relative ``pages/…`` paths # below would no longer resolve). from src.gui._home import _home_page # noqa: E402 # --------------------------------------------------------------------------- # Navigation registration # --------------------------------------------------------------------------- # # ``st.navigation`` overrides Streamlit's auto-discovery of the # ``pages/`` directory, so every page we want in the sidebar must be # listed here. The dict key becomes the section header rendered above # the entries; an empty-string key suppresses the header so the Home # entry sits at the top without a label above it. from src.gui.tools_registry import TOOLS, section_label, tool_name # noqa: E402 from src.i18n import t as _t # noqa: E402 def _page_for(tool_id: str, *, page_slug: str, icon: str, title: str) -> "st.Page": return st.Page( f"pages/{page_slug}.py", title=title, icon=icon, url_path=tool_id, ) def _build_navigation() -> dict[str, list]: by_section: dict[str, list] = { "analysis": [], "cleaners": [], "transformations": [], "automations": [], } # Resolve the tool name through ``tool_name`` (i18n lookup) instead # of using the registry's English ``tool.name`` field, otherwise the # sidebar stays in English when the user switches to Spanish. for tool in TOOLS: by_section[tool.section].append( _page_for( tool.tool_id, page_slug=tool.page_slug, icon=tool.icon, title=tool_name(tool.tool_id), ) ) # Home is now surfaced under the new "Analysis" section as # "File Analysis" — the home page's content (importing files, # running the analyzer, browsing findings) is itself a data-analysis # workflow, so grouping it next to Reconcile keeps the sidebar's # mental model coherent. ``default=True`` still points at this # page so first-visit lands here regardless of section placement. home = st.Page( _home_page, title=_t("nav.file_analysis_title") or "File Analysis", icon=":material/insert_chart_outlined:", default=True, url_path="home", ) # Activate + Close are both reached from the sticky-footer Help # popover (Activate / Manage-license link and Close button). They # are registered with ``st.navigation`` so ``/activate`` and # ``/close`` remain URL-routable — pages not listed in the dict # 404 even by direct hit — but their sidebar entries are hidden # via CSS in ``hide_streamlit_chrome``. We group them under a # single section header that is also hidden by that same CSS rule # so no orphan "Account" / "Close" label is left in the sidebar. activate = st.Page( "pages/_Activate.py", title=_t("nav.activate_title") or "Activate", icon=":material/key:", url_path="activate", ) logs = st.Page( "pages/_Logs.py", title="Logs", icon=":material/description:", url_path="logs", ) close = st.Page( "pages/99_Close.py", title=_t("nav.close_title") or "Close", icon=":material/close:", url_path="close", ) # Activate / Logs / Close stay in the unlabeled section (key ``""``) # so the CSS in ``hide_streamlit_chrome`` keeps hiding them by # ``href``. Home moved out of that bucket into "Analysis" — the # unlabeled section now contains ONLY hidden pages, so no orphan # entry appears above the "Analysis" header in the sidebar. return { "": [activate, logs, close], section_label("analysis"): [home, *by_section["analysis"]], section_label("cleaners"): by_section["cleaners"], section_label("transformations"): by_section["transformations"], section_label("automations"): by_section["automations"], } pg = st.navigation(_build_navigation()) pg.run()