Files
datatools-dev/src/gui/tools_registry.py
Michael 1895074b8f test+fix(gui): retire the now-empty "analysis" nav section
The journey-level nav restructure moved Home to a standalone "Start
here" entry and Reconcile into the "Finance" group, leaving the
"analysis" section with zero tools. Two registry tests encoded the old
layout and failed:
- test_every_section_has_at_least_one_tool[analysis] (empty section)
- test_reconciler_present (asserted section == "analysis")

Drop "analysis" from the Section literal, SECTION_LABELS, and app.py's
by_section bucket — it's genuinely dead now (home isn't a registry Tool).
Update the presence tests to assert Reconcile + PDF to CSV live in
"finance". The section-invariant tests (every section non-empty, has a
label, no orphan labels) are preserved and pass.

Full suite: 2441 passed, 91 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:11:02 +00:00

251 lines
8.2 KiB
Python

"""Per-tool manifest registry.
Single source of truth for what tools exist, their display strings, the
left-nav section they belong to, and the tier (which controls whether a
tool ships in a given build SKU). The home-page sidebar consumes this
list; future per-tool packaging will filter it via the ``tier`` field.
Adding a tool: append one ``Tool`` entry. Page filenames must match the
``page_slug`` so Streamlit's automatic page discovery picks them up.
Selling subsets: builds can filter ``TOOLS`` by tier or tool_id at
import time — no other code changes required, since pages key off
``tool_id`` for findings badges and the home grid renders whatever's in
the filtered list.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
Tier = Literal["core", "pro", "enterprise"]
Status = Literal["Ready", "Coming Soon"]
# Sidebar grouping. Tools are bucketed by what the user is trying to
# accomplish rather than by implementation detail.
Section = Literal[
"cleaners", "transformations", "automations",
"finance", "coming_soon",
]
@dataclass(frozen=True)
class Tool:
"""One tool's manifest entry."""
tool_id: str # Stable identifier matching the analyzer's tool field.
icon: str # Single-glyph icon for the home grid card.
name: str # Display name (sidebar + card title).
description: str # One-sentence card body.
page_slug: str # Streamlit page filename without ".py" (e.g. "1_Deduplicator").
status: Status # "Ready" or "Coming Soon" — drives the card badge color.
section: Section # Sidebar group this tool belongs to.
tier: Tier = "core" # Build-time gating hook; every tool is "core" today.
# Order in this list IS the order shown in each sidebar section, so
# arranging it carefully matters. Within "cleaners" the order is the
# recommended PIPELINE order (Clean Text → Standardize → Fix Missing →
# Find Duplicates) so a user running tools by hand follows the sequence
# the orchestrator would. "Coming Soon" tools are grouped at the end in
# their own section so they never interleave with working tools, and the
# finance-oriented tools (Reconcile, PDF to CSV) live in their own group
# (see DECISIONS.md 2026-06-08).
TOOLS: list[Tool] = [
Tool(
tool_id="02_text_cleaner",
icon=":material/text_format:",
name="Clean Text",
description=(
"Trim extra spaces and strip out odd characters that copy-paste "
"leaves behind."
),
page_slug="2_Text_Cleaner",
status="Ready",
section="cleaners",
),
Tool(
tool_id="03_format_standardizer",
icon=":material/format_list_bulleted:",
name="Standardize Formats",
description=(
"Make dates, phone numbers, currency, names, and addresses look "
"the same throughout."
),
page_slug="3_Format_Standardizer",
status="Ready",
section="cleaners",
),
Tool(
tool_id="04_missing_handler",
icon=":material/help_outline:",
name="Fix Missing Values",
description=(
"Find blank cells (even ones written as 'N/A' or '?') and fill "
"them in or remove them."
),
page_slug="4_Missing_Values",
status="Ready",
section="cleaners",
),
Tool(
tool_id="01_deduplicator",
icon=":material/search:",
name="Find Duplicates",
description=(
"Find rows that repeat — exact and similar — and remove the extras."
),
page_slug="1_Deduplicator",
status="Ready",
section="cleaners",
),
Tool(
tool_id="05_column_mapper",
icon=":material/view_column:",
name="Map Columns",
description=(
"Rename columns, change their order, and set each one as text, "
"number, or date."
),
page_slug="5_Column_Mapper",
status="Ready",
section="transformations",
),
Tool(
tool_id="09_pipeline_runner",
icon=":material/auto_awesome:",
name="Automated Workflows",
description=(
"Run several tools in a row — save the steps once, reuse them "
"anytime."
),
page_slug="9_Pipeline_Runner",
status="Ready",
section="automations",
),
Tool(
tool_id="11_reconciler",
icon=":material/compare_arrows:",
name="Reconcile Two Files",
description=(
"Compare two lists of transactions (e.g. bank vs. ledger) and "
"flag what doesn't match."
),
page_slug="11_Reconciler",
status="Ready",
section="finance",
),
Tool(
tool_id="10_pdf_extractor",
icon=":material/picture_as_pdf:",
name="PDF to CSV",
description=(
"Pull transactions out of bank-statement PDFs into a clean CSV file."
),
page_slug="10_PDF_Extractor",
status="Ready",
section="finance",
),
Tool(
tool_id="06_outlier_detector",
icon=":material/insights:",
name="Find Unusual Values",
description=(
"Spot values that look wrong — way too high, way too low, or "
"breaking your rules."
),
page_slug="6_Outlier_Detector",
status="Coming Soon",
section="coming_soon",
),
Tool(
tool_id="08_validator_reporter",
icon=":material/check_circle:",
name="Quality Check",
description=(
"Check your file against rules you set, and export a PDF or "
"Excel report."
),
page_slug="8_Validator_Reporter",
status="Coming Soon",
section="coming_soon",
),
Tool(
tool_id="07_multi_file_merger",
icon=":material/account_tree:",
name="Combine Files",
description=(
"Combine several CSV or Excel files into one — even if their "
"columns don't match."
),
page_slug="7_Multi_File_Merger",
status="Coming Soon",
section="coming_soon",
),
]
# Display labels for each sidebar section. Kept here so i18n falls back
# to a sensible English string if a translation pack is missing the key.
SECTION_LABELS: dict[Section, str] = {
"cleaners": "Data Cleaners",
"transformations": "Transformations",
"automations": "Automations",
"finance": "Finance",
"coming_soon": "Coming soon",
}
def tools_for_tier(*tiers: Tier) -> list[Tool]:
"""Subset filter for build-time slicing.
Empty *tiers* returns every tool. Used by per-tool packaging to ship
only the relevant subset of pages and home-grid cards.
"""
if not tiers:
return list(TOOLS)
keep = set(tiers)
return [t for t in TOOLS if t.tier in keep]
def tools_in_section(section: Section) -> list[Tool]:
"""Return the tools in *section*, preserving registry order."""
return [t for t in TOOLS if t.section == section]
def tool_by_id(tool_id: str) -> Tool | None:
return next((t for t in TOOLS if t.tool_id == tool_id), None)
def display_name(tool_id: str) -> str:
"""Return the human-readable name; fall back to the id when unknown."""
t = tool_by_id(tool_id)
return t.name if t else tool_id
def tool_name(tool_id: str) -> str:
"""Return the localized tool name, falling back to the registry default."""
from src.i18n import t as _t
fallback = display_name(tool_id)
translated = _t(f"tools.{tool_id}.name")
return translated if translated != f"tools.{tool_id}.name" else fallback
def tool_description(tool_id: str) -> str:
"""Return the localized tool description, falling back to the registry default."""
from src.i18n import t as _t
tool = tool_by_id(tool_id)
fallback = tool.description if tool else ""
translated = _t(f"tools.{tool_id}.description")
return translated if translated != f"tools.{tool_id}.description" else fallback
def section_label(section: Section) -> str:
"""Return the localized sidebar section header."""
from src.i18n import t as _t
fallback = SECTION_LABELS[section]
key = f"nav.section_{section}"
translated = _t(key)
return translated if translated != key else fallback