cleanframe-engine 0.3.0__py3-none-any.whl

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 (46) hide show
  1. cleanframe/__init__.py +169 -0
  2. cleanframe/__main__.py +5 -0
  3. cleanframe/_util.py +438 -0
  4. cleanframe/_version.py +1 -0
  5. cleanframe/api.py +559 -0
  6. cleanframe/cli.py +688 -0
  7. cleanframe/codegen.py +666 -0
  8. cleanframe/dataio.py +506 -0
  9. cleanframe/detectors/__init__.py +43 -0
  10. cleanframe/detectors/base.py +221 -0
  11. cleanframe/detectors/categories.py +199 -0
  12. cleanframe/detectors/contacts.py +105 -0
  13. cleanframe/detectors/currency.py +112 -0
  14. cleanframe/detectors/dates.py +204 -0
  15. cleanframe/detectors/dedup.py +150 -0
  16. cleanframe/detectors/nulls.py +109 -0
  17. cleanframe/detectors/outliers.py +73 -0
  18. cleanframe/detectors/schema_mapping.py +125 -0
  19. cleanframe/detectors/text.py +105 -0
  20. cleanframe/detectors/units.py +86 -0
  21. cleanframe/diff.py +369 -0
  22. cleanframe/drift.py +283 -0
  23. cleanframe/errors.py +66 -0
  24. cleanframe/executor.py +229 -0
  25. cleanframe/fingerprint.py +83 -0
  26. cleanframe/issues.py +186 -0
  27. cleanframe/llm.py +811 -0
  28. cleanframe/ops.py +1245 -0
  29. cleanframe/planner.py +353 -0
  30. cleanframe/profile.py +413 -0
  31. cleanframe/py.typed +1 -0
  32. cleanframe/quality.py +81 -0
  33. cleanframe/readfix.py +160 -0
  34. cleanframe/recipe.py +398 -0
  35. cleanframe/report.py +345 -0
  36. cleanframe/result.py +144 -0
  37. cleanframe/schema.py +259 -0
  38. cleanframe/streaming.py +354 -0
  39. cleanframe/types.py +119 -0
  40. cleanframe/validate.py +363 -0
  41. cleanframe/workbook.py +370 -0
  42. cleanframe_engine-0.3.0.dist-info/METADATA +323 -0
  43. cleanframe_engine-0.3.0.dist-info/RECORD +46 -0
  44. cleanframe_engine-0.3.0.dist-info/WHEEL +4 -0
  45. cleanframe_engine-0.3.0.dist-info/entry_points.txt +2 -0
  46. cleanframe_engine-0.3.0.dist-info/licenses/LICENSE +201 -0
cleanframe/issues.py ADDED
@@ -0,0 +1,186 @@
1
+ """Detected problems and the fixes proposed for them.
2
+
3
+ A detector's job is to return :class:`Issues` — a small, ergonomic collection of
4
+ :class:`Issue` objects. Each issue may carry a :class:`Proposal`: the ops and/or
5
+ rename that would fix it. The planner later folds proposals into a recipe.
6
+
7
+ This separation is deliberate. Detectors own *domain knowledge* ("these three
8
+ spellings are the same city"); the planner owns *policy* ("include this fix only
9
+ if confidence clears the bar for the current mode"). Keeping proposals on the
10
+ issue means a rules-only run and an LLM-assisted run share the exact same fix
11
+ vocabulary.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Iterable, Iterator
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ from .types import Op, Severity
21
+
22
+
23
+ def _cap_examples(values: Iterable[Any], limit: int = 5) -> list[Any]:
24
+ """Take up to ``limit`` example values for evidence, preserving order."""
25
+ out: list[Any] = []
26
+ for v in values:
27
+ out.append(v)
28
+ if len(out) >= limit:
29
+ break
30
+ return out
31
+
32
+
33
+ @dataclass
34
+ class Proposal:
35
+ """A concrete, replayable fix for an issue: an optional rename plus ops.
36
+
37
+ ``ops`` are applied in order to the column. ``rename_to`` (if set) becomes the
38
+ column's output name. The planner may merge proposals from several issues that
39
+ target the same column, concatenating their ops in detector-priority order.
40
+ """
41
+
42
+ rename_to: str | None = None
43
+ ops: list[Op] = field(default_factory=list)
44
+
45
+ def is_empty(self) -> bool:
46
+ return self.rename_to is None and not self.ops
47
+
48
+
49
+ @dataclass
50
+ class Issue:
51
+ """One detected problem, optionally with a proposed fix.
52
+
53
+ Attributes
54
+ ----------
55
+ detector:
56
+ Name of the detector that raised it (stamped automatically by the runner).
57
+ column:
58
+ Column the issue pertains to, or ``None`` for dataset-level issues
59
+ (duplicates, for example). Stamped by the runner for column detectors.
60
+ kind:
61
+ A stable machine slug (``"mixed_date_formats"``) used by reports and tests.
62
+ severity:
63
+ :class:`~cleanframe.types.Severity`.
64
+ message:
65
+ Human-readable, one line.
66
+ confidence:
67
+ How sure the detector is, in ``[0, 1]``. The planner gates on this.
68
+ evidence:
69
+ Small JSON-serialisable dict of counts and example values for the report.
70
+ proposal:
71
+ The fix, or ``None`` if the issue is informational / needs a human.
72
+ """
73
+
74
+ kind: str
75
+ message: str
76
+ severity: Severity = Severity.WARNING
77
+ column: str | None = None
78
+ detector: str = ""
79
+ confidence: float = 1.0
80
+ evidence: dict[str, Any] = field(default_factory=dict)
81
+ proposal: Proposal | None = None
82
+
83
+ @property
84
+ def has_fix(self) -> bool:
85
+ return self.proposal is not None and not self.proposal.is_empty()
86
+
87
+
88
+ class Issues:
89
+ """An ordered, list-like collection of :class:`Issue` objects.
90
+
91
+ This is the public return type for detectors (``def detect(series) -> Issues``).
92
+ The :meth:`add` helper is the ergonomic path — it builds an :class:`Issue`,
93
+ appends it, and returns it so evidence can be tweaked inline::
94
+
95
+ issues = cf.Issues()
96
+ issues.add("bad_email", "3 invalid addresses", severity=cf.Severity.ERROR)
97
+ """
98
+
99
+ def __init__(self, items: Iterable[Issue] | None = None) -> None:
100
+ self._items: list[Issue] = list(items) if items else []
101
+
102
+ # -- construction ----------------------------------------------------
103
+ def add(
104
+ self,
105
+ kind: str,
106
+ message: str,
107
+ *,
108
+ severity: Severity | str = Severity.WARNING,
109
+ column: str | None = None,
110
+ confidence: float = 1.0,
111
+ evidence: dict[str, Any] | None = None,
112
+ proposal: Proposal | None = None,
113
+ ops: list[Op] | None = None,
114
+ rename_to: str | None = None,
115
+ ) -> Issue:
116
+ """Build, append, and return an :class:`Issue`.
117
+
118
+ ``ops`` / ``rename_to`` are a shorthand for constructing a
119
+ :class:`Proposal`; passing them alongside an explicit ``proposal`` is an
120
+ error to avoid silently dropping one.
121
+ """
122
+ if proposal is not None and (ops or rename_to):
123
+ raise ValueError("Pass either `proposal` or `ops`/`rename_to`, not both.")
124
+ if ops or rename_to:
125
+ proposal = Proposal(rename_to=rename_to, ops=list(ops or []))
126
+ if not isinstance(severity, Severity):
127
+ severity = Severity(str(severity))
128
+ issue = Issue(
129
+ kind=kind,
130
+ message=message,
131
+ severity=severity,
132
+ column=column,
133
+ confidence=confidence,
134
+ evidence=evidence or {},
135
+ proposal=proposal,
136
+ )
137
+ self._items.append(issue)
138
+ return issue
139
+
140
+ def append(self, issue: Issue) -> None:
141
+ self._items.append(issue)
142
+
143
+ def extend(self, other: Iterable[Issue]) -> None:
144
+ self._items.extend(other)
145
+
146
+ # -- querying --------------------------------------------------------
147
+ def for_column(self, column: str | None) -> Issues:
148
+ return Issues(i for i in self._items if i.column == column)
149
+
150
+ def by_severity(self, minimum: Severity) -> Issues:
151
+ return Issues(i for i in self._items if i.severity.rank >= minimum.rank)
152
+
153
+ def with_fixes(self) -> Issues:
154
+ return Issues(i for i in self._items if i.has_fix)
155
+
156
+ def columns(self) -> list[str]:
157
+ seen: list[str] = []
158
+ for i in self._items:
159
+ if i.column is not None and i.column not in seen:
160
+ seen.append(i.column)
161
+ return seen
162
+
163
+ @property
164
+ def max_severity(self) -> Severity | None:
165
+ if not self._items:
166
+ return None
167
+ return max((i.severity for i in self._items), key=lambda s: s.rank)
168
+
169
+ # -- dunder ----------------------------------------------------------
170
+ def __iter__(self) -> Iterator[Issue]:
171
+ return iter(self._items)
172
+
173
+ def __len__(self) -> int:
174
+ return len(self._items)
175
+
176
+ def __getitem__(self, idx: int) -> Issue:
177
+ return self._items[idx]
178
+
179
+ def __bool__(self) -> bool:
180
+ return bool(self._items)
181
+
182
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
183
+ return f"Issues({len(self._items)} issue(s))"
184
+
185
+
186
+ __all__ = ["Issue", "Issues", "Proposal", "_cap_examples"]