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>
251 lines
8.2 KiB
Python
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
|