truero 0.2.1__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.
Files changed (41) hide show
  1. truero-0.2.1/LICENSE +16 -0
  2. truero-0.2.1/PKG-INFO +84 -0
  3. truero-0.2.1/README.md +36 -0
  4. truero-0.2.1/pyproject.toml +3 -0
  5. truero-0.2.1/setup.cfg +4 -0
  6. truero-0.2.1/setup.py +60 -0
  7. truero-0.2.1/tests/test_cleaning_checks.py +390 -0
  8. truero-0.2.1/tests/test_custom_rules.py +378 -0
  9. truero-0.2.1/tests/test_metadata.py +121 -0
  10. truero-0.2.1/tests/test_web_app.py +585 -0
  11. truero-0.2.1/tests/test_web_history.py +99 -0
  12. truero-0.2.1/truero/__init__.py +1 -0
  13. truero-0.2.1/truero/categories.py +111 -0
  14. truero-0.2.1/truero/category_registry.py +130 -0
  15. truero-0.2.1/truero/cleaning_engine.py +969 -0
  16. truero-0.2.1/truero/config.py +146 -0
  17. truero-0.2.1/truero/fixer.py +41 -0
  18. truero-0.2.1/truero/formatter.py +799 -0
  19. truero-0.2.1/truero/legal/legal_content.json +173 -0
  20. truero-0.2.1/truero/main.py +601 -0
  21. truero-0.2.1/truero/questionnaire.py +326 -0
  22. truero-0.2.1/truero/rule_catalog.py +230 -0
  23. truero-0.2.1/truero/rules_engine.py +373 -0
  24. truero-0.2.1/truero/rules_picker.py +276 -0
  25. truero-0.2.1/truero/rules_profile.py +122 -0
  26. truero-0.2.1/truero/runner.py +751 -0
  27. truero-0.2.1/truero/scanner.py +155 -0
  28. truero-0.2.1/truero/terms.py +194 -0
  29. truero-0.2.1/truero/uploader.py +88 -0
  30. truero-0.2.1/truero/web/__init__.py +0 -0
  31. truero-0.2.1/truero/web/static/app.js +741 -0
  32. truero-0.2.1/truero/web/static/index.html +303 -0
  33. truero-0.2.1/truero/web/static/styles.css +361 -0
  34. truero-0.2.1/truero/web/web_app.py +517 -0
  35. truero-0.2.1/truero/web/web_history.py +107 -0
  36. truero-0.2.1/truero.egg-info/PKG-INFO +84 -0
  37. truero-0.2.1/truero.egg-info/SOURCES.txt +39 -0
  38. truero-0.2.1/truero.egg-info/dependency_links.txt +1 -0
  39. truero-0.2.1/truero.egg-info/entry_points.txt +2 -0
  40. truero-0.2.1/truero.egg-info/requires.txt +8 -0
  41. truero-0.2.1/truero.egg-info/top_level.txt +1 -0
truero-0.2.1/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright (c) 2026 Truero.ai Limited. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are the
4
+ proprietary property of Truero.ai Limited.
5
+
6
+ Subject to the Truero Terms & Conditions (https://truero.ai/terms), you are
7
+ granted a limited, non-exclusive, non-transferable licence to install and use
8
+ the Software for your internal business or personal purposes.
9
+
10
+ You may not sublicence, sell, resell, reverse-engineer, or otherwise exploit
11
+ the Software commercially without prior written consent from Truero.ai Limited.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. See the Terms
14
+ & Conditions and Privacy Policy (https://truero.ai/privacy) for full details.
15
+
16
+ Contact: legal@truero.ai
truero-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: truero
3
+ Version: 0.2.1
4
+ Summary: Understand and clean your data before it causes problems
5
+ Home-page: https://github.com/Thisizzellie/Truero
6
+ Author: Truero
7
+ Author-email: hello@truero.ai
8
+ License: Proprietary
9
+ Project-URL: Homepage, https://www.truero.ai
10
+ Project-URL: Dashboard, https://dashboard.truero.ai
11
+ Project-URL: Source, https://github.com/Thisizzellie/Truero
12
+ Keywords: ml data-quality validation bias audit cli
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: License :: Other/Proprietary License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: click>=8.0
28
+ Requires-Dist: requests>=2.28
29
+ Requires-Dist: rich>=13.0
30
+ Requires-Dist: pandas>=1.5
31
+ Requires-Dist: numpy>=1.23
32
+ Requires-Dist: flask>=3.0
33
+ Requires-Dist: openpyxl>=3.0
34
+ Requires-Dist: pyarrow>=10.0
35
+ Dynamic: author
36
+ Dynamic: author-email
37
+ Dynamic: classifier
38
+ Dynamic: description
39
+ Dynamic: description-content-type
40
+ Dynamic: home-page
41
+ Dynamic: keywords
42
+ Dynamic: license
43
+ Dynamic: license-file
44
+ Dynamic: project-url
45
+ Dynamic: requires-dist
46
+ Dynamic: requires-python
47
+ Dynamic: summary
48
+
49
+ # Truero CLI
50
+
51
+ Understand and clean your data before it causes problems.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install truero
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ truero scan data.csv
63
+ truero web
64
+ ```
65
+
66
+ ## Options
67
+
68
+ | Flag / env | Purpose |
69
+ |------------|---------|
70
+ | `--api-key` / `TRUERO_API_KEY` | API key sent to the Truero backend |
71
+ | `--user-id` / `TRUERO_USER_ID` | Supabase auth UUID for dashboard upload |
72
+ | `--no-upload` | Skip dashboard upload prompt |
73
+ | `TRUERO_API_URL` | Backend base URL (default: hosted Railway API for pip installs; `http://localhost:8001` in monorepo) |
74
+ | `TRUERO_DASHBOARD_URL` | Dashboard base URL (default: `https://dashboard.truero.ai`) |
75
+
76
+ You can also set API/dashboard URLs in the web wizard Settings, or in `~/.truero/config.json` (`web_api_url`, `web_dashboard_url`).
77
+
78
+ ## Notes
79
+
80
+ - Cleaning checks run on your machine.
81
+ - ML checks (Option A) call the Truero backend API.
82
+ - Upload sends the report to the backend so it appears in the dashboard.
83
+ - Supported file types: CSV, JSON, Excel (`.xlsx`/`.xls`), Parquet.
84
+ - First run prompts you to accept the Terms & Conditions (or pass `--accept-terms`).
truero-0.2.1/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Truero CLI
2
+
3
+ Understand and clean your data before it causes problems.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install truero
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ truero scan data.csv
15
+ truero web
16
+ ```
17
+
18
+ ## Options
19
+
20
+ | Flag / env | Purpose |
21
+ |------------|---------|
22
+ | `--api-key` / `TRUERO_API_KEY` | API key sent to the Truero backend |
23
+ | `--user-id` / `TRUERO_USER_ID` | Supabase auth UUID for dashboard upload |
24
+ | `--no-upload` | Skip dashboard upload prompt |
25
+ | `TRUERO_API_URL` | Backend base URL (default: hosted Railway API for pip installs; `http://localhost:8001` in monorepo) |
26
+ | `TRUERO_DASHBOARD_URL` | Dashboard base URL (default: `https://dashboard.truero.ai`) |
27
+
28
+ You can also set API/dashboard URLs in the web wizard Settings, or in `~/.truero/config.json` (`web_api_url`, `web_dashboard_url`).
29
+
30
+ ## Notes
31
+
32
+ - Cleaning checks run on your machine.
33
+ - ML checks (Option A) call the Truero backend API.
34
+ - Upload sends the report to the backend so it appears in the dashboard.
35
+ - Supported file types: CSV, JSON, Excel (`.xlsx`/`.xls`), Parquet.
36
+ - First run prompts you to accept the Terms & Conditions (or pass `--accept-terms`).
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
truero-0.2.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
truero-0.2.1/setup.py ADDED
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+
3
+ from setuptools import setup, find_packages
4
+
5
+ _README = Path(__file__).with_name("README.md")
6
+ _long_description = _README.read_text(encoding="utf-8") if _README.exists() else ""
7
+
8
+ setup(
9
+ name="truero",
10
+ version="0.2.1",
11
+ description="Understand and clean your data before it causes problems",
12
+ long_description=_long_description,
13
+ long_description_content_type="text/markdown",
14
+ author="Truero",
15
+ author_email="hello@truero.ai",
16
+ url="https://github.com/Thisizzellie/Truero",
17
+ project_urls={
18
+ "Homepage": "https://www.truero.ai",
19
+ "Dashboard": "https://dashboard.truero.ai",
20
+ "Source": "https://github.com/Thisizzellie/Truero",
21
+ },
22
+ license="Proprietary",
23
+ license_files=("LICENSE",),
24
+ packages=find_packages(exclude=["tests", "tests.*"]),
25
+ package_data={
26
+ "truero": ["legal/*.json"],
27
+ "truero.web": ["static/*.html", "static/*.js", "static/*.css"],
28
+ },
29
+ include_package_data=True,
30
+ install_requires=[
31
+ "click>=8.0",
32
+ "requests>=2.28",
33
+ "rich>=13.0",
34
+ "pandas>=1.5",
35
+ "numpy>=1.23",
36
+ "flask>=3.0",
37
+ "openpyxl>=3.0",
38
+ "pyarrow>=10.0",
39
+ ],
40
+ entry_points={
41
+ "console_scripts": [
42
+ "truero=truero.main:cli",
43
+ ],
44
+ },
45
+ python_requires=">=3.9",
46
+ classifiers=[
47
+ "Development Status :: 4 - Beta",
48
+ "Environment :: Console",
49
+ "Intended Audience :: Developers",
50
+ "Intended Audience :: Science/Research",
51
+ "License :: Other/Proprietary License",
52
+ "Programming Language :: Python :: 3",
53
+ "Programming Language :: Python :: 3.9",
54
+ "Programming Language :: Python :: 3.10",
55
+ "Programming Language :: Python :: 3.11",
56
+ "Programming Language :: Python :: 3.12",
57
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
58
+ ],
59
+ keywords="ml data-quality validation bias audit cli",
60
+ )
@@ -0,0 +1,390 @@
1
+ """Unit tests for CLI cleaning checks — all 8 checks in cleaning_engine.py.
2
+
3
+ Run with:
4
+ cd cli && source .venv/bin/activate && pip install -e . && pytest tests/ -v
5
+ """
6
+
7
+ import pytest
8
+ import pandas as pd
9
+ import numpy as np
10
+ from truero.cleaning_engine import (
11
+ check_duplicate_rows,
12
+ check_missing_values,
13
+ check_outliers,
14
+ check_data_types,
15
+ check_cardinality,
16
+ check_correlation,
17
+ check_format_consistency,
18
+ check_text_consistency,
19
+ run_cleaning_checks,
20
+ )
21
+
22
+
23
+ # ── Duplicate Rows ───────────────────────────────────────────────────
24
+
25
+ def test_duplicate_rows_pass():
26
+ df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
27
+ result = check_duplicate_rows(df)
28
+ assert result["status"] == "PASS"
29
+ assert result["detail"]["duplicate_count"] == 0
30
+ assert result["detail"]["duplicate_rate"] == 0.0
31
+
32
+
33
+ def test_duplicate_rows_warning():
34
+ # 2 dupes out of 102 rows ≈ 2.0% (between 1% and 5%)
35
+ uniques = pd.DataFrame({"a": range(100), "b": [f"x{i}" for i in range(100)]})
36
+ dupes = pd.DataFrame({"a": [0, 1], "b": ["x0", "x1"]})
37
+ df = pd.concat([uniques, dupes], ignore_index=True)
38
+ result = check_duplicate_rows(df)
39
+ assert result["status"] == "WARNING"
40
+ assert result["detail"]["duplicate_count"] == 2
41
+ assert "df.drop_duplicates()" in result["suggestion"]
42
+
43
+
44
+ def test_duplicate_rows_critical():
45
+ # 10 dupes out of 110 rows ≈ 9.1% (above 5%)
46
+ uniques = pd.DataFrame({"a": range(100), "b": [f"x{i}" for i in range(100)]})
47
+ dupes = pd.DataFrame({"a": range(10), "b": [f"x{i}" for i in range(10)]})
48
+ df = pd.concat([uniques, dupes], ignore_index=True)
49
+ result = check_duplicate_rows(df)
50
+ assert result["status"] == "CRITICAL"
51
+ assert result["detail"]["duplicate_count"] == 10
52
+ assert "df.drop_duplicates()" in result["suggestion"]
53
+
54
+
55
+ def test_duplicate_rows_empty():
56
+ df = pd.DataFrame()
57
+ result = check_duplicate_rows(df)
58
+ assert result["status"] == "PASS"
59
+ assert result["detail"]["duplicate_count"] == 0
60
+
61
+
62
+ def test_duplicate_rows_suggestion_includes_count():
63
+ uniques = pd.DataFrame({"a": range(100)})
64
+ dupes = pd.DataFrame({"a": range(10)})
65
+ df = pd.concat([uniques, dupes], ignore_index=True)
66
+ result = check_duplicate_rows(df)
67
+ assert "10 duplicate rows" in result["suggestion"]
68
+
69
+
70
+ # ── Missing Values ────────────────────────────────────────────────────
71
+
72
+ def test_missing_values_pass():
73
+ df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
74
+ result = check_missing_values(df, critical_threshold=0.20)
75
+ assert result["status"] == "PASS"
76
+ assert result["detail"]["total_missing"] == 0
77
+
78
+
79
+ def test_missing_values_critical():
80
+ # 50% missing should exceed even the lowest critical threshold
81
+ df = pd.DataFrame({"a": [1, None], "b": [None, "x"]})
82
+ result = check_missing_values(df, critical_threshold=0.10)
83
+ assert result["status"] == "CRITICAL"
84
+ assert result["detail"]["total_missing"] > 0
85
+
86
+
87
+ def test_missing_values_warning():
88
+ # 10% missing (below 20% critical, above 5% warning)
89
+ df = pd.DataFrame({"a": [1, 2, 3, 4, 5, 6, 7, 8, 9, None]})
90
+ result = check_missing_values(df, critical_threshold=0.20)
91
+ assert result["status"] in ("WARNING", "CRITICAL")
92
+
93
+
94
+ def test_missing_values_suggestion_includes_fillna():
95
+ df = pd.DataFrame({"age": [25, None, 35, None, 45]})
96
+ result = check_missing_values(df, critical_threshold=0.20)
97
+ assert "fillna" in result["suggestion"]
98
+ assert "age" in result["suggestion"]
99
+
100
+
101
+ def test_missing_values_empty():
102
+ df = pd.DataFrame()
103
+ result = check_missing_values(df)
104
+ assert result["status"] == "PASS"
105
+
106
+
107
+ # ── Outlier Detection ─────────────────────────────────────────────────
108
+
109
+ def test_outliers_pass():
110
+ np.random.seed(42)
111
+ df = pd.DataFrame({"score": np.random.randn(100) * 10 + 50})
112
+ result = check_outliers(df, threshold=0.05)
113
+ assert result["status"] == "PASS"
114
+ assert result["detail"]["columns_with_outliers"] == 0
115
+
116
+
117
+ def test_outliers_warning():
118
+ np.random.seed(42)
119
+ df = pd.DataFrame(
120
+ {"score": list(np.random.randn(100) * 10 + 50) + [500, 600, -100, -200]}
121
+ )
122
+ result = check_outliers(df, threshold=0.02)
123
+ assert result["status"] == "WARNING"
124
+ assert result["detail"]["columns_with_outliers"] > 0
125
+
126
+
127
+ def test_outliers_suggestion_includes_clip():
128
+ np.random.seed(42)
129
+ df = pd.DataFrame(
130
+ {"score": list(np.random.randn(50) * 10 + 50) + [500, 600]}
131
+ )
132
+ result = check_outliers(df, threshold=0.02)
133
+ if result["status"] != "PASS":
134
+ assert "clip" in result["suggestion"]
135
+ assert "score" in result["suggestion"]
136
+
137
+
138
+ # ── Data Type Consistency ─────────────────────────────────────────────
139
+
140
+ def test_data_types_pass():
141
+ df = pd.DataFrame({"age": [25, 30, 35], "name": ["Alice", "Bob", "Carol"]})
142
+ result = check_data_types(df)
143
+ assert result["status"] == "PASS"
144
+ assert result["detail"]["issues_found"] == 0
145
+
146
+
147
+ def test_data_types_date_keyword():
148
+ df = pd.DataFrame({"join_date": ["2024-01-01", "2024-02-01", "2024-03-01"]})
149
+ result = check_data_types(df)
150
+ assert any(i["column"] == "join_date" for i in result["detail"]["issues"])
151
+
152
+
153
+ def test_data_types_numeric_keyword():
154
+ df = pd.DataFrame({"age": ["25", "30", "35"]})
155
+ result = check_data_types(df)
156
+ assert any(i["column"] == "age" for i in result["detail"]["issues"])
157
+
158
+
159
+ def test_data_types_value_sampling_catches_numeric():
160
+ """Column with non-obvious name but 100% numeric values should be caught."""
161
+ df = pd.DataFrame({"info": ["100", "200", "300"]})
162
+ result = check_data_types(df)
163
+ assert any(
164
+ i["column"] == "info" and "numeric" in i["issue"]
165
+ for i in result["detail"]["issues"]
166
+ )
167
+
168
+
169
+ def test_data_types_value_sampling_catches_dates():
170
+ """Column with non-obvious name but 100% date values should be caught."""
171
+ df = pd.DataFrame({"field_x": ["2024-01-01", "2024-02-01", "2024-03-01"]})
172
+ result = check_data_types(df)
173
+ assert any(
174
+ i["column"] == "field_x" and "date" in i["issue"]
175
+ for i in result["detail"]["issues"]
176
+ )
177
+
178
+
179
+ # ── Cardinality ────────────────────────────────────────────────────────
180
+
181
+ def test_cardinality_pass():
182
+ df = pd.DataFrame({"category": ["A", "B", "A", "C", "B", "D"]})
183
+ result = check_cardinality(df)
184
+ assert result["status"] == "PASS"
185
+ assert result["detail"]["issues_found"] == 0
186
+
187
+
188
+ def test_cardinality_constant_column():
189
+ df = pd.DataFrame({"always_x": ["x", "x", "x"], "varies": [1, 2, 3]})
190
+ result = check_cardinality(df)
191
+ assert any(
192
+ i["column"] == "always_x" and i["type"] == "constant"
193
+ for i in result["detail"]["all_issues"]
194
+ )
195
+
196
+
197
+ def test_cardinality_likely_id():
198
+ df = pd.DataFrame({"id": [f"ID_{i:05d}" for i in range(200)]})
199
+ result = check_cardinality(df)
200
+ assert any(
201
+ i["column"] == "id" and i["type"] == "likely_id"
202
+ for i in result["detail"]["all_issues"]
203
+ )
204
+
205
+
206
+ # ── Correlation ────────────────────────────────────────────────────────
207
+
208
+ def test_correlation_pass():
209
+ # Uncorrelated data should pass
210
+ np.random.seed(42)
211
+ df = pd.DataFrame({"a": np.random.randn(100), "b": np.random.randn(100)})
212
+ result = check_correlation(df)
213
+ assert result["status"] == "PASS"
214
+
215
+
216
+ def test_correlation_warning():
217
+ df = pd.DataFrame({"a": range(100), "b": [x + np.random.randn() * 0.01 for x in range(100)]})
218
+ result = check_correlation(df)
219
+ assert result["status"] == "WARNING"
220
+ assert result["detail"]["pairs_found"] > 0
221
+
222
+
223
+ def test_correlation_not_enough_numeric():
224
+ df = pd.DataFrame({"a": ["x", "y", "z"], "b": ["p", "q", "r"]})
225
+ result = check_correlation(df)
226
+ assert result["status"] == "PASS"
227
+
228
+
229
+ # ── Format Consistency ─────────────────────────────────────────────────
230
+
231
+ def test_format_consistency_mixed_dates():
232
+ df = pd.DataFrame({
233
+ "dt": ["2024-01-01", "01/15/2024", "2024-03-20", "12/31/2024"]
234
+ })
235
+ result = check_format_consistency(df)
236
+ assert result["status"] != "PASS"
237
+ assert any(
238
+ "mixed date formats" in i["issue"]
239
+ for i in result["detail"]["issues"]
240
+ )
241
+
242
+
243
+ def test_format_consistency_number_formatting():
244
+ df = pd.DataFrame({
245
+ "revenue": ["$50,000", "$75,200", "$120,000.50", "$90K", "$1.2M"]
246
+ })
247
+ result = check_format_consistency(df)
248
+ assert any(
249
+ "number formatting" in i["issue"]
250
+ for i in result["detail"]["issues"]
251
+ )
252
+
253
+
254
+ def test_format_consistency_whitespace():
255
+ df = pd.DataFrame({
256
+ "name": ["Alice ", " Bob", "Carol"]
257
+ })
258
+ result = check_format_consistency(df)
259
+ assert any(
260
+ "whitespace" in i["issue"]
261
+ for i in result["detail"]["issues"]
262
+ )
263
+
264
+
265
+ def test_format_consistency_mixed_case():
266
+ df = pd.DataFrame({
267
+ "dept": ["ENGINEERING", "Sales", "HR", "engineering", "marketing"]
268
+ })
269
+ result = check_format_consistency(df)
270
+ assert any(
271
+ "mixed text case" in i["issue"]
272
+ for i in result["detail"]["issues"]
273
+ )
274
+
275
+
276
+ def test_format_consistency_unit_inconsistency():
277
+ df = pd.DataFrame({
278
+ "weight": ["kg", "KG", "Kilogram", "kilograms", "kg", "kgs"]
279
+ })
280
+ result = check_format_consistency(df)
281
+ assert any(
282
+ "inconsistent units" in i["issue"]
283
+ for i in result["detail"]["issues"]
284
+ )
285
+
286
+
287
+ def test_format_consistency_pass():
288
+ df = pd.DataFrame({
289
+ "date": ["2024-01-01", "2024-02-01"],
290
+ "amount": ["50000", "75000"],
291
+ "name": ["Alice", "Bob"],
292
+ })
293
+ result = check_format_consistency(df)
294
+ assert result["status"] == "PASS"
295
+
296
+
297
+ # ── Text Consistency ───────────────────────────────────────────────────
298
+
299
+ def test_text_consistency_case_variants():
300
+ df = pd.DataFrame({
301
+ "dept": ["Engineering", "engineering", "ENGINEERING"]
302
+ })
303
+ result = check_text_consistency(df)
304
+ assert result["status"] != "PASS"
305
+ assert any(
306
+ "case/space variants" in i["issue"]
307
+ for i in result["detail"]["issues"]
308
+ )
309
+
310
+
311
+ def test_text_consistency_near_duplicates():
312
+ df = pd.DataFrame({
313
+ "product": ["iPhone", "I Phone", "iPhone ", "iphone"]
314
+ })
315
+ result = check_text_consistency(df)
316
+ # Should catch at least case variants
317
+ assert result["detail"]["issues_found"] > 0
318
+
319
+
320
+ def test_text_consistency_skips_numeric_columns():
321
+ df = pd.DataFrame({
322
+ "revenue": ["$50,000", "$75,200", "$120,000"]
323
+ })
324
+ result = check_text_consistency(df)
325
+ # Revenue values are numbers with formatting — should be skipped
326
+ issues = [
327
+ i for i in result["detail"]["issues"] if i["column"] == "revenue"
328
+ ]
329
+ assert len(issues) == 0
330
+
331
+
332
+ def test_text_consistency_skips_date_columns():
333
+ df = pd.DataFrame({
334
+ "dt": ["2024-01-01", "2024-02-01", "2024-03-01"]
335
+ })
336
+ result = check_text_consistency(df)
337
+ issues = [i for i in result["detail"]["issues"] if i["column"] == "dt"]
338
+ assert len(issues) == 0
339
+
340
+
341
+ def test_text_consistency_pass():
342
+ df = pd.DataFrame({"name": ["Alice", "Bob", "Carol"]})
343
+ result = check_text_consistency(df)
344
+ assert result["status"] == "PASS"
345
+
346
+
347
+ def test_text_consistency_rows_affected_is_int():
348
+ df = pd.DataFrame({
349
+ "dept": ["Sales", "SALES", "sales"]
350
+ })
351
+ result = check_text_consistency(df)
352
+ for i in result["detail"]["issues"]:
353
+ assert isinstance(i["rows_affected"], (int, np.integer))
354
+
355
+
356
+ # ── Run All Checks ─────────────────────────────────────────────────────
357
+
358
+ def test_run_cleaning_checks_returns_all_8():
359
+ df = pd.DataFrame({
360
+ "age": [25, 30, 35, None, 45, 50],
361
+ "name": ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank"],
362
+ "salary": [50000, 60000, 70000, 80000, 90000, 100000],
363
+ })
364
+ context = {"missing_critical": 20, "outlier_threshold": 0.05}
365
+ results = run_cleaning_checks(df, context)
366
+ assert len(results) == 8
367
+ expected = {
368
+ "duplicate_rows", "missing_values", "outlier_detection",
369
+ "data_type_consistency", "cardinality", "correlation",
370
+ "format_consistency", "text_consistency",
371
+ }
372
+ actual = {r["check_name"] for r in results}
373
+ assert actual == expected
374
+
375
+
376
+ def test_run_cleaning_checks_all_have_categories():
377
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
378
+ context = {"missing_critical": 20, "outlier_threshold": 0.05}
379
+ results = run_cleaning_checks(df, context)
380
+ for r in results:
381
+ assert "category" in r, f"Check {r['check_name']} missing category"
382
+ assert r["category"] in ("integrity", "reliability", "ethics")
383
+
384
+
385
+ def test_run_cleaning_checks_all_return_status():
386
+ df = pd.DataFrame({"a": [1, 2, 3]})
387
+ context = {"missing_critical": 20, "outlier_threshold": 0.05}
388
+ results = run_cleaning_checks(df, context)
389
+ for r in results:
390
+ assert r["status"] in ("PASS", "WARNING", "CRITICAL")