feat(gui): sidebar sections + non-technical tool labels
Sidebar nav now groups tools under Data Review / Data Cleaners / Transformations / Automations via st.navigation, replacing the flat auto-discovered list. Tool display names switch to action-first phrasing (Find Duplicates, Fix Missing Values, Find Unusual Values, Standardize Formats, Clean Text, Quality Check, Map Columns, Combine Files, Automated Workflows) in EN + ES packs and on each page's H1. The Data Cleaners section follows the requested order: Missing Values → Outliers → Text Cleaner → Format Standardizer → Deduplicator → Quality Check. (Text Cleaner kept inside cleaners since the request didn't list it but the tool still ships.) Registry now carries a section field; helpers added: tools_in_section(), section_label(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
158
src/gui/app.py
158
src/gui/app.py
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
Launch:
|
Launch:
|
||||||
streamlit run src/gui/app.py
|
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
|
from __future__ import annotations
|
||||||
@@ -18,70 +26,70 @@ if str(_project_root) not in sys.path:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Page config
|
# Home page (rendered when the user selects the default nav entry)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _home_page() -> None:
|
||||||
|
"""Render the home page — upload section + tool grid + footer."""
|
||||||
from src.gui.components import (
|
from src.gui.components import (
|
||||||
findings_count_for_tool,
|
findings_count_for_tool,
|
||||||
hide_streamlit_chrome,
|
hide_streamlit_chrome,
|
||||||
upload_and_analyze_section,
|
upload_and_analyze_section,
|
||||||
)
|
)
|
||||||
|
from src.gui.tools_registry import (
|
||||||
|
TOOLS,
|
||||||
|
section_label,
|
||||||
|
tool_description,
|
||||||
|
tool_name,
|
||||||
|
)
|
||||||
from src.i18n import t
|
from src.i18n import t
|
||||||
|
from src.license import get_manager
|
||||||
|
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
page_title=t("home.page_title"),
|
page_title=t("home.page_title"),
|
||||||
page_icon="🧹",
|
page_icon="🧹",
|
||||||
layout="wide",
|
layout="wide",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ``hide_streamlit_chrome`` also renders the sidebar language selector,
|
|
||||||
# so every page that hides chrome picks up the same picker.
|
|
||||||
hide_streamlit_chrome()
|
hide_streamlit_chrome()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Home page
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
st.title(t("home.title"))
|
st.title(t("home.title"))
|
||||||
st.caption(t("home.caption"))
|
st.caption(t("home.caption"))
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Upload & analyze (optional onboarding step)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
upload_and_analyze_section()
|
upload_and_analyze_section()
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Tool cards
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
from src.gui.tools_registry import TOOLS, tool_description, tool_name
|
|
||||||
from src.license import get_manager
|
|
||||||
|
|
||||||
# Per-tier feature set for badging — Ready tools that the user's
|
# Per-tier feature set for badging — Ready tools that the user's
|
||||||
# tier doesn't unlock get a red "🔒 Locked" pill instead of the green
|
# tier doesn't unlock get a red "🔒 Locked" pill instead of the green
|
||||||
# "Ready" so they see at a glance which tools an upgrade would
|
# "Ready" so they see at a glance which tools an upgrade would
|
||||||
# unlock. Dev-mode skips the lock check so the home grid renders the
|
# unlock. Dev-mode skips the lock check so the home grid renders the
|
||||||
# full SKU during development.
|
# full SKU during development.
|
||||||
_license_state = get_manager().current_state()
|
_license_state = get_manager().current_state()
|
||||||
_unlocked_features = set(_license_state.features) if _license_state.valid else set()
|
_unlocked_features = (
|
||||||
|
set(_license_state.features) if _license_state.valid else set()
|
||||||
|
)
|
||||||
_dev_mode = get_manager().dev_mode
|
_dev_mode = get_manager().dev_mode
|
||||||
|
|
||||||
# Render tool cards in a 3-column grid. Cards picked up by the analyzer get a
|
# Group tool cards by sidebar section so the home grid mirrors the
|
||||||
# coloured "N findings" badge so the user can see at a glance which tools
|
# left-nav layout — same vocabulary, same ordering.
|
||||||
# would help with the just-uploaded file.
|
sections: list[tuple[str, list]] = []
|
||||||
for row_start in range(0, len(TOOLS), 3):
|
for section in ("review", "cleaners", "transformations", "automations"):
|
||||||
|
tools = [tool for tool in TOOLS if tool.section == section]
|
||||||
|
if not tools:
|
||||||
|
continue
|
||||||
|
sections.append((section_label(section), tools))
|
||||||
|
|
||||||
|
for header, tools in sections:
|
||||||
|
st.subheader(header)
|
||||||
|
for row_start in range(0, len(tools), 3):
|
||||||
cols = st.columns(3)
|
cols = st.columns(3)
|
||||||
for i, col in enumerate(cols):
|
for i, col in enumerate(cols):
|
||||||
idx = row_start + i
|
idx = row_start + i
|
||||||
if idx >= len(TOOLS):
|
if idx >= len(tools):
|
||||||
break
|
break
|
||||||
tool = TOOLS[idx]
|
tool = tools[idx]
|
||||||
with col:
|
with col:
|
||||||
tool_locked = (
|
tool_locked = (
|
||||||
tool.status == "Ready"
|
tool.status == "Ready"
|
||||||
@@ -101,7 +109,11 @@ for row_start in range(0, len(TOOLS), 3):
|
|||||||
badge = ""
|
badge = ""
|
||||||
n = findings_count_for_tool(tool.tool_id)
|
n = findings_count_for_tool(tool.tool_id)
|
||||||
if n:
|
if n:
|
||||||
badge_key = "home.findings_badge_one" if n == 1 else "home.findings_badge_other"
|
badge_key = (
|
||||||
|
"home.findings_badge_one"
|
||||||
|
if n == 1
|
||||||
|
else "home.findings_badge_other"
|
||||||
|
)
|
||||||
badge = f" :red-background[**{t(badge_key, n=n)}**]"
|
badge = f" :red-background[**{t(badge_key, n=n)}**]"
|
||||||
lock_glyph = "🔒 " if tool_locked else ""
|
lock_glyph = "🔒 " if tool_locked else ""
|
||||||
st.markdown(
|
st.markdown(
|
||||||
@@ -110,10 +122,90 @@ for row_start in range(0, len(TOOLS), 3):
|
|||||||
f":{status_color}[**{lock_glyph}{t(status_key)}**]"
|
f":{status_color}[**{lock_glyph}{t(status_key)}**]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Footer
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
st.caption(t("chrome.footer"))
|
st.caption(t("chrome.footer"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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 # 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] = {
|
||||||
|
"review": [],
|
||||||
|
"cleaners": [],
|
||||||
|
"transformations": [],
|
||||||
|
"automations": [],
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# The Review gate has no entry in the registry (it isn't a "tool")
|
||||||
|
# so register it by hand at the top of its section.
|
||||||
|
review_page = st.Page(
|
||||||
|
"pages/0_Review.py",
|
||||||
|
title=_t("nav.review_page_title") or "Review",
|
||||||
|
icon="🛡️",
|
||||||
|
url_path="review",
|
||||||
|
)
|
||||||
|
by_section["review"].insert(0, review_page)
|
||||||
|
|
||||||
|
home = st.Page(
|
||||||
|
_home_page,
|
||||||
|
title=_t("nav.home_page_title") or "Home",
|
||||||
|
icon="🧹",
|
||||||
|
default=True,
|
||||||
|
url_path="home",
|
||||||
|
)
|
||||||
|
activate = st.Page(
|
||||||
|
"pages/_Activate.py",
|
||||||
|
title=_t("nav.activate_title") or "Activate",
|
||||||
|
icon="🔑",
|
||||||
|
url_path="activate",
|
||||||
|
)
|
||||||
|
close = st.Page(
|
||||||
|
"pages/99_Close.py",
|
||||||
|
title=_t("nav.close_title") or "Close",
|
||||||
|
icon="🛑",
|
||||||
|
url_path="close",
|
||||||
|
)
|
||||||
|
|
||||||
|
account_header = _t("nav.section_account") or "Account"
|
||||||
|
return {
|
||||||
|
"": [home],
|
||||||
|
section_label("review"): by_section["review"],
|
||||||
|
section_label("cleaners"): by_section["cleaners"],
|
||||||
|
section_label("transformations"): by_section["transformations"],
|
||||||
|
section_label("automations"): by_section["automations"],
|
||||||
|
account_header: [activate, close],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pg = st.navigation(_build_navigation())
|
||||||
|
pg.run()
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ for key, default in _DEFAULTS.items():
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("🔍 Deduplicator")
|
st.title("🔍 Find Duplicates")
|
||||||
st.caption("Find and remove duplicate rows in CSV, delimited text, and Excel files.")
|
st.caption("Find and remove duplicate rows in CSV, delimited text, and Excel files.")
|
||||||
|
|
||||||
|
|
||||||
@@ -363,5 +363,5 @@ else:
|
|||||||
st.divider()
|
st.divider()
|
||||||
st.caption(
|
st.caption(
|
||||||
"Runs locally. Your data never leaves this computer. "
|
"Runs locally. Your data never leaves this computer. "
|
||||||
"| DataTools Deduplicator v3.0"
|
"| DataTools v3.0"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("✂️ Text Cleaner")
|
st.title("✂️ Clean Text")
|
||||||
st.caption(
|
st.caption(
|
||||||
"Trim whitespace, fold smart quotes, strip invisible characters, and "
|
"Trim whitespace, fold smart quotes, strip invisible characters, and "
|
||||||
"normalize line endings. Runs locally — your data never leaves this computer."
|
"normalize line endings. Runs locally — your data never leaves this computer."
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("📐 Format Standardizer")
|
st.title("📐 Standardize Formats")
|
||||||
st.caption(
|
st.caption(
|
||||||
"Canonicalize dates, phone numbers, currency, names, addresses, and "
|
"Canonicalize dates, phone numbers, currency, names, addresses, and "
|
||||||
"booleans on a per-column basis. Runs locally — your data never leaves "
|
"booleans on a per-column basis. Runs locally — your data never leaves "
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("🕳️ Missing Value Handler")
|
st.title("🕳️ Fix Missing Values")
|
||||||
st.caption(
|
st.caption(
|
||||||
"Detect disguised nulls, profile missingness, and apply imputation or "
|
"Detect disguised nulls, profile missingness, and apply imputation or "
|
||||||
"drop strategies. Runs locally — your data never leaves this computer."
|
"drop strategies. Runs locally — your data never leaves this computer."
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("🗂️ Column Mapper")
|
st.title("🗂️ Map Columns")
|
||||||
st.caption(
|
st.caption(
|
||||||
"Rename columns, enforce a target schema, and coerce types. Runs locally — "
|
"Rename columns, enforce a target schema, and coerce types. Runs locally — "
|
||||||
"your data never leaves this computer."
|
"your data never leaves this computer."
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("📊 Outlier Detector")
|
st.title("📊 Find Unusual Values")
|
||||||
st.caption("Detect and handle outliers in numeric columns.")
|
st.caption("Detect and handle outliers in numeric columns.")
|
||||||
|
|
||||||
st.info("This tool is under development.")
|
st.info("This tool is under development.")
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("📎 Multi-File Merger")
|
st.title("📎 Combine Files")
|
||||||
st.caption("Combine multiple CSV and Excel files into one dataset.")
|
st.caption("Combine multiple CSV and Excel files into one dataset.")
|
||||||
|
|
||||||
st.info("This tool is under development.")
|
st.info("This tool is under development.")
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("✅ Validator & Reporter")
|
st.title("✅ Quality Check")
|
||||||
st.caption("Validate data against rules and generate quality reports.")
|
st.caption("Validate data against rules and generate quality reports.")
|
||||||
|
|
||||||
st.info("This tool is under development.")
|
st.info("This tool is under development.")
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ require_normalization_gate()
|
|||||||
# Header
|
# Header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
st.title("⚙️ Pipeline Runner")
|
st.title("⚙️ Automated Workflows")
|
||||||
st.caption(
|
st.caption(
|
||||||
"Chain DataTools cleaning steps into one repeatable workflow. The "
|
"Chain DataTools cleaning steps into one repeatable workflow. The "
|
||||||
"pipeline recommends an order; you stay in control."
|
"pipeline recommends an order; you stay in control."
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Per-tool manifest registry.
|
"""Per-tool manifest registry.
|
||||||
|
|
||||||
Single source of truth for what tools exist, their display strings, and
|
Single source of truth for what tools exist, their display strings, the
|
||||||
the tier (which controls whether a tool ships in a given build SKU). The
|
left-nav section they belong to, and the tier (which controls whether a
|
||||||
home-page sidebar consumes this list; future per-tool packaging will
|
tool ships in a given build SKU). The home-page sidebar consumes this
|
||||||
filter it via the ``tier`` field.
|
list; future per-tool packaging will filter it via the ``tier`` field.
|
||||||
|
|
||||||
Adding a tool: append one ``Tool`` entry. Page filenames must match the
|
Adding a tool: append one ``Tool`` entry. Page filenames must match the
|
||||||
``page_slug`` so Streamlit's automatic page discovery picks them up.
|
``page_slug`` so Streamlit's automatic page discovery picks them up.
|
||||||
@@ -22,6 +22,10 @@ from typing import Literal
|
|||||||
|
|
||||||
Tier = Literal["core", "pro", "enterprise"]
|
Tier = Literal["core", "pro", "enterprise"]
|
||||||
Status = Literal["Ready", "Coming Soon"]
|
Status = Literal["Ready", "Coming Soon"]
|
||||||
|
# Sidebar grouping. The Review gate is its own section; cleaners,
|
||||||
|
# transformations, and automations group the tools by what the user is
|
||||||
|
# trying to accomplish rather than by implementation detail.
|
||||||
|
Section = Literal["review", "cleaners", "transformations", "automations"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -34,102 +38,127 @@ class Tool:
|
|||||||
description: str # One-sentence card body.
|
description: str # One-sentence card body.
|
||||||
page_slug: str # Streamlit page filename without ".py" (e.g. "1_Deduplicator").
|
page_slug: str # Streamlit page filename without ".py" (e.g. "1_Deduplicator").
|
||||||
status: Status # "Ready" or "Coming Soon" — drives the card badge color.
|
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.
|
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" we lead with the
|
||||||
|
# operations a non-technical user is most likely to need (filling
|
||||||
|
# blanks, flagging outliers) before progressing to format cleanup,
|
||||||
|
# dedup, and the final quality report.
|
||||||
TOOLS: list[Tool] = [
|
TOOLS: list[Tool] = [
|
||||||
Tool(
|
|
||||||
tool_id="01_deduplicator",
|
|
||||||
icon="🔍",
|
|
||||||
name="Deduplicator",
|
|
||||||
description=(
|
|
||||||
"Fuzzy matching, normalization, survivor selection, and "
|
|
||||||
"interactive review."
|
|
||||||
),
|
|
||||||
page_slug="1_Deduplicator",
|
|
||||||
status="Ready",
|
|
||||||
),
|
|
||||||
Tool(
|
|
||||||
tool_id="02_text_cleaner",
|
|
||||||
icon="✂️",
|
|
||||||
name="Text Cleaner",
|
|
||||||
description=(
|
|
||||||
"Whitespace trim, multi-space collapse, Unicode normalization, "
|
|
||||||
"BOM and line-ending handling."
|
|
||||||
),
|
|
||||||
page_slug="2_Text_Cleaner",
|
|
||||||
status="Ready",
|
|
||||||
),
|
|
||||||
Tool(
|
|
||||||
tool_id="03_format_standardizer",
|
|
||||||
icon="📐",
|
|
||||||
name="Format Standardizer",
|
|
||||||
description=(
|
|
||||||
"Standardize dates, currencies, names, phone numbers, and addresses."
|
|
||||||
),
|
|
||||||
page_slug="3_Format_Standardizer",
|
|
||||||
status="Ready",
|
|
||||||
),
|
|
||||||
Tool(
|
Tool(
|
||||||
tool_id="04_missing_handler",
|
tool_id="04_missing_handler",
|
||||||
icon="🕳️",
|
icon="🕳️",
|
||||||
name="Missing Value Handler",
|
name="Fix Missing Values",
|
||||||
description=(
|
description=(
|
||||||
"Detect disguised nulls, missingness analysis, and imputation strategies."
|
"Detect disguised nulls, missingness analysis, and imputation strategies."
|
||||||
),
|
),
|
||||||
page_slug="4_Missing_Values",
|
page_slug="4_Missing_Values",
|
||||||
status="Ready",
|
status="Ready",
|
||||||
),
|
section="cleaners",
|
||||||
Tool(
|
|
||||||
tool_id="05_column_mapper",
|
|
||||||
icon="🗂️",
|
|
||||||
name="Column Mapper",
|
|
||||||
description="Rename columns, enforce a target schema, and coerce types.",
|
|
||||||
page_slug="5_Column_Mapper",
|
|
||||||
status="Ready",
|
|
||||||
),
|
),
|
||||||
Tool(
|
Tool(
|
||||||
tool_id="06_outlier_detector",
|
tool_id="06_outlier_detector",
|
||||||
icon="📊",
|
icon="📊",
|
||||||
name="Outlier Detector",
|
name="Find Unusual Values",
|
||||||
description=(
|
description=(
|
||||||
"Z-score, IQR, and MAD detection with domain-rule violations and "
|
"Z-score, IQR, and MAD detection with domain-rule violations and "
|
||||||
"winsorization."
|
"winsorization."
|
||||||
),
|
),
|
||||||
page_slug="6_Outlier_Detector",
|
page_slug="6_Outlier_Detector",
|
||||||
status="Coming Soon",
|
status="Coming Soon",
|
||||||
|
section="cleaners",
|
||||||
),
|
),
|
||||||
Tool(
|
Tool(
|
||||||
tool_id="07_multi_file_merger",
|
tool_id="02_text_cleaner",
|
||||||
icon="📎",
|
icon="✂️",
|
||||||
name="Multi-File Merger",
|
name="Clean Text",
|
||||||
description="Combine multiple CSV/Excel files with schema alignment.",
|
description=(
|
||||||
page_slug="7_Multi_File_Merger",
|
"Whitespace trim, multi-space collapse, Unicode normalization, "
|
||||||
status="Coming Soon",
|
"BOM and line-ending handling."
|
||||||
|
),
|
||||||
|
page_slug="2_Text_Cleaner",
|
||||||
|
status="Ready",
|
||||||
|
section="cleaners",
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
tool_id="03_format_standardizer",
|
||||||
|
icon="📐",
|
||||||
|
name="Standardize Formats",
|
||||||
|
description=(
|
||||||
|
"Standardize dates, currencies, names, phone numbers, and addresses."
|
||||||
|
),
|
||||||
|
page_slug="3_Format_Standardizer",
|
||||||
|
status="Ready",
|
||||||
|
section="cleaners",
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
tool_id="01_deduplicator",
|
||||||
|
icon="🔍",
|
||||||
|
name="Find Duplicates",
|
||||||
|
description=(
|
||||||
|
"Fuzzy matching, normalization, survivor selection, and "
|
||||||
|
"interactive review."
|
||||||
|
),
|
||||||
|
page_slug="1_Deduplicator",
|
||||||
|
status="Ready",
|
||||||
|
section="cleaners",
|
||||||
),
|
),
|
||||||
Tool(
|
Tool(
|
||||||
tool_id="08_validator_reporter",
|
tool_id="08_validator_reporter",
|
||||||
icon="✅",
|
icon="✅",
|
||||||
name="Validator & Reporter",
|
name="Quality Check",
|
||||||
description=(
|
description=(
|
||||||
"Validate against rules and generate PDF/Excel quality reports."
|
"Validate against rules and generate PDF/Excel quality reports."
|
||||||
),
|
),
|
||||||
page_slug="8_Validator_Reporter",
|
page_slug="8_Validator_Reporter",
|
||||||
status="Coming Soon",
|
status="Coming Soon",
|
||||||
|
section="cleaners",
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
tool_id="05_column_mapper",
|
||||||
|
icon="🗂️",
|
||||||
|
name="Map Columns",
|
||||||
|
description="Rename columns, enforce a target schema, and coerce types.",
|
||||||
|
page_slug="5_Column_Mapper",
|
||||||
|
status="Ready",
|
||||||
|
section="transformations",
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
tool_id="07_multi_file_merger",
|
||||||
|
icon="📎",
|
||||||
|
name="Combine Files",
|
||||||
|
description="Combine multiple CSV/Excel files with schema alignment.",
|
||||||
|
page_slug="7_Multi_File_Merger",
|
||||||
|
status="Coming Soon",
|
||||||
|
section="transformations",
|
||||||
),
|
),
|
||||||
Tool(
|
Tool(
|
||||||
tool_id="09_pipeline_runner",
|
tool_id="09_pipeline_runner",
|
||||||
icon="⚙️",
|
icon="⚙️",
|
||||||
name="Pipeline Runner",
|
name="Automated Workflows",
|
||||||
description=(
|
description=(
|
||||||
"Chain tools in recommended order and pass output between steps."
|
"Chain tools in recommended order and pass output between steps."
|
||||||
),
|
),
|
||||||
page_slug="9_Pipeline_Runner",
|
page_slug="9_Pipeline_Runner",
|
||||||
status="Ready",
|
status="Ready",
|
||||||
|
section="automations",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# 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] = {
|
||||||
|
"review": "Data Review",
|
||||||
|
"cleaners": "Data Cleaners",
|
||||||
|
"transformations": "Transformations",
|
||||||
|
"automations": "Automations",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def tools_for_tier(*tiers: Tier) -> list[Tool]:
|
def tools_for_tier(*tiers: Tier) -> list[Tool]:
|
||||||
"""Subset filter for build-time slicing.
|
"""Subset filter for build-time slicing.
|
||||||
|
|
||||||
@@ -142,6 +171,11 @@ def tools_for_tier(*tiers: Tier) -> list[Tool]:
|
|||||||
return [t for t in TOOLS if t.tier in keep]
|
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:
|
def tool_by_id(tool_id: str) -> Tool | None:
|
||||||
return next((t for t in TOOLS if t.tool_id == tool_id), None)
|
return next((t for t in TOOLS if t.tool_id == tool_id), None)
|
||||||
|
|
||||||
@@ -167,3 +201,12 @@ def tool_description(tool_id: str) -> str:
|
|||||||
fallback = tool.description if tool else ""
|
fallback = tool.description if tool else ""
|
||||||
translated = _t(f"tools.{tool_id}.description")
|
translated = _t(f"tools.{tool_id}.description")
|
||||||
return translated if translated != f"tools.{tool_id}.description" else fallback
|
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
|
||||||
|
|||||||
@@ -99,40 +99,51 @@
|
|||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"01_deduplicator": {
|
"01_deduplicator": {
|
||||||
"name": "Deduplicator",
|
"name": "Find Duplicates",
|
||||||
"description": "Fuzzy matching, normalization, survivor selection, and interactive review."
|
"description": "Fuzzy matching, normalization, survivor selection, and interactive review."
|
||||||
},
|
},
|
||||||
"02_text_cleaner": {
|
"02_text_cleaner": {
|
||||||
"name": "Text Cleaner",
|
"name": "Clean Text",
|
||||||
"description": "Whitespace trim, multi-space collapse, Unicode normalization, BOM and line-ending handling."
|
"description": "Whitespace trim, multi-space collapse, Unicode normalization, BOM and line-ending handling."
|
||||||
},
|
},
|
||||||
"03_format_standardizer": {
|
"03_format_standardizer": {
|
||||||
"name": "Format Standardizer",
|
"name": "Standardize Formats",
|
||||||
"description": "Standardize dates, currencies, names, phone numbers, and addresses."
|
"description": "Standardize dates, currencies, names, phone numbers, and addresses."
|
||||||
},
|
},
|
||||||
"04_missing_handler": {
|
"04_missing_handler": {
|
||||||
"name": "Missing Value Handler",
|
"name": "Fix Missing Values",
|
||||||
"description": "Detect disguised nulls, missingness analysis, and imputation strategies."
|
"description": "Detect disguised nulls, missingness analysis, and imputation strategies."
|
||||||
},
|
},
|
||||||
"05_column_mapper": {
|
"05_column_mapper": {
|
||||||
"name": "Column Mapper",
|
"name": "Map Columns",
|
||||||
"description": "Rename columns, enforce a target schema, and coerce types."
|
"description": "Rename columns, enforce a target schema, and coerce types."
|
||||||
},
|
},
|
||||||
"06_outlier_detector": {
|
"06_outlier_detector": {
|
||||||
"name": "Outlier Detector",
|
"name": "Find Unusual Values",
|
||||||
"description": "Z-score, IQR, and MAD detection with domain-rule violations and winsorization."
|
"description": "Z-score, IQR, and MAD detection with domain-rule violations and winsorization."
|
||||||
},
|
},
|
||||||
"07_multi_file_merger": {
|
"07_multi_file_merger": {
|
||||||
"name": "Multi-File Merger",
|
"name": "Combine Files",
|
||||||
"description": "Combine multiple CSV/Excel files with schema alignment."
|
"description": "Combine multiple CSV/Excel files with schema alignment."
|
||||||
},
|
},
|
||||||
"08_validator_reporter": {
|
"08_validator_reporter": {
|
||||||
"name": "Validator & Reporter",
|
"name": "Quality Check",
|
||||||
"description": "Validate against rules and generate PDF/Excel quality reports."
|
"description": "Validate against rules and generate PDF/Excel quality reports."
|
||||||
},
|
},
|
||||||
"09_pipeline_runner": {
|
"09_pipeline_runner": {
|
||||||
"name": "Pipeline Runner",
|
"name": "Automated Workflows",
|
||||||
"description": "Chain tools in recommended order and pass output between steps."
|
"description": "Chain tools in recommended order and pass output between steps."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"section_review": "Data Review",
|
||||||
|
"section_cleaners": "Data Cleaners",
|
||||||
|
"section_transformations": "Transformations",
|
||||||
|
"section_automations": "Automations",
|
||||||
|
"review_page_title": "Review",
|
||||||
|
"home_page_title": "Home",
|
||||||
|
"section_account": "Account",
|
||||||
|
"activate_title": "Activate",
|
||||||
|
"close_title": "Close"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,40 +99,51 @@
|
|||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"01_deduplicator": {
|
"01_deduplicator": {
|
||||||
"name": "Eliminador de duplicados",
|
"name": "Buscar duplicados",
|
||||||
"description": "Coincidencia difusa, normalización, selección de superviviente y revisión interactiva."
|
"description": "Coincidencia difusa, normalización, selección de superviviente y revisión interactiva."
|
||||||
},
|
},
|
||||||
"02_text_cleaner": {
|
"02_text_cleaner": {
|
||||||
"name": "Limpiador de texto",
|
"name": "Limpiar texto",
|
||||||
"description": "Recorte de espacios, colapso de espacios múltiples, normalización Unicode, manejo de BOM y de finales de línea."
|
"description": "Recorte de espacios, colapso de espacios múltiples, normalización Unicode, manejo de BOM y de finales de línea."
|
||||||
},
|
},
|
||||||
"03_format_standardizer": {
|
"03_format_standardizer": {
|
||||||
"name": "Estandarizador de formatos",
|
"name": "Estandarizar formatos",
|
||||||
"description": "Estandariza fechas, monedas, nombres, números de teléfono y direcciones."
|
"description": "Estandariza fechas, monedas, nombres, números de teléfono y direcciones."
|
||||||
},
|
},
|
||||||
"04_missing_handler": {
|
"04_missing_handler": {
|
||||||
"name": "Gestor de valores faltantes",
|
"name": "Corregir valores faltantes",
|
||||||
"description": "Detecta nulos disfrazados, analiza la ausencia de datos y aplica estrategias de imputación."
|
"description": "Detecta nulos disfrazados, analiza la ausencia de datos y aplica estrategias de imputación."
|
||||||
},
|
},
|
||||||
"05_column_mapper": {
|
"05_column_mapper": {
|
||||||
"name": "Mapeador de columnas",
|
"name": "Mapear columnas",
|
||||||
"description": "Renombra columnas, aplica un esquema objetivo y fuerza tipos de datos."
|
"description": "Renombra columnas, aplica un esquema objetivo y fuerza tipos de datos."
|
||||||
},
|
},
|
||||||
"06_outlier_detector": {
|
"06_outlier_detector": {
|
||||||
"name": "Detector de valores atípicos",
|
"name": "Detectar valores atípicos",
|
||||||
"description": "Detección por Z-score, IQR y MAD con reglas de dominio y winsorización."
|
"description": "Detección por Z-score, IQR y MAD con reglas de dominio y winsorización."
|
||||||
},
|
},
|
||||||
"07_multi_file_merger": {
|
"07_multi_file_merger": {
|
||||||
"name": "Combinador de varios archivos",
|
"name": "Combinar archivos",
|
||||||
"description": "Combina varios archivos CSV/Excel alineando sus esquemas."
|
"description": "Combina varios archivos CSV/Excel alineando sus esquemas."
|
||||||
},
|
},
|
||||||
"08_validator_reporter": {
|
"08_validator_reporter": {
|
||||||
"name": "Validador e informes",
|
"name": "Verificación de calidad",
|
||||||
"description": "Valida contra reglas y genera informes de calidad en PDF/Excel."
|
"description": "Valida contra reglas y genera informes de calidad en PDF/Excel."
|
||||||
},
|
},
|
||||||
"09_pipeline_runner": {
|
"09_pipeline_runner": {
|
||||||
"name": "Ejecutor de canalizaciones",
|
"name": "Flujos automatizados",
|
||||||
"description": "Encadena herramientas en el orden recomendado y pasa la salida entre pasos."
|
"description": "Encadena herramientas en el orden recomendado y pasa la salida entre pasos."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"section_review": "Revisión de datos",
|
||||||
|
"section_cleaners": "Limpiadores de datos",
|
||||||
|
"section_transformations": "Transformaciones",
|
||||||
|
"section_automations": "Automatizaciones",
|
||||||
|
"review_page_title": "Revisión",
|
||||||
|
"home_page_title": "Inicio",
|
||||||
|
"section_account": "Cuenta",
|
||||||
|
"activate_title": "Activar",
|
||||||
|
"close_title": "Cerrar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,11 +168,11 @@ class TestHomeToolGridLocalization:
|
|||||||
English names. Pin a few representative ones."""
|
English names. Pin a few representative ones."""
|
||||||
|
|
||||||
@pytest.mark.parametrize("needle", [
|
@pytest.mark.parametrize("needle", [
|
||||||
"Eliminador de duplicados",
|
"Buscar duplicados",
|
||||||
"Limpiador de texto",
|
"Limpiar texto",
|
||||||
"Estandarizador de formatos",
|
"Estandarizar formatos",
|
||||||
"Gestor de valores faltantes",
|
"Corregir valores faltantes",
|
||||||
"Mapeador de columnas",
|
"Mapear columnas",
|
||||||
])
|
])
|
||||||
def test_es_tool_name_on_home_grid(self, home_app, needle):
|
def test_es_tool_name_on_home_grid(self, home_app, needle):
|
||||||
with_language(home_app, "es")
|
with_language(home_app, "es")
|
||||||
|
|||||||
@@ -113,13 +113,13 @@ class TestGrouping:
|
|||||||
labels = [e.label for e in app.expander]
|
labels = [e.label for e in app.expander]
|
||||||
# Two unique tools → two expanders. Each label carries the
|
# Two unique tools → two expanders. Each label carries the
|
||||||
# tool's display name + finding count.
|
# tool's display name + finding count.
|
||||||
text_cleaner_expanders = [lbl for lbl in labels if "Text Cleaner" in lbl]
|
text_cleaner_expanders = [lbl for lbl in labels if "Clean Text" in lbl]
|
||||||
format_expanders = [lbl for lbl in labels if "Format Standardizer" in lbl]
|
format_expanders = [lbl for lbl in labels if "Standardize Formats" in lbl]
|
||||||
assert len(text_cleaner_expanders) == 1, (
|
assert len(text_cleaner_expanders) == 1, (
|
||||||
f"expected one Text Cleaner expander; got: {labels}"
|
f"expected one Clean Text expander; got: {labels}"
|
||||||
)
|
)
|
||||||
assert len(format_expanders) == 1, (
|
assert len(format_expanders) == 1, (
|
||||||
f"expected one Format Standardizer expander; got: {labels}"
|
f"expected one Standardize Formats expander; got: {labels}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tool_names_localize_in_spanish(self):
|
def test_tool_names_localize_in_spanish(self):
|
||||||
@@ -127,7 +127,7 @@ class TestGrouping:
|
|||||||
app = _harness(findings, lang="es")
|
app = _harness(findings, lang="es")
|
||||||
app.run()
|
app.run()
|
||||||
labels = [e.label for e in app.expander]
|
labels = [e.label for e in app.expander]
|
||||||
assert any("Limpiador de texto" in lbl for lbl in labels), (
|
assert any("Limpiar texto" in lbl for lbl in labels), (
|
||||||
f"Spanish tool name missing; expanders: {labels}"
|
f"Spanish tool name missing; expanders: {labels}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ class TestGrouping:
|
|||||||
app.run()
|
app.run()
|
||||||
labels = [e.label for e in app.expander]
|
labels = [e.label for e in app.expander]
|
||||||
# Pack template: "{tool} — {n} finding(s)"
|
# Pack template: "{tool} — {n} finding(s)"
|
||||||
text_cleaner_label = next(l for l in labels if "Text Cleaner" in l)
|
text_cleaner_label = next(l for l in labels if "Clean Text" in l)
|
||||||
assert "3" in text_cleaner_label, (
|
assert "3" in text_cleaner_label, (
|
||||||
f"expected count '3' in expander label; got {text_cleaner_label!r}"
|
f"expected count '3' in expander label; got {text_cleaner_label!r}"
|
||||||
)
|
)
|
||||||
@@ -163,7 +163,7 @@ class TestOpenToolButton:
|
|||||||
# raw markdown. We probe both.
|
# raw markdown. We probe both.
|
||||||
text = collected_text(app)
|
text = collected_text(app)
|
||||||
# Pack template: "Open {tool} →"
|
# Pack template: "Open {tool} →"
|
||||||
assert "Open Text Cleaner" in text
|
assert "Open Clean Text" in text
|
||||||
|
|
||||||
def test_open_tool_label_spanish(self):
|
def test_open_tool_label_spanish(self):
|
||||||
findings = [_make_finding(tool="02_text_cleaner")]
|
findings = [_make_finding(tool="02_text_cleaner")]
|
||||||
@@ -171,7 +171,7 @@ class TestOpenToolButton:
|
|||||||
app.run()
|
app.run()
|
||||||
text = collected_text(app)
|
text = collected_text(app)
|
||||||
# Pack template: "Abrir {tool} →"
|
# Pack template: "Abrir {tool} →"
|
||||||
assert "Abrir Limpiador de texto" in text
|
assert "Abrir Limpiar texto" in text
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class TestGateNoUpload:
|
|||||||
text = collected_text(app)
|
text = collected_text(app)
|
||||||
# The dedup page title is the unambiguous signal that the gate
|
# The dedup page title is the unambiguous signal that the gate
|
||||||
# didn't short-circuit.
|
# didn't short-circuit.
|
||||||
assert "Deduplicator" in text
|
assert "Find Duplicates" in text
|
||||||
|
|
||||||
def test_no_upload_no_gate_warning(self, app_factory):
|
def test_no_upload_no_gate_warning(self, app_factory):
|
||||||
app = app_factory(GATED_PAGE)
|
app = app_factory(GATED_PAGE)
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ def lite_license(monkeypatch, tmp_path):
|
|||||||
|
|
||||||
class TestLiteUnlockedPages:
|
class TestLiteUnlockedPages:
|
||||||
@pytest.mark.parametrize("slug,signal", [
|
@pytest.mark.parametrize("slug,signal", [
|
||||||
("1_Deduplicator", "Deduplicator"),
|
("1_Deduplicator", "Find Duplicates"),
|
||||||
("2_Text_Cleaner", "Text Cleaner"),
|
("2_Text_Cleaner", "Clean Text"),
|
||||||
("3_Format_Standardizer", "Format Standardizer"),
|
("3_Format_Standardizer", "Standardize Formats"),
|
||||||
])
|
])
|
||||||
def test_unlocked_pages_render_body(
|
def test_unlocked_pages_render_body(
|
||||||
self, lite_license, app_factory, slug, signal, small_csv_bytes,
|
self, lite_license, app_factory, slug, signal, small_csv_bytes,
|
||||||
|
|||||||
@@ -54,15 +54,15 @@ PAGE_SLUGS = [
|
|||||||
# the localized substring — the test surface stays the same.
|
# the localized substring — the test surface stays the same.
|
||||||
EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
|
EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
|
||||||
"0_Review": {"en": "Review", "es": "Review"},
|
"0_Review": {"en": "Review", "es": "Review"},
|
||||||
"1_Deduplicator": {"en": "Deduplicator", "es": "Deduplicator"},
|
"1_Deduplicator": {"en": "Find Duplicates", "es": "Find Duplicates"},
|
||||||
"2_Text_Cleaner": {"en": "Text Cleaner", "es": "Text Cleaner"},
|
"2_Text_Cleaner": {"en": "Clean Text", "es": "Clean Text"},
|
||||||
"3_Format_Standardizer": {"en": "Format", "es": "Format"},
|
"3_Format_Standardizer": {"en": "Standardize", "es": "Standardize"},
|
||||||
"4_Missing_Values": {"en": "Missing", "es": "Missing"},
|
"4_Missing_Values": {"en": "Fix Missing", "es": "Fix Missing"},
|
||||||
"5_Column_Mapper": {"en": "Column", "es": "Column"},
|
"5_Column_Mapper": {"en": "Map Columns", "es": "Map Columns"},
|
||||||
"6_Outlier_Detector": {"en": "Outlier", "es": "Outlier"},
|
"6_Outlier_Detector": {"en": "Unusual", "es": "Unusual"},
|
||||||
"7_Multi_File_Merger": {"en": "Merger", "es": "Merger"},
|
"7_Multi_File_Merger": {"en": "Combine Files", "es": "Combine Files"},
|
||||||
"8_Validator_Reporter": {"en": "Validator", "es": "Validator"},
|
"8_Validator_Reporter": {"en": "Quality Check", "es": "Quality Check"},
|
||||||
"9_Pipeline_Runner": {"en": "Pipeline", "es": "Pipeline"},
|
"9_Pipeline_Runner": {"en": "Automated", "es": "Automated"},
|
||||||
"99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"},
|
"99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class TestHomePageRenders:
|
|||||||
with_language(home_app, "es")
|
with_language(home_app, "es")
|
||||||
home_app.run()
|
home_app.run()
|
||||||
text = collected_text(home_app)
|
text = collected_text(home_app)
|
||||||
assert "Eliminador de duplicados" in text
|
assert "Buscar duplicados" in text
|
||||||
|
|
||||||
|
|
||||||
class TestEveryPageRenders:
|
class TestEveryPageRenders:
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class TestTextCleanerWorkflow:
|
|||||||
app.run()
|
app.run()
|
||||||
assert not app.exception
|
assert not app.exception
|
||||||
text = collected_text(app)
|
text = collected_text(app)
|
||||||
assert "Text Cleaner" in text
|
assert "Clean Text" in text
|
||||||
|
|
||||||
def test_preview_or_clean_button_present(self, app_factory, small_csv_bytes):
|
def test_preview_or_clean_button_present(self, app_factory, small_csv_bytes):
|
||||||
"""The text cleaner ships a primary action (label varies by
|
"""The text cleaner ships a primary action (label varies by
|
||||||
@@ -106,7 +106,7 @@ class TestFormatStandardizerWorkflow:
|
|||||||
app.run()
|
app.run()
|
||||||
assert not app.exception
|
assert not app.exception
|
||||||
text = collected_text(app)
|
text = collected_text(app)
|
||||||
assert "Format Standardizer" in text
|
assert "Standardize Formats" in text
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -148,7 +148,7 @@ class TestPipelineRunnerWorkflow:
|
|||||||
app.run()
|
app.run()
|
||||||
assert not app.exception
|
assert not app.exception
|
||||||
text = collected_text(app)
|
text = collected_text(app)
|
||||||
assert "Pipeline" in text
|
assert "Automated Workflows" in text
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -194,9 +194,9 @@ class TestReviewWorkflow:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@pytest.mark.parametrize("slug,name", [
|
@pytest.mark.parametrize("slug,name", [
|
||||||
("6_Outlier_Detector", "Outlier"),
|
("6_Outlier_Detector", "Unusual Values"),
|
||||||
("7_Multi_File_Merger", "Merger"),
|
("7_Multi_File_Merger", "Combine Files"),
|
||||||
("8_Validator_Reporter", "Validator"),
|
("8_Validator_Reporter", "Quality Check"),
|
||||||
])
|
])
|
||||||
class TestComingSoonStubs:
|
class TestComingSoonStubs:
|
||||||
def test_stub_renders(self, app_factory, slug, name):
|
def test_stub_renders(self, app_factory, slug, name):
|
||||||
|
|||||||
Reference in New Issue
Block a user