feat: 3 new tools, format streaming, distribution-ready demo + landing pages
Tools shipped this batch (4 → 6 of 9 Ready):
04 Missing Value Handler src/core/missing.py + cli_missing.py + GUI
05 Column Mapper src/core/column_mapper.py + cli_column_map.py + GUI
09 Pipeline Runner src/core/pipeline.py + cli_pipeline.py + GUI
with soft tool-dependency graph (recommended,
not enforced) and JSON save/load for repeatable
weekly cleanups.
Format Standardizer reworked for 1 GB international files:
• Vectorised dispatch + LRU cache over phone/date/currency/boolean/email
• Per-row country / address columns drive parsing
• Audit cap (default 10 k rows, ~50 MB RAM)
• standardize_file(): chunked streaming entry point (~165 k rows/sec)
• currency_decimal="auto" for EU comma-decimal locales
• R$ / kr / zł multi-char currency prefixes
• cli_format.py with auto-stream above 100 MB inputs
Encoding detection arbiter + language-aware probe:
Closes the last 4 xfails (cp1250 / mac_iceland / shift_jis_2004 / lying-BOM)
via tied-confidence arbiter + Cyrillic / EE-Latin coverage probes.
Distribution-readiness assets:
• streamlit_app.py — Streamlit Community Cloud entry shim
• src/gui/app_demo.py — single-page demo, ?p=<persona> routing,
100-row cap + watermark, free-vs-paid boundary enforced at surface
• samples/demo/ — 3 niche datasets + pre-tuned pipeline JSONs
• landing/ — 4 static HTML pages (apex chooser + 3 niche),
shared CSS, deploy.py URL-substitution script,
auto-generated robots.txt + sitemap.xml + 404.html + favicon
• docs/PLAN.md, DEMO-PLAN.md, DEPLOYMENT.md, POST-LAUNCH.md, NEXT-STEPS.md
— full strategy + measurement + deployment + master checklist
Test counts:
before: 1,520 passed · 4 skipped · 17 xfailed
after: 1,729 passed · 0 skipped · 0 xfailed
Tier-1 corpora added:
• missing-corpus 3 use cases + 16 edge cases
• column-mapper-corpus 3 use cases + 5 edge cases
• format-cleaner intl 20-row 13-country stress fixture
Engine hardening flushed out by the corpora:
• interpolate guards against object-dtype columns
• mean/median skip all-NaN columns (silences numpy warning)
• fillna runs under future.no_silent_downcasting (silences pandas warning)
• mojibake test no longer skips when ftfy installed (monkeypatch path)
• drop-row threshold semantics: strict-greater (consistent across rows / cols)
• currency_decimal validator allow-set updated for "auto"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
105
tests/test_format_intl_corpus.py
Normal file
105
tests/test_format_intl_corpus.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Acceptance corpus for international format standardization.
|
||||
|
||||
Stresses the rework's three pillars on a single mixed-locale fixture:
|
||||
* Per-row country column drives phone parsing.
|
||||
* ``currency_decimal="auto"`` resolves comma-decimal locales.
|
||||
* Streaming entry point handles the same content unchanged.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from src.core.format_standardize import (
|
||||
FieldType,
|
||||
StandardizeOptions,
|
||||
standardize_dataframe,
|
||||
standardize_file,
|
||||
)
|
||||
|
||||
CORPUS = Path(__file__).resolve().parents[1] / "test-cases" / "format-cleaner-corpus" / "international"
|
||||
FIXTURE = CORPUS / "intl_phones_addresses.csv"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def df():
|
||||
return pd.read_csv(FIXTURE, dtype=str, keep_default_na=False)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def options():
|
||||
return StandardizeOptions(
|
||||
column_types={
|
||||
"name": FieldType.NAME,
|
||||
"phone": FieldType.PHONE,
|
||||
"price": FieldType.CURRENCY,
|
||||
},
|
||||
phone_country_column="country",
|
||||
currency_preserve_code=True,
|
||||
currency_decimal="auto",
|
||||
)
|
||||
|
||||
|
||||
class TestPhonesByRegion:
|
||||
def test_every_row_lands_on_correct_e164_prefix(self, df, options):
|
||||
# Each row's country column drives the per-row region used by
|
||||
# phonenumbers.parse — the correct + prefix is the acceptance bar.
|
||||
res = standardize_dataframe(df, options)
|
||||
out = res.standardized_df
|
||||
# ISO-2 → expected E.164 country code prefix
|
||||
prefix_for_country = {
|
||||
"US": "+1", "GB": "+44", "RU": "+7", "ES": "+34",
|
||||
"FR": "+33", "JP": "+81", "DE": "+49", "IT": "+39",
|
||||
"CN": "+86", "IN": "+91", "EG": "+20", "AU": "+61",
|
||||
"BR": "+55", "MX": "+52", "KR": "+82", "TR": "+90",
|
||||
"IL": "+972", "PL": "+48", "DK": "+45", "SE": "+46",
|
||||
}
|
||||
bad: list[tuple[str, str, str]] = []
|
||||
for _, row in out.iterrows():
|
||||
want = prefix_for_country[row["country"]]
|
||||
got = row["phone"]
|
||||
if not got.startswith(want):
|
||||
bad.append((row["country"], want, got))
|
||||
assert not bad, f"phone prefix mismatches: {bad}"
|
||||
|
||||
|
||||
class TestCurrencyByLocale:
|
||||
def test_eu_decimal_comma_resolves_under_auto(self, df, options):
|
||||
res = standardize_dataframe(df, options)
|
||||
# Spain, France, Germany, Italy, Brazil, Sweden all use decimal
|
||||
# comma. Verify a clean numeric result post-standardization.
|
||||
eu_idx = df.index[df["country"].isin(
|
||||
["ES", "FR", "DE", "IT", "BR", "SE"]
|
||||
)]
|
||||
for i in eu_idx:
|
||||
val = res.standardized_df.loc[i, "price"]
|
||||
# Either ``CODE NNN.NN`` or bare ``NNN.NN`` — but the comma
|
||||
# in the source must have become a dot in the output.
|
||||
assert "," not in val, (
|
||||
f"row {i} ({df.loc[i, 'country']}): comma persisted in {val!r}"
|
||||
)
|
||||
|
||||
def test_brl_real_prefix_recognised(self, df, options):
|
||||
res = standardize_dataframe(df, options)
|
||||
br_row = res.standardized_df[res.standardized_df["country"] == "BR"].iloc[0]
|
||||
assert "BRL" in br_row["price"]
|
||||
|
||||
|
||||
class TestStreamingMatchesInMemory:
|
||||
def test_same_output_via_streaming(self, tmp_path, df, options):
|
||||
# Streaming the same fixture through standardize_file should
|
||||
# produce a CSV byte-equivalent to the in-memory path.
|
||||
in_mem = standardize_dataframe(df, options).standardized_df
|
||||
out = tmp_path / "out.csv"
|
||||
# Use a chunk size that splits the 20-row fixture mid-way.
|
||||
res = standardize_file(FIXTURE, out, options, chunk_size=7)
|
||||
assert res.rows_processed == len(df)
|
||||
streamed = pd.read_csv(out, dtype=str, keep_default_na=False)
|
||||
# Compare typed columns only — others pass through.
|
||||
for col in options.column_types:
|
||||
assert streamed[col].tolist() == in_mem[col].astype(str).tolist(), (
|
||||
f"column {col} differs between in-memory and streaming"
|
||||
)
|
||||
Reference in New Issue
Block a user