cleanframe-engine 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/CHANGELOG.md +162 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/CONTRIBUTING.md +29 -3
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/PKG-INFO +40 -16
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/README.md +35 -15
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/SECURITY.md +13 -6
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/__init__.py +8 -0
- cleanframe_engine-0.4.0/cleanframe/_numparse.py +233 -0
- cleanframe_engine-0.4.0/cleanframe/_textparse.py +110 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/_util.py +71 -2
- cleanframe_engine-0.4.0/cleanframe/_version.py +1 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/api.py +110 -26
- cleanframe_engine-0.4.0/cleanframe/checks.py +127 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/cli.py +214 -22
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/codegen.py +202 -121
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/dataio.py +291 -6
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/categories.py +33 -2
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/currency.py +67 -14
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/dates.py +2 -4
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/dedup.py +11 -1
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/text.py +41 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/diff.py +27 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/drift.py +56 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/executor.py +32 -5
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/fingerprint.py +11 -7
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/llm.py +88 -25
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/ops.py +341 -155
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/planner.py +3 -1
- cleanframe_engine-0.4.0/cleanframe/plugins.py +87 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/profile.py +3 -27
- cleanframe_engine-0.4.0/cleanframe/readfix.py +310 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/recipe.py +30 -4
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/report.py +11 -6
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/streaming.py +253 -60
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/types.py +24 -4
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/validate.py +12 -86
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/workbook.py +37 -2
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/README.md +2 -1
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/api-reference.md +31 -11
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/architecture.md +10 -3
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/cli.md +32 -2
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/detectors-and-ops.md +28 -2
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/faq.md +3 -1
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/getting-started.md +1 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/installation.md +31 -2
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/llm.md +27 -11
- cleanframe_engine-0.4.0/docs/plugins.md +119 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/production.md +70 -24
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/recipe-spec.md +43 -18
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/pyproject.toml +6 -2
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_audit_hardening.py +8 -9
- cleanframe_engine-0.4.0/tests/test_cli_json_logging.py +63 -0
- cleanframe_engine-0.4.0/tests/test_docs_in_sync.py +49 -0
- cleanframe_engine-0.4.0/tests/test_fix_llm_trust.py +149 -0
- cleanframe_engine-0.4.0/tests/test_fix_numbers_and_parity.py +415 -0
- cleanframe_engine-0.4.0/tests/test_fix_reading.py +287 -0
- cleanframe_engine-0.4.0/tests/test_fix_streaming.py +148 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_llm_schema.py +2 -2
- cleanframe_engine-0.4.0/tests/test_plugins.py +149 -0
- cleanframe_engine-0.4.0/tests/test_properties.py +212 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_release_hardening.py +10 -0
- cleanframe_engine-0.4.0/tests/test_review_round2.py +257 -0
- cleanframe_engine-0.4.0/tests/test_unique_mapping.py +91 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave1_hardening.py +2 -3
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave2_vectorization.py +1 -2
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave5_streaming.py +5 -2
- cleanframe_engine-0.3.0/cleanframe/_version.py +0 -1
- cleanframe_engine-0.3.0/cleanframe/readfix.py +0 -160
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/.gitignore +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/LICENSE +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/__main__.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/__init__.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/base.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/contacts.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/nulls.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/outliers.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/schema_mapping.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/detectors/units.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/errors.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/issues.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/py.typed +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/quality.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/result.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/cleanframe/schema.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/concepts.md +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/docs/schema-spec.md +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/examples/customer.recipe.yaml +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/examples/customer.schema.yaml +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/examples/messy_customers.csv +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/conftest.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_api_report_cli.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_cross_platform.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_drift_codegen.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_executor_diff.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_ops.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_planner.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_production_safety.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_profile_detectors.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_recipe.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_validate.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave1_codegen.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave1_correctness.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave1_lineage.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave1_llm.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave2_scaling.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave3_selection.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave3_workbook.py +0 -0
- {cleanframe_engine-0.3.0 → cleanframe_engine-0.4.0}/tests/test_wave4_readfix.py +0 -0
|
@@ -7,6 +7,162 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] — 2026-09-30
|
|
11
|
+
|
|
12
|
+
A correctness release. An empirical review (26 hard-input probes, a 1M/3M-row benchmark and a
|
|
13
|
+
docs audit that ran every example) found places where CleanFrame produced a **wrong number, a
|
|
14
|
+
wrong merge or a wrong file with no warning**. Every one is fixed, each has a regression test, and
|
|
15
|
+
the invariants that should have caught them now have property-based tests.
|
|
16
|
+
|
|
17
|
+
### Fixed — silent wrong results
|
|
18
|
+
|
|
19
|
+
- **A new number format no longer replays silently.** Replaying a `.`-decimal recipe on `€1.200,50`
|
|
20
|
+
produced `1.2005`, exit 0, no warning. Numbers are now parsed by structure (below), and `apply_recipe`
|
|
21
|
+
raises a `number_format_drift` finding (exit 3) when digit-bearing values stop fitting the recipe's
|
|
22
|
+
`decimal`/`thousands` convention (a small share is INFO, so a stray `12ab34` is not a stop).
|
|
23
|
+
- **Signs were dropped.** `-$5`, `-€3.50`, `-₹500` and `$(8)` parsed as positives. `parse_number` now finds
|
|
24
|
+
the sign wherever it is written (`-5`, `-$5`, `$-5`, `5-`, `(5)`, `$(5)`, unicode minus).
|
|
25
|
+
- **`€2,5` became 25 and `€999,99` became 99999** whenever one value in the column looked US-grouped.
|
|
26
|
+
Grouping is now validated (3-digit or Indian lakh) and the currency detector infers comma-decimal only
|
|
27
|
+
from unambiguous evidence; values that fit neither convention become a counted, warned null.
|
|
28
|
+
- **The category detector merged different numbers**, in every mode including `strict`: `-1,200`→`1,200`,
|
|
29
|
+
`12.00`→`1,200`, `inf`→`-inf`, `A1`→`A-1`. Values that differ in sign, decimal or grouping are never one category.
|
|
30
|
+
- **`normalize_unit` read `1,500 g` as 1.5 g** (1000x off). Ambiguous numbers are refused (null + warning);
|
|
31
|
+
`1,5 kg` and `0.500 kg` still parse, and `1.5e3 g` now does too.
|
|
32
|
+
- **`cast: int` rounded IDs above 2**53** through float64; integer text is now converted exactly.
|
|
33
|
+
- **The cell diff missed precision loss** (`9007199254740993` → `...992.0`) and `1` → `True`, because numpy
|
|
34
|
+
compared `int64` with `float64` after promotion. Both are now reported.
|
|
35
|
+
- **`R$`, `A$`, `C$`, `HK$`, `NZ$`, `S$` were all labelled USD**: symbols are matched longest-first, and `₺ ₫ ₱ ₪ ฿` were added.
|
|
36
|
+
- **Auto-dedup treated `1`, `True` and `1.0` as the same value**, and collapsed a one-column frame to its
|
|
37
|
+
distinct values. Booleans stay distinct from numbers, and one-column frames are reported, not deduplicated.
|
|
38
|
+
- **Offset timestamps were shifted to the UTC calendar day** (`2024-01-01T02:00+05:30` → `2023-12-31`); a
|
|
39
|
+
single-offset column now keeps its wall-clock date.
|
|
40
|
+
- **`title_case` produced `3Rd Street` and `Don'T`**, and **`normalize_phone` turned `0044…` into `+91442…` and `12345` into
|
|
41
|
+
`+9112345`**. Ordinals and apostrophes are handled; `00` is read as `+`; numbers too short to be phones never get a country code invented.
|
|
42
|
+
- **Any non-UTF-8 file was accepted as latin-1** (Shift-JIS became mojibake with exit 0). The encoding ladder
|
|
43
|
+
can now refuse: a file that is neither UTF-8, cp1252 nor plausibly a single-byte Latin text raises with the likely
|
|
44
|
+
encodings and `--encoding` advice.
|
|
45
|
+
- **`read_frame` silently truncated `a\x00b` to `a`**; it now raises like `clean`.
|
|
46
|
+
- **Generated code diverged from the executor**: `05/01/2024` read as Jan 5 instead of May 1, offset timestamps
|
|
47
|
+
crashed it, `to_na case_insensitive: false` was ignored, `normalize_values` skipped non-string cells, integer column
|
|
48
|
+
labels and missing columns raised `KeyError`. Exported pandas now **embeds the executor's own helper source**
|
|
49
|
+
for numbers, units, ints, dates and title case, and skips missing columns like the executor.
|
|
50
|
+
- **`stream_apply` leaked raw pandas errors** on a late chunk that did not fit the recorded dtypes (exit 70), and its
|
|
51
|
+
output could differ from `apply_recipe` (`2` vs `2.0`, drift missed beyond the first 200 rows). It now makes a first
|
|
52
|
+
bounded-memory pass, checks drift across every row, and writes byte-identical output or refuses with a named error.
|
|
53
|
+
Streaming also stops writing a quarantine file nobody asked for.
|
|
54
|
+
- **`suggest --update` "learned" a competing reading of dates that already parse** (`03/03/2026` → `%m/%d/%Y`) and could
|
|
55
|
+
not fix the README's own `Jan 5, 26` example. It now learns only from values the recipe cannot parse.
|
|
56
|
+
- **YAML 1.1 traps in hand-written recipes.** `yes:`, `no:`, `null:`, `010:`, `12:30:` keys were read as `True`, `False`,
|
|
57
|
+
`None`, `8`, `750` and the column was skipped with only a warning. Plain keys now stay text; `to_na: [no, null]` is refused
|
|
58
|
+
with a "quote it" hint.
|
|
59
|
+
- **`cleanframe clean` silently overwrote a hand-edited recipe.** It now refuses to replace a recipe that differs from
|
|
60
|
+
the one just planned unless `--overwrite` is given (re-running over unchanged input is still fine).
|
|
61
|
+
- `Recipe.from_yaml(r.to_yaml()) == r` was `False` for every generated recipe: equality now compares the canonical serialisation.
|
|
62
|
+
- **Money in `1.234` shape is never guessed.** With no other evidence `€1.234` could be 1234 (German) or 1.234 (Irish); the currency detector now reports `ambiguous_number_format` and proposes nothing, instead of picking one and being 1000x wrong.
|
|
63
|
+
- **Currency codes:** an explicit code beats a symbol (`$5 MXN` is pesos), `$`-family symbols glued to letters are no longer mislabelled (`AR$`, `MX$`, `NT$`, `CL$`, `CO$` added; an unknown `XY$5` is "no currency", not USD), `Rs` is INR, and more ISO codes are recognised.
|
|
64
|
+
- **`normalize_unit` recognises the spellings people write** (`lbs`, `pounds`, `kgs`, `ounces`, `inches`, `feet`, `gallons`, …).
|
|
65
|
+
- **A deliberate `to_na` was reported as "values became missing because an op could not parse them"**; it is now logged as intentional and only real parse failures warn.
|
|
66
|
+
- **A derived column could be named `None_currency`** when `extract_currency` followed an op that returned an unnamed Series; op results keep the column name.
|
|
67
|
+
- **`stream_apply` silently dropped the extra field of a row wider than the header** (pandas' chunked reader truncates it); streaming now refuses such a file, as `apply_recipe` does.
|
|
68
|
+
- `dedup` / `drop_columns` accept numeric-looking column names (`subset: [2024]`); `on_fail: null` explains the YAML trap; the Excel writer warns when it removes illegal control characters; `read_frame` on a `;`-delimited file with commas in the text says "semicolon-delimited" instead of suggesting `header_row`; a lone `-`/`+` is no longer formula-escaped; a percent or currency symbol keeps `10 %` distinct from `10` in category clustering.
|
|
69
|
+
|
|
70
|
+
### Security & trust
|
|
71
|
+
|
|
72
|
+
- **`OPENAI_API_KEY` is no longer sent to other providers.** Each provider reads only its own key variable.
|
|
73
|
+
- **LLM plans can never contain `fill_na` or `drop_columns`** (any mode); they are stripped with a warning, closing a
|
|
74
|
+
hole in the "nothing silently imputed or dropped" invariant. An LLM recipe is also dry-run on the first rows and falls back to the rules planner if it cannot run.
|
|
75
|
+
- `sample` exposure is documented as what it is (values partly redacted, no approval step) and warns which columns are sent; `llm_exposure` typos raise a clear error; `max_tokens_budget` without `llm=` warns.
|
|
76
|
+
- ReDoS-prone `replace` patterns and regex back-references to missing groups are refused **at load**.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **Plugins that travel.** `cleanframe.plugins`: entry points (`cleanframe.plugins` group), `CLEANFRAME_PLUGINS`,
|
|
81
|
+
`--plugin MODULE`, `cf.load_plugins()`, and lazy discovery when a recipe names an unknown op.
|
|
82
|
+
`register_op(codegen=, helpers=, streamable=)` lets a custom op export as standalone pandas and stream (default-deny). See the new plugin guide.
|
|
83
|
+
- **`--json`**: one machine-readable summary object on stdout (human output to stderr) for every command, on success and failure — including usage errors (`status: usage_error`, exit 2), and `ops`/`detectors` list themselves. A workbook `apply --chunksize` is now refused instead of ignored.
|
|
84
|
+
- **`normalize_unicode` op + `unicode` detector**: NFC-normalises text and removes zero-width spaces, word joiners, BOMs and non-breaking spaces (zero-width joiners are kept). It fixes `café` typed two ways splitting a category, and invisible characters that `strip` leaves behind.
|
|
85
|
+
- **Logging**: the library logs through `logging.getLogger("cleanframe")` (silent `NullHandler`); `--verbose` enables INFO.
|
|
86
|
+
- **`header_row=` / `--header-row`** for files with title rows above the header, with a suggested value when the header looks misplaced; read-time warnings for ragged rows and `Total` footer rows; hidden Excel sheets stay hidden on write-back; warnings for uncached formulas and merged cells.
|
|
87
|
+
- **Property-based tests (Hypothesis)** for the parser, generated-code parity, determinism, recipe round-trip, "nothing silently dropped" and streaming parity; `scripts/sync_wiki.py` + a test that keeps `docs/` and `wiki/` identical.
|
|
88
|
+
- More date formats (`%b %d, %y`, `%B %d, %y`, `%d %b, %Y`, `%d.%m.%y`, `%Y.%m.%d`) — appended, so existing recipes are unaffected.
|
|
89
|
+
- A `detect` extra (`charset-normalizer`) that ranks candidate encodings in the refusal message.
|
|
90
|
+
- Unknown `options=` keys warn with a did-you-mean.
|
|
91
|
+
|
|
92
|
+
### Performance
|
|
93
|
+
|
|
94
|
+
- Pure text ops (`parse_number`, `normalize_unit`, `normalize_phone`, `normalize_values`, `extract_currency`, explicit-format `parse_date`, and the email/url/phone/regex validators) now run **once per distinct value** of an all-string column and fan the results out — identical output, 15-25x faster on repetitive columns (1M rows: `parse_number` 4.5 s → 0.17 s, `normalize_unit` 3.8 s → 0.26 s). Measured end to end on a realistic 18-column, 1M-row file: `clean()` 42 s → 29 s, replay 28 s → 16 s.
|
|
95
|
+
- Peak memory is unchanged and is now documented honestly: the frame plus the op-touched columns, about 2x when most columns are cleaned. Use `stream_apply` for row-independent recipes on files that do not fit.
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
|
|
99
|
+
- **Behaviour changes worth knowing when upgrading:**
|
|
100
|
+
- Numbers with invalid or ambiguous grouping now become a warned null instead of a guess (`2,5` under `thousands=","`, `1,500 g`).
|
|
101
|
+
- Streaming reads the file twice, and writes a quarantine file only when `quarantine_path` is given.
|
|
102
|
+
- Offset timestamps keep their wall-clock date; mixed-offset columns are converted to UTC.
|
|
103
|
+
- `title_case` no longer capitalises after apostrophes or digits; `normalize_phone` treats `00` as `+`.
|
|
104
|
+
- `to_na` tokens, `replace` pattern/repl and `parse_number` separators must be text; `version: true` and unknown option keys are rejected.
|
|
105
|
+
- `Recipe` defines `__eq__` on its canonical form (it was already unhashable).
|
|
106
|
+
- Streaming `apply` measured at ~270 MB peak for 600k rows at `chunksize=50_000` (docs updated with the baseline).
|
|
107
|
+
- The validation check registry moved into `cleanframe.checks` (internal; removes the last import cycle).
|
|
108
|
+
|
|
109
|
+
## [0.3.1] — 2026-09-04
|
|
110
|
+
|
|
111
|
+
First follow-up to the 0.3.0 release, from the CI and code-scanning results that
|
|
112
|
+
only appear once a project is public.
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- **`Severity` comparisons were alphabetical, not ordered.** Only `__lt__` was
|
|
117
|
+
defined, so the `str` mixin answered everything else: `Severity.ERROR >
|
|
118
|
+
Severity.INFO` returned `False` because `"error" < "info"` as text. All four
|
|
119
|
+
comparisons are now defined on the severity rank. Library code always used
|
|
120
|
+
`.rank` explicitly, so nothing internal was affected, but any caller comparing
|
|
121
|
+
severities directly got wrong answers. Found by CodeQL's incomplete-ordering
|
|
122
|
+
query.
|
|
123
|
+
- **`mypy` failed in CI** with `numpy/__init__.pyi:737: error: Type statement is
|
|
124
|
+
only supported in Python 3.12 and greater`. The `python_version = "3.10"` pin
|
|
125
|
+
also governs how dependency stubs are parsed, and numpy 2.5's stubs use syntax
|
|
126
|
+
that needs 3.12. Removed the pin; minimum-version support is still checked by
|
|
127
|
+
ruff's `target-version` and by the Python 3.10 test job.
|
|
128
|
+
- **The wiki sync workflow could not push.** GitHub Actions' built-in token can
|
|
129
|
+
clone a wiki but not write to it, so the job committed and then failed on
|
|
130
|
+
authentication. It now requires the `WIKI_TOKEN` secret and stops up front with
|
|
131
|
+
an explanatory message. `wiki/README.md` claimed no token was needed; corrected.
|
|
132
|
+
- Two module-level import cycles removed: `profile` with `ops`, and `recipe` with
|
|
133
|
+
`validate`. `COMMON_DATE_FORMATS` now lives in `ops`, which owns date parsing,
|
|
134
|
+
and is re-exported from `profile` so every existing import keeps working.
|
|
135
|
+
`validate` imports `ValidationRule` for annotations only.
|
|
136
|
+
- Empty exception handlers in `fingerprint` and `report` replaced with a helper
|
|
137
|
+
that returns whether a cell is missing, so the intent is in the code rather
|
|
138
|
+
than in a comment beside `pass`.
|
|
139
|
+
- Redundant function-level `import re` in the dates detector removed; it now
|
|
140
|
+
reuses the module's compiled pattern instead of recompiling per call.
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
|
|
144
|
+
- Workflow actions updated: checkout to v7, setup-python to v7, upload-artifact
|
|
145
|
+
to v7, download-artifact to v8, codeql-action to v4. Clears the Node 20
|
|
146
|
+
deprecation warnings.
|
|
147
|
+
- Dependabot groups the `github-actions` ecosystem, so action bumps arrive as one
|
|
148
|
+
pull request instead of one per action.
|
|
149
|
+
- CodeQL analyses the library and skips `tests`. Test fixtures are deliberately
|
|
150
|
+
hostile (catastrophic-backtracking patterns, injection payloads, handlers that
|
|
151
|
+
assert something must not raise) and are not part of the wheel, so reporting
|
|
152
|
+
them buries real findings. Configuration lives in
|
|
153
|
+
`.github/codeql/codeql-config.yml`.
|
|
154
|
+
- Protocol methods on `LLMClient` and `Planner` carry a docstring instead of a
|
|
155
|
+
bare `...`.
|
|
156
|
+
- Installation docs link the package page, show how to pin a version, and mention
|
|
157
|
+
`cleanframe --version`. `CHANGELOG.md` gained the Keep a Changelog link
|
|
158
|
+
definitions now that releases are tagged.
|
|
159
|
+
|
|
160
|
+
### Verified
|
|
161
|
+
|
|
162
|
+
335 tests pass on Python 3.10 with the declared minimum pins (pandas 1.5.3,
|
|
163
|
+
numpy 1.23.5), on Python 3.13 with pandas 2.3.3, and on Python 3.13 with pandas
|
|
164
|
+
3.0.5 and numpy 2.5.2, which is what a fresh `pip install` resolves today.
|
|
165
|
+
|
|
10
166
|
## [0.3.0] — 2026-09-04
|
|
11
167
|
|
|
12
168
|
Release-readiness pass driven by a full audit of the CLI, the Python API, packaging and
|
|
@@ -233,3 +389,9 @@ now fail loudly, and a few transforms that quietly corrupted data no longer run.
|
|
|
233
389
|
- Initial public release: profiler, detectors, rules + optional LLM planner, recipe YAML,
|
|
234
390
|
deterministic executor, validation/quarantine, cell-level diff, schema drift, HTML reports,
|
|
235
391
|
codegen, and CLI.
|
|
392
|
+
|
|
393
|
+
[Unreleased]: https://github.com/inboxpraveen/Cleanframe/compare/v0.3.1...HEAD
|
|
394
|
+
[0.3.1]: https://github.com/inboxpraveen/Cleanframe/releases/tag/v0.3.1
|
|
395
|
+
[0.3.0]: https://github.com/inboxpraveen/Cleanframe/releases/tag/v0.3.0
|
|
396
|
+
[0.2.0]: https://github.com/inboxpraveen/Cleanframe/commits/main
|
|
397
|
+
[0.1.0]: https://github.com/inboxpraveen/Cleanframe/commits/main
|
|
@@ -9,7 +9,7 @@ than no change at all. The rules below exist to protect it.
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
## The
|
|
12
|
+
## The six invariants (do not break these)
|
|
13
13
|
|
|
14
14
|
1. **Determinism.** The same input must always produce the same recipe, the same
|
|
15
15
|
cleaned frame, the same diff — on any machine, in any process. That means:
|
|
@@ -43,11 +43,25 @@ than no change at all. The rules below exist to protect it.
|
|
|
43
43
|
name it in the op's signature (or in `aliases=`) so the loader accepts it — a
|
|
44
44
|
parameter the loader silently ignores is a data-loss bug.
|
|
45
45
|
|
|
46
|
+
It applies to *guessing* as well: when a value is ambiguous (`1,500 g` — 1500 g or
|
|
47
|
+
1.5 g?) or malformed for the declared convention (`2,5` under `thousands=","`), the
|
|
48
|
+
result is a counted, warned null, never a plausible-looking number. Wrong numbers
|
|
49
|
+
are worse than missing ones. Likewise a clustering/merge rule must never fold together
|
|
50
|
+
values that differ in sign, decimal or grouping (`-300`/`300`).
|
|
51
|
+
|
|
46
52
|
5. **Every changed cell is tracked.** The executor assigns a stable row id and
|
|
47
53
|
tracks column lineage so `CellDiff` can attribute every change. If you add a
|
|
48
54
|
transform that adds/removes/reorders columns or rows, make sure the lineage in
|
|
49
55
|
`executor.py` stays correct and the diff still reconciles.
|
|
50
56
|
|
|
57
|
+
6. **Exported code equals the executor — because it is the same code.** The pandas that
|
|
58
|
+
`result.code` writes must produce exactly what `apply_recipe` produces. Do not
|
|
59
|
+
re-implement an op's logic in `codegen.py`: put shared scalar logic in a small
|
|
60
|
+
self-contained module (`_numparse.py`, `_textparse.py`) or an existing `ops.py` helper
|
|
61
|
+
and *embed its source* with `inspect.getsource`, as codegen already does. Two
|
|
62
|
+
implementations drift; one implementation cannot. `tests/test_properties.py` fuzzes
|
|
63
|
+
this, and `tests/test_fix_numbers_and_parity.py` pins the known cases.
|
|
64
|
+
|
|
51
65
|
If a change would weaken one of these, open an issue to discuss it first.
|
|
52
66
|
|
|
53
67
|
---
|
|
@@ -74,6 +88,9 @@ DataFrame ─▶ profile.py ─▶ detectors/ ─▶ planner.py ─▶ Recipe (r
|
|
|
74
88
|
renames, and — critically — orders ops via `OP_ORDER`.
|
|
75
89
|
- **`executor.py`** — replays a recipe in fixed phases with lineage tracking.
|
|
76
90
|
- **`recipe.py` / `schema.py`** — the durable, human-reviewable artifacts.
|
|
91
|
+
- **`checks.py`** — the validation check registry, plus the guard that rejects an
|
|
92
|
+
unknown check name at load time. It lives apart from `validate.py` so `recipe.py`
|
|
93
|
+
can use the guard without an import cycle.
|
|
77
94
|
- **`llm.py`** — optional; writes a recipe from metadata only.
|
|
78
95
|
|
|
79
96
|
The dependency direction is strictly downward (detectors import ops, planner
|
|
@@ -218,6 +235,14 @@ you.
|
|
|
218
235
|
- **Every PR keeps `pytest` green.** New behavior needs a new test.
|
|
219
236
|
- **Test the invariant, not just the happy path.** For anything order-sensitive, add
|
|
220
237
|
a determinism assertion. For a new op with params, add the round-trip.
|
|
238
|
+
- **Fuzz the invariants.** `tests/test_properties.py` uses [Hypothesis](https://hypothesis.readthedocs.io/)
|
|
239
|
+
to check parser totality/sign, generated-code == executor, determinism, recipe round-trip,
|
|
240
|
+
"nothing silently dropped" and streaming == whole-frame. CI runs a small deterministic
|
|
241
|
+
sample; to hunt for new bugs locally run
|
|
242
|
+
`CLEANFRAME_PROP_EXAMPLES=2000 CLEANFRAME_PROP_RANDOM=1 pytest tests/test_properties.py`.
|
|
243
|
+
If it finds one, add the minimal case as a normal example test as well.
|
|
244
|
+
- **New op?** Decide `codegen=` / `streamable=` deliberately (see the [plugin guide](docs/plugins.md));
|
|
245
|
+
both default to "refuse", which is safe.
|
|
221
246
|
- Prefer small, focused tests using the fixtures in `tests/conftest.py`.
|
|
222
247
|
- `ruff check cleanframe tests` and `mypy` must both pass — CI runs them.
|
|
223
248
|
|
|
@@ -227,8 +252,9 @@ you.
|
|
|
227
252
|
docstrings that explain *why* (the reader can see *what*).
|
|
228
253
|
- Target Python 3.10+. Keep the core dependencies minimal (pandas, numpy, pyyaml,
|
|
229
254
|
jinja2). Anything heavier goes in an optional extra in `pyproject.toml`.
|
|
230
|
-
- No `print` in library code — return data
|
|
231
|
-
|
|
255
|
+
- No `print` in library code — return data, add to the run `log`, or use
|
|
256
|
+
`logging.getLogger(__name__)` (the package installs a `NullHandler`). `print` is for the
|
|
257
|
+
CLI only, and under `--json` the CLI keeps stdout for the one JSON summary object.
|
|
232
258
|
|
|
233
259
|
## Pull requests
|
|
234
260
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cleanframe-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: The reproducible data-cleaning engine for Python. AI writes the recipe once; the recipe runs forever — deterministic, diffable, reviewable.
|
|
5
5
|
Project-URL: Homepage, https://github.com/inboxpraveen/Cleanframe
|
|
6
6
|
Project-URL: Repository, https://github.com/inboxpraveen/Cleanframe
|
|
@@ -29,10 +29,14 @@ Requires-Dist: python-dateutil>=2.8
|
|
|
29
29
|
Requires-Dist: pyyaml>=6.0
|
|
30
30
|
Provides-Extra: all
|
|
31
31
|
Requires-Dist: anthropic>=0.30; extra == 'all'
|
|
32
|
+
Requires-Dist: charset-normalizer>=3; extra == 'all'
|
|
32
33
|
Requires-Dist: openai>=1.0; extra == 'all'
|
|
33
34
|
Requires-Dist: openpyxl>=3.0; extra == 'all'
|
|
34
35
|
Requires-Dist: pyarrow>=12.0; extra == 'all'
|
|
36
|
+
Provides-Extra: detect
|
|
37
|
+
Requires-Dist: charset-normalizer>=3; extra == 'detect'
|
|
35
38
|
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: hypothesis>=6.80; extra == 'dev'
|
|
36
40
|
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
37
41
|
Requires-Dist: openpyxl>=3.0; extra == 'dev'
|
|
38
42
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
@@ -147,8 +151,10 @@ If the new file's schema drifted (a renamed column, a new currency format), Clea
|
|
|
147
151
|
```
|
|
148
152
|
⚠ Schema drift detected in new_customers.csv
|
|
149
153
|
• Column "Amt (INR)" is new — 94% match to recipe column "amount_inr"
|
|
150
|
-
• 312
|
|
151
|
-
|
|
154
|
+
• 312 value(s) in "signup_date" match no allowed date format (new: 'Jan 5, 26')
|
|
155
|
+
• 40 of 312 numeric value(s) in "Amount" do not fit the recipe's number format
|
|
156
|
+
(decimal '.', thousands ','; new: '€1.200,50')
|
|
157
|
+
Run `cleanframe suggest new_customers.csv --recipe <recipe.yaml> --update` to review a patch.
|
|
152
158
|
```
|
|
153
159
|
|
|
154
160
|
## Bring your own API key (or no key at all)
|
|
@@ -159,30 +165,34 @@ CleanFrame is LLM-optional and provider-agnostic.
|
|
|
159
165
|
|---|---|---|
|
|
160
166
|
| **Rules-only** (default) | Deterministic detectors + heuristics | Never |
|
|
161
167
|
| **Metadata** | LLM sees column names, dtypes, and *value patterns* (regex sketches) — never raw values | Only metadata |
|
|
162
|
-
| **Sample** | LLM sees
|
|
168
|
+
| **Sample** | LLM sees a small shuffled sample of *values*. Emails, phones and long strings are redacted; other short values (names, cities) are sent verbatim. Passing `llm_exposure="sample"` is the opt-in — there is no separate approval step | Sample values, per the redaction above |
|
|
163
169
|
| **Replay** | Saved recipes | Never — recipes need no LLM |
|
|
164
170
|
|
|
165
171
|
- Keys come from environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
|
|
166
|
-
`OPENROUTER_API_KEY`, `GROQ_API_KEY`, …). CleanFrame never stores
|
|
167
|
-
|
|
172
|
+
`OPENROUTER_API_KEY`, `GROQ_API_KEY`, …). CleanFrame never stores or logs them, and
|
|
173
|
+
each provider reads **only its own** variable — `OPENAI_API_KEY` is never sent to
|
|
174
|
+
another provider.
|
|
168
175
|
- Any provider that speaks OpenAI Chat Completions works out of the box —
|
|
169
176
|
Anthropic (native), OpenAI, OpenRouter, Groq, Together, Fireworks, DeepSeek,
|
|
170
177
|
Mistral, Google Gemini, xAI, Perplexity, Cohere, plus local Ollama / LM Studio
|
|
171
178
|
or any custom `OPENAI_BASE_URL`. Spec format: `provider/model`
|
|
172
179
|
(e.g. `openrouter/anthropic/claude-sonnet-4`, `groq/llama-3.3-70b-versatile`).
|
|
173
|
-
-
|
|
180
|
+
- Cost cap: `cf.clean(..., max_tokens_budget=50_000)` is checked before each call (against an
|
|
181
|
+
estimate) and again against the usage the provider reports — leave headroom, since a call
|
|
182
|
+
that overshoots has already been billed.
|
|
174
183
|
- Fully offline / air-gapped operation is a supported first-class mode, not a degraded one.
|
|
175
184
|
|
|
176
185
|
## What it cleans
|
|
177
186
|
|
|
178
187
|
| Problem | Example |
|
|
179
188
|
|---|---|
|
|
189
|
+
| Invisible characters | zero-width spaces, BOMs, non-breaking spaces and decomposed accents (`café` typed two ways) → one normalised form |
|
|
180
190
|
| Column name chaos | `Cust Name`, `customer_name`, `CustomerName` → `customer_name` |
|
|
181
191
|
| Date formats | `12/01/24`, `1 Jan 2024`, `2024-01-01` → ISO dates |
|
|
182
|
-
| Currency & numbers | `₹1,20,000`, `$1,200`, `1200 INR` → typed floats + currency column |
|
|
183
|
-
| Category variants | `Bengaluru` / `
|
|
184
|
-
| Emails & phones | validation, normalization, country
|
|
185
|
-
| Duplicates | exact
|
|
192
|
+
| Currency & numbers | `₹1,20,000`, `$1,200`, `-$5`, `(1,200)`, `1.234,56`, `1200 INR` → typed floats (+ a currency column); a value whose grouping is invalid or ambiguous becomes a *counted, warned* null, never a guess |
|
|
193
|
+
| Category variants | `Bengaluru` / `bengaluru ` / `BENGALURU` and typos (`Banglore`) → one canonical value; abbreviations such as `BLR` need a seed map or an LLM |
|
|
194
|
+
| Emails & phones | validation, normalization, `00`/`+` international prefixes, optional default country code |
|
|
195
|
+
| Duplicates | exact duplicates are dropped (tracked in the diff); fuzzy near-duplicates and key collisions are *reported* with keep/drop proposals for you to act on |
|
|
186
196
|
| Missing values | detected and explained; strategies proposed, never silently applied |
|
|
187
197
|
| Units | `5kg`, `5000 g`, `5 KG` → normalized |
|
|
188
198
|
| Schema mapping | messy file → your target schema, with confidence scores |
|
|
@@ -196,14 +206,18 @@ CleanFrame is built for pipelines, not just demos:
|
|
|
196
206
|
- **Multi-sheet workbooks** — clean every Excel tab into one reviewable recipe; write-back refuses to overwrite the source in place
|
|
197
207
|
- **Out-of-core streaming** — replay row-independent recipes over larger-than-RAM CSVs at chunk-bounded memory
|
|
198
208
|
- **Format auto-detection** — encoding (utf-8 → cp1252) and delimiter sniffed at read time, pinned into the recipe for replay
|
|
199
|
-
- **Bounded memory** — detector sampling (50k values) + capped cell-diff detail (100k); the diff snapshots
|
|
209
|
+
- **Bounded memory** — detector sampling (50k values) + capped cell-diff detail (100k); the diff snapshots the op-touched columns (peak is the frame plus those columns — about 2× when most columns are cleaned; use streaming for files that do not fit)
|
|
200
210
|
- **Safe CSV exports** — spreadsheet formula injection escaped by default
|
|
201
211
|
- **Regex guards** — oversized / nested-quantifier patterns rejected
|
|
202
212
|
- **Visible degradation** — missing columns, unparseable values and LLM fallbacks warn instead of failing silently
|
|
203
213
|
- **Quarantine, don't delete** — validation failures are held with reasons
|
|
204
214
|
- **Verbatim reads** — `text=True` keeps leading zeros, literal `NA` and `1e5` exactly as the file has them; otherwise CleanFrame warns about what pandas' type inference changed on read
|
|
205
215
|
- **Never overwrites its input** — writing output over the source file takes an explicit `--overwrite`, and every write lands via a temporary file
|
|
206
|
-
- **Scriptable exits** — distinct exit codes for usage errors, data errors, drift stops and validation failures
|
|
216
|
+
- **Scriptable exits** — distinct exit codes for usage errors, data errors, drift stops and validation failures; `--json` prints one machine-readable summary on stdout
|
|
217
|
+
- **Number-format drift** — a file whose numbers stop fitting the recipe (`€1.200,50` against a `.`-decimal recipe) stops the replay instead of yielding `1.2005`
|
|
218
|
+
- **Strict recipes** — unknown ops/params, duplicate keys and bad regexes fail at *load*; plain YAML keys like `yes`, `010` or `12:30` stay column names
|
|
219
|
+
- **Generated code = executor** — exported pandas embeds the executor's own parsers, and property-based tests check they agree
|
|
220
|
+
- **Plugins that travel** — custom ops/detectors load from entry points, `--plugin` or `CLEANFRAME_PLUGINS`, and can opt in to code export and streaming
|
|
207
221
|
|
|
208
222
|
See the [Production Guide](https://github.com/inboxpraveen/Cleanframe/wiki/Production-Guide).
|
|
209
223
|
|
|
@@ -271,13 +285,21 @@ Profiler ─→ Issue Detectors ─→ Planner ──────────→
|
|
|
271
285
|
Validator · Cell-diff · HTML report
|
|
272
286
|
```
|
|
273
287
|
|
|
274
|
-
Detectors are plugins — write your own in ~30 lines:
|
|
288
|
+
Detectors and ops are plugins — write your own in ~30 lines:
|
|
275
289
|
|
|
276
290
|
```python
|
|
291
|
+
@cf.register_op("normalize_iban", streamable=True,
|
|
292
|
+
codegen=lambda p, col: [f"df[{col!r}] = df[{col!r}].str.replace(' ', '').str.upper()"])
|
|
293
|
+
def normalize_iban(series: pd.Series) -> pd.Series:
|
|
294
|
+
return series.str.replace(" ", "").str.upper()
|
|
295
|
+
|
|
277
296
|
@cf.detector("iban")
|
|
278
|
-
def detect_iban(series: pd.Series) -> cf.Issues: ...
|
|
297
|
+
def detect_iban(series: pd.Series) -> cf.Issues: ... # propose cf.Op("normalize_iban")
|
|
279
298
|
```
|
|
280
299
|
|
|
300
|
+
Package it under the `cleanframe.plugins` entry-point group and `cleanframe apply` in CI
|
|
301
|
+
finds it — see the [plugin guide](https://github.com/inboxpraveen/Cleanframe/wiki/Plugins).
|
|
302
|
+
|
|
281
303
|
## Install
|
|
282
304
|
|
|
283
305
|
```bash
|
|
@@ -291,7 +313,7 @@ pip install "cleanframe-engine[all]"
|
|
|
291
313
|
pip install "cleanframe-engine @ git+https://github.com/inboxpraveen/Cleanframe"
|
|
292
314
|
```
|
|
293
315
|
|
|
294
|
-
Python 3.10+. The distribution is `cleanframe-engine
|
|
316
|
+
Python 3.10+. The distribution is [`cleanframe-engine`](https://pypi.org/project/cleanframe-engine/); the import package is
|
|
295
317
|
`cleanframe` (`import cleanframe as cf`), and the CLI is `cleanframe` (or
|
|
296
318
|
`python -m cleanframe`).
|
|
297
319
|
|
|
@@ -305,6 +327,8 @@ Python 3.10+. The distribution is `cleanframe-engine`; the import package is
|
|
|
305
327
|
- [x] Selective ingestion (sheet / columns / rows)
|
|
306
328
|
- [x] Out-of-core streaming replay (larger-than-RAM CSVs)
|
|
307
329
|
- [x] Read-time format auto-correction (encoding + delimiter)
|
|
330
|
+
- [x] Plugin discovery; custom ops that export and stream
|
|
331
|
+
- [x] `--json` run summaries, library logging, property-based invariant tests
|
|
308
332
|
- [ ] `MessyData-100` public benchmark + leaderboard
|
|
309
333
|
- [ ] pandera / GX / dbt exporters
|
|
310
334
|
- [ ] Polars backend
|
|
@@ -97,8 +97,10 @@ If the new file's schema drifted (a renamed column, a new currency format), Clea
|
|
|
97
97
|
```
|
|
98
98
|
⚠ Schema drift detected in new_customers.csv
|
|
99
99
|
• Column "Amt (INR)" is new — 94% match to recipe column "amount_inr"
|
|
100
|
-
• 312
|
|
101
|
-
|
|
100
|
+
• 312 value(s) in "signup_date" match no allowed date format (new: 'Jan 5, 26')
|
|
101
|
+
• 40 of 312 numeric value(s) in "Amount" do not fit the recipe's number format
|
|
102
|
+
(decimal '.', thousands ','; new: '€1.200,50')
|
|
103
|
+
Run `cleanframe suggest new_customers.csv --recipe <recipe.yaml> --update` to review a patch.
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
## Bring your own API key (or no key at all)
|
|
@@ -109,30 +111,34 @@ CleanFrame is LLM-optional and provider-agnostic.
|
|
|
109
111
|
|---|---|---|
|
|
110
112
|
| **Rules-only** (default) | Deterministic detectors + heuristics | Never |
|
|
111
113
|
| **Metadata** | LLM sees column names, dtypes, and *value patterns* (regex sketches) — never raw values | Only metadata |
|
|
112
|
-
| **Sample** | LLM sees
|
|
114
|
+
| **Sample** | LLM sees a small shuffled sample of *values*. Emails, phones and long strings are redacted; other short values (names, cities) are sent verbatim. Passing `llm_exposure="sample"` is the opt-in — there is no separate approval step | Sample values, per the redaction above |
|
|
113
115
|
| **Replay** | Saved recipes | Never — recipes need no LLM |
|
|
114
116
|
|
|
115
117
|
- Keys come from environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
|
|
116
|
-
`OPENROUTER_API_KEY`, `GROQ_API_KEY`, …). CleanFrame never stores
|
|
117
|
-
|
|
118
|
+
`OPENROUTER_API_KEY`, `GROQ_API_KEY`, …). CleanFrame never stores or logs them, and
|
|
119
|
+
each provider reads **only its own** variable — `OPENAI_API_KEY` is never sent to
|
|
120
|
+
another provider.
|
|
118
121
|
- Any provider that speaks OpenAI Chat Completions works out of the box —
|
|
119
122
|
Anthropic (native), OpenAI, OpenRouter, Groq, Together, Fireworks, DeepSeek,
|
|
120
123
|
Mistral, Google Gemini, xAI, Perplexity, Cohere, plus local Ollama / LM Studio
|
|
121
124
|
or any custom `OPENAI_BASE_URL`. Spec format: `provider/model`
|
|
122
125
|
(e.g. `openrouter/anthropic/claude-sonnet-4`, `groq/llama-3.3-70b-versatile`).
|
|
123
|
-
-
|
|
126
|
+
- Cost cap: `cf.clean(..., max_tokens_budget=50_000)` is checked before each call (against an
|
|
127
|
+
estimate) and again against the usage the provider reports — leave headroom, since a call
|
|
128
|
+
that overshoots has already been billed.
|
|
124
129
|
- Fully offline / air-gapped operation is a supported first-class mode, not a degraded one.
|
|
125
130
|
|
|
126
131
|
## What it cleans
|
|
127
132
|
|
|
128
133
|
| Problem | Example |
|
|
129
134
|
|---|---|
|
|
135
|
+
| Invisible characters | zero-width spaces, BOMs, non-breaking spaces and decomposed accents (`café` typed two ways) → one normalised form |
|
|
130
136
|
| Column name chaos | `Cust Name`, `customer_name`, `CustomerName` → `customer_name` |
|
|
131
137
|
| Date formats | `12/01/24`, `1 Jan 2024`, `2024-01-01` → ISO dates |
|
|
132
|
-
| Currency & numbers | `₹1,20,000`, `$1,200`, `1200 INR` → typed floats + currency column |
|
|
133
|
-
| Category variants | `Bengaluru` / `
|
|
134
|
-
| Emails & phones | validation, normalization, country
|
|
135
|
-
| Duplicates | exact
|
|
138
|
+
| Currency & numbers | `₹1,20,000`, `$1,200`, `-$5`, `(1,200)`, `1.234,56`, `1200 INR` → typed floats (+ a currency column); a value whose grouping is invalid or ambiguous becomes a *counted, warned* null, never a guess |
|
|
139
|
+
| Category variants | `Bengaluru` / `bengaluru ` / `BENGALURU` and typos (`Banglore`) → one canonical value; abbreviations such as `BLR` need a seed map or an LLM |
|
|
140
|
+
| Emails & phones | validation, normalization, `00`/`+` international prefixes, optional default country code |
|
|
141
|
+
| Duplicates | exact duplicates are dropped (tracked in the diff); fuzzy near-duplicates and key collisions are *reported* with keep/drop proposals for you to act on |
|
|
136
142
|
| Missing values | detected and explained; strategies proposed, never silently applied |
|
|
137
143
|
| Units | `5kg`, `5000 g`, `5 KG` → normalized |
|
|
138
144
|
| Schema mapping | messy file → your target schema, with confidence scores |
|
|
@@ -146,14 +152,18 @@ CleanFrame is built for pipelines, not just demos:
|
|
|
146
152
|
- **Multi-sheet workbooks** — clean every Excel tab into one reviewable recipe; write-back refuses to overwrite the source in place
|
|
147
153
|
- **Out-of-core streaming** — replay row-independent recipes over larger-than-RAM CSVs at chunk-bounded memory
|
|
148
154
|
- **Format auto-detection** — encoding (utf-8 → cp1252) and delimiter sniffed at read time, pinned into the recipe for replay
|
|
149
|
-
- **Bounded memory** — detector sampling (50k values) + capped cell-diff detail (100k); the diff snapshots
|
|
155
|
+
- **Bounded memory** — detector sampling (50k values) + capped cell-diff detail (100k); the diff snapshots the op-touched columns (peak is the frame plus those columns — about 2× when most columns are cleaned; use streaming for files that do not fit)
|
|
150
156
|
- **Safe CSV exports** — spreadsheet formula injection escaped by default
|
|
151
157
|
- **Regex guards** — oversized / nested-quantifier patterns rejected
|
|
152
158
|
- **Visible degradation** — missing columns, unparseable values and LLM fallbacks warn instead of failing silently
|
|
153
159
|
- **Quarantine, don't delete** — validation failures are held with reasons
|
|
154
160
|
- **Verbatim reads** — `text=True` keeps leading zeros, literal `NA` and `1e5` exactly as the file has them; otherwise CleanFrame warns about what pandas' type inference changed on read
|
|
155
161
|
- **Never overwrites its input** — writing output over the source file takes an explicit `--overwrite`, and every write lands via a temporary file
|
|
156
|
-
- **Scriptable exits** — distinct exit codes for usage errors, data errors, drift stops and validation failures
|
|
162
|
+
- **Scriptable exits** — distinct exit codes for usage errors, data errors, drift stops and validation failures; `--json` prints one machine-readable summary on stdout
|
|
163
|
+
- **Number-format drift** — a file whose numbers stop fitting the recipe (`€1.200,50` against a `.`-decimal recipe) stops the replay instead of yielding `1.2005`
|
|
164
|
+
- **Strict recipes** — unknown ops/params, duplicate keys and bad regexes fail at *load*; plain YAML keys like `yes`, `010` or `12:30` stay column names
|
|
165
|
+
- **Generated code = executor** — exported pandas embeds the executor's own parsers, and property-based tests check they agree
|
|
166
|
+
- **Plugins that travel** — custom ops/detectors load from entry points, `--plugin` or `CLEANFRAME_PLUGINS`, and can opt in to code export and streaming
|
|
157
167
|
|
|
158
168
|
See the [Production Guide](https://github.com/inboxpraveen/Cleanframe/wiki/Production-Guide).
|
|
159
169
|
|
|
@@ -221,13 +231,21 @@ Profiler ─→ Issue Detectors ─→ Planner ──────────→
|
|
|
221
231
|
Validator · Cell-diff · HTML report
|
|
222
232
|
```
|
|
223
233
|
|
|
224
|
-
Detectors are plugins — write your own in ~30 lines:
|
|
234
|
+
Detectors and ops are plugins — write your own in ~30 lines:
|
|
225
235
|
|
|
226
236
|
```python
|
|
237
|
+
@cf.register_op("normalize_iban", streamable=True,
|
|
238
|
+
codegen=lambda p, col: [f"df[{col!r}] = df[{col!r}].str.replace(' ', '').str.upper()"])
|
|
239
|
+
def normalize_iban(series: pd.Series) -> pd.Series:
|
|
240
|
+
return series.str.replace(" ", "").str.upper()
|
|
241
|
+
|
|
227
242
|
@cf.detector("iban")
|
|
228
|
-
def detect_iban(series: pd.Series) -> cf.Issues: ...
|
|
243
|
+
def detect_iban(series: pd.Series) -> cf.Issues: ... # propose cf.Op("normalize_iban")
|
|
229
244
|
```
|
|
230
245
|
|
|
246
|
+
Package it under the `cleanframe.plugins` entry-point group and `cleanframe apply` in CI
|
|
247
|
+
finds it — see the [plugin guide](https://github.com/inboxpraveen/Cleanframe/wiki/Plugins).
|
|
248
|
+
|
|
231
249
|
## Install
|
|
232
250
|
|
|
233
251
|
```bash
|
|
@@ -241,7 +259,7 @@ pip install "cleanframe-engine[all]"
|
|
|
241
259
|
pip install "cleanframe-engine @ git+https://github.com/inboxpraveen/Cleanframe"
|
|
242
260
|
```
|
|
243
261
|
|
|
244
|
-
Python 3.10+. The distribution is `cleanframe-engine
|
|
262
|
+
Python 3.10+. The distribution is [`cleanframe-engine`](https://pypi.org/project/cleanframe-engine/); the import package is
|
|
245
263
|
`cleanframe` (`import cleanframe as cf`), and the CLI is `cleanframe` (or
|
|
246
264
|
`python -m cleanframe`).
|
|
247
265
|
|
|
@@ -255,6 +273,8 @@ Python 3.10+. The distribution is `cleanframe-engine`; the import package is
|
|
|
255
273
|
- [x] Selective ingestion (sheet / columns / rows)
|
|
256
274
|
- [x] Out-of-core streaming replay (larger-than-RAM CSVs)
|
|
257
275
|
- [x] Read-time format auto-correction (encoding + delimiter)
|
|
276
|
+
- [x] Plugin discovery; custom ops that export and stream
|
|
277
|
+
- [x] `--json` run summaries, library logging, property-based invariant tests
|
|
258
278
|
- [ ] `MessyData-100` public benchmark + leaderboard
|
|
259
279
|
- [ ] pandera / GX / dbt exporters
|
|
260
280
|
- [ ] Polars backend
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
| Version | Supported |
|
|
6
6
|
|---------|-----------|
|
|
7
|
-
| 0.
|
|
8
|
-
| 0.
|
|
9
|
-
| 0.
|
|
7
|
+
| 0.4.x | Yes |
|
|
8
|
+
| 0.3.x | Security fixes only |
|
|
9
|
+
| < 0.3 | No (never published under the `cleanframe-engine` name) |
|
|
10
10
|
|
|
11
11
|
## Reporting a vulnerability
|
|
12
12
|
|
|
@@ -28,15 +28,17 @@ and disclosure timeline with you.
|
|
|
28
28
|
## Design guarantees relevant to security
|
|
29
29
|
|
|
30
30
|
- **No `eval` / `exec`** in the library path. Recipes are data, not code.
|
|
31
|
-
- Recipes and schemas are loaded with `
|
|
31
|
+
- Recipes and schemas are loaded with a `SafeLoader` subclass (no arbitrary Python object construction).
|
|
32
32
|
- HTML reports use Jinja2 autoescaping.
|
|
33
|
-
- The LLM planner never receives raw cell values in the default `metadata` exposure
|
|
33
|
+
- The LLM planner never receives raw cell values in the default `metadata` exposure, and each
|
|
34
|
+
provider reads only its own API-key environment variable (an `OPENAI_API_KEY` is never sent to another provider).
|
|
35
|
+
- An LLM-authored recipe can never contain `fill_na` or `drop_columns`; they are stripped with a warning.
|
|
34
36
|
- CSV/Excel exports sanitise formula-like cells **and header labels** (`=`, `@`, and
|
|
35
37
|
`+`/`-` followed by anything that is not a plain number) by default. A signed
|
|
36
38
|
number is left alone so normalised phone numbers and negative amounts survive.
|
|
37
39
|
- Generated standalone pandas escapes column names and validation labels, so a
|
|
38
40
|
crafted CSV header cannot inject code into the exported module.
|
|
39
|
-
- User-supplied regex patterns in recipes are length- and complexity-bounded.
|
|
41
|
+
- User-supplied regex patterns in recipes are length- and complexity-bounded, and refused at load time.
|
|
40
42
|
- Recipes and schemas reject duplicate YAML keys instead of silently keeping the last.
|
|
41
43
|
- Cleaned output never overwrites its own input file without an explicit opt-in.
|
|
42
44
|
|
|
@@ -46,3 +48,8 @@ CleanFrame is a **data-cleaning library**, not a sandbox. Callers who pass untru
|
|
|
46
48
|
file paths, recipes, or schemas still control the host filesystem and process.
|
|
47
49
|
Treat recipe YAML from untrusted sources like any other untrusted config: review it
|
|
48
50
|
before applying it to production data.
|
|
51
|
+
|
|
52
|
+
**Plugins run code.** `cleanframe.plugins` entry points, `CLEANFRAME_PLUGINS` and `--plugin MODULE` import Python
|
|
53
|
+
modules, exactly like installing a package. Only install plugins you trust; set `CLEANFRAME_NO_PLUGINS=1` (or
|
|
54
|
+
pass `--no-plugins`) to disable automatic discovery. A recipe cannot load a plugin by itself: it can only *name* an
|
|
55
|
+
op, which is resolved against the plugins already installed or explicitly requested.
|
|
@@ -13,6 +13,8 @@ input → same output, every time. See the README for the full tour.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import logging as _logging
|
|
17
|
+
|
|
16
18
|
from ._version import __version__
|
|
17
19
|
|
|
18
20
|
# -- high-level API --------------------------------------------------------
|
|
@@ -52,6 +54,7 @@ from .ops import list_ops, register_op
|
|
|
52
54
|
|
|
53
55
|
# -- planning / execution --------------------------------------------------
|
|
54
56
|
from .planner import Planner, RulesPlanner, plan_recipe
|
|
57
|
+
from .plugins import load_plugins
|
|
55
58
|
|
|
56
59
|
# -- profiling -------------------------------------------------------------
|
|
57
60
|
from .profile import ColumnProfile, DataFrameProfile, profile_dataframe
|
|
@@ -83,6 +86,10 @@ from .workbook import (
|
|
|
83
86
|
read_workbook,
|
|
84
87
|
)
|
|
85
88
|
|
|
89
|
+
# A library must not configure logging. Applications opt in with
|
|
90
|
+
# ``logging.getLogger("cleanframe").setLevel(...)`` (or the CLI's --verbose).
|
|
91
|
+
_logging.getLogger("cleanframe").addHandler(_logging.NullHandler())
|
|
92
|
+
|
|
86
93
|
__all__ = [
|
|
87
94
|
"__version__",
|
|
88
95
|
# api
|
|
@@ -103,6 +110,7 @@ __all__ = [
|
|
|
103
110
|
"detector",
|
|
104
111
|
"validator",
|
|
105
112
|
"register_op",
|
|
113
|
+
"load_plugins",
|
|
106
114
|
"DetectorContext",
|
|
107
115
|
"run_detectors",
|
|
108
116
|
"list_detectors",
|