datasentry-core 0.7.0__tar.gz → 0.8.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.
- datasentry_core-0.8.0/.gitignore +43 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/PKG-INFO +1 -1
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/pyproject.toml +1 -1
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/__init__.py +1 -1
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/csv.py +19 -0
- datasentry_core-0.8.0/src/datasentry_core/connectors/sampling.py +141 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/xlsx.py +12 -1
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/base.py +13 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/common.py +9 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/datetime.py +31 -12
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/anomaly_ml.py +4 -1
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/categorical.py +27 -13
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/formula.py +6 -2
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/missing.py +18 -9
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/numeric.py +44 -21
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/textual.py +42 -16
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/uniqueness.py +12 -4
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/runner.py +74 -10
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/textual_variants.py +11 -4
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/engine/profiler.py +27 -2
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/repair.py +14 -1
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/scan.py +1 -0
- datasentry_core-0.8.0/src/datasentry_core/plugin_fixtures.py +156 -0
- datasentry_core-0.8.0/src/datasentry_core/plugin_locks.py +187 -0
- datasentry_core-0.8.0/src/datasentry_core/plugins.py +402 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/repair/engine.py +43 -0
- datasentry_core-0.8.0/src/datasentry_core/reporting/column_profiles.py +204 -0
- datasentry_core-0.8.0/src/datasentry_core/reporting/evidence_desc.py +72 -0
- datasentry_core-0.8.0/src/datasentry_core/reporting/html.py +623 -0
- datasentry_core-0.8.0/src/datasentry_core/reporting/i18n.py +725 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/reporting/interactive.py +140 -34
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/reporting/markdown.py +38 -21
- datasentry_core-0.8.0/src/datasentry_core/reporting/suggestions.py +101 -0
- datasentry_core-0.8.0/src/datasentry_core/reporting/translate.py +72 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/storage/paths.py +9 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/storage/schema.py +99 -7
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/storage/store.py +13 -5
- datasentry_core-0.7.0/.gitignore +0 -32
- datasentry_core-0.7.0/src/datasentry_core/plugins.py +0 -177
- datasentry_core-0.7.0/src/datasentry_core/reporting/html.py +0 -267
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/README.md +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/base.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/duckdb.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/errors.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/file_based.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/jsonl.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/mysql.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/parquet.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/postgres.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/registry.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/remote_file.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/spec.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/connectors/sqlite.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/contracts/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/contracts/exporters.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/cross_field.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/cross_table.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/missingness.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/safe_eval.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/drift/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/drift/engine.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/engine/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/engine/base.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/engine/duckdb.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/engine/fusion.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/engine/sql_guard.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/llm/provider.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/audit.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/contract.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/detector.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/drift.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/enums.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/evidence.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/feedback.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/fingerprint.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/issue.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/llm.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/profile.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/quality.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/rules.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/models/validation.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/privacy/redactor.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/repair/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/reporting/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/reporting/junit.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/reporting/sarif.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/rules/engine.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/scoring/__init__.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/scoring/engine.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/scoring/gate.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/scoring/quality.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/scoring/weights.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/secrets.py +0 -0
- {datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/storage/__init__.py +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.coverage
|
|
9
|
+
htmlcov/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
hypothesis/
|
|
14
|
+
.hypothesis/
|
|
15
|
+
|
|
16
|
+
# Project data
|
|
17
|
+
.datasentry/
|
|
18
|
+
*.db
|
|
19
|
+
*.sqlite3
|
|
20
|
+
|
|
21
|
+
# Env / secrets
|
|
22
|
+
.env
|
|
23
|
+
credentials.json
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
|
|
28
|
+
# Editors
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
~$*
|
|
32
|
+
workspace/
|
|
33
|
+
|
|
34
|
+
# Debug scratch (agent sessions)
|
|
35
|
+
p/
|
|
36
|
+
|
|
37
|
+
# 开发任务书(prompt)——本地保留,不进仓库(V22 起约定)
|
|
38
|
+
docs/V*_DEV_PROMPT.md
|
|
39
|
+
|
|
40
|
+
# 完整产品设计与开发 Prompt(本地保留,不进仓库)
|
|
41
|
+
DataSentry AI 产品设计与开发 Prompt*.md
|
|
42
|
+
DataSentry AI 产品设计与开发 Prompt*.docx
|
|
43
|
+
DataSentry AI 增长与推广 Prompt.md
|
|
@@ -33,6 +33,9 @@ from datasentry_core.engine import DuckDBExecutor
|
|
|
33
33
|
from datasentry_core.models.enums import Severity
|
|
34
34
|
from datasentry_core.models.fingerprint import DatasetFingerprint
|
|
35
35
|
|
|
36
|
+
#: 非 utf-8 全文件入内存的警告阈值(Step 73/ADR-073)
|
|
37
|
+
_NON_UTF8_FULL_READ_WARN_BYTES = 512 * 1024 * 1024
|
|
38
|
+
|
|
36
39
|
FORMULA_INJECTION_PREFIXES: tuple[str, ...] = ("=", "+", "-", "@", "\t", "\r")
|
|
37
40
|
_WARNING_CAP = 100
|
|
38
41
|
|
|
@@ -132,6 +135,22 @@ class CsvDataHandle:
|
|
|
132
135
|
f"delim={_sql_string_literal(self._delimiter)})"
|
|
133
136
|
)
|
|
134
137
|
else:
|
|
138
|
+
# Step 73/ADR-073:非 utf-8 全文件入内存(pyarrow 无流式编码读取),
|
|
139
|
+
# 超阈值时预置提示 warning,引导 --sampling 或转码
|
|
140
|
+
if self._path.stat().st_size > _NON_UTF8_FULL_READ_WARN_BYTES:
|
|
141
|
+
self._warnings = [
|
|
142
|
+
LoadWarning(
|
|
143
|
+
row=0,
|
|
144
|
+
column="",
|
|
145
|
+
message=(
|
|
146
|
+
"non-utf-8 csv is fully loaded into memory "
|
|
147
|
+
f"({self._path.stat().st_size} bytes > "
|
|
148
|
+
f"{_NON_UTF8_FULL_READ_WARN_BYTES}); "
|
|
149
|
+
"use --sampling or re-encode as utf-8"
|
|
150
|
+
),
|
|
151
|
+
severity=Severity.LOW,
|
|
152
|
+
)
|
|
153
|
+
]
|
|
135
154
|
table = pa_csv.read_csv(
|
|
136
155
|
self._path,
|
|
137
156
|
read_options=pa_csv.ReadOptions(encoding=self._encoding),
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""抽样数据句柄(Step 71/73,ADR-071/073):检测器 SQL 顶层 FROM data 改写。
|
|
2
|
+
|
|
3
|
+
零连接器改动:与底层句柄共享 executor,首次数据访问时把抽样子集物化为
|
|
4
|
+
TEMP TABLE(reservoir + REPEATABLE(seed) 可复现,20.3),后续检测器
|
|
5
|
+
查询直接读内存表(ADR-073:避免每检测器重复 reservoir 重扫 1e6 行,
|
|
6
|
+
bench 抽样档 52s→~15s、峰值 449MB→<300MB);无 executor 的连接器
|
|
7
|
+
(协议兜底)退回抽样子查询重写。
|
|
8
|
+
count_rows() 返回抽样行数(该句柄实际可观测的行数)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from datasentry_core.connectors.base import (
|
|
18
|
+
DataHandle,
|
|
19
|
+
FingerprintMode,
|
|
20
|
+
FrameBatch,
|
|
21
|
+
LoadWarning,
|
|
22
|
+
SamplingMethod,
|
|
23
|
+
SchemaInfo,
|
|
24
|
+
)
|
|
25
|
+
from datasentry_core.connectors.spec import DataSourceType
|
|
26
|
+
from datasentry_core.models.fingerprint import DatasetFingerprint
|
|
27
|
+
|
|
28
|
+
#: 仓库内检测器/规则/画像 SQL 一律以顶层 FROM data 引用视图(50 处均为该形态)。
|
|
29
|
+
#: 重写后残留(子查询内部)不匹配 lookahead,见 _rewrite 守卫。
|
|
30
|
+
_FROM_DATA_RE = re.compile(r"\bFROM data\b")
|
|
31
|
+
|
|
32
|
+
#: 物化 TEMP TABLE 名(与 data 视图同 executor 连接内,无命名冲突)
|
|
33
|
+
_SAMPLED_TABLE = "sampled_data"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SampledDataHandle:
|
|
37
|
+
"""包装句柄:把对 data 视图的查询改写为抽样数据(顶层 FROM data 单形态)。
|
|
38
|
+
|
|
39
|
+
首次数据访问物化 TEMP TABLE(每次检测器查询复用,避免重复 reservoir
|
|
40
|
+
重扫);不支持 read_batches(抽样语义面向 SQL 下推);read_sample
|
|
41
|
+
委托底层(对抽样数据再抽样无意义,交由调用方决定)。
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
handle: DataHandle,
|
|
47
|
+
n: int,
|
|
48
|
+
seed: int = 42,
|
|
49
|
+
method: SamplingMethod = "reservoir",
|
|
50
|
+
) -> None:
|
|
51
|
+
if n < 1:
|
|
52
|
+
raise ValueError("n must be >= 1")
|
|
53
|
+
self._inner = handle
|
|
54
|
+
self._n = n
|
|
55
|
+
self._seed = seed
|
|
56
|
+
self._method = method
|
|
57
|
+
self._executor = getattr(handle, "_executor", None)
|
|
58
|
+
self._sampled_ready = False
|
|
59
|
+
|
|
60
|
+
def _ensure_sampled_table(self) -> None:
|
|
61
|
+
if self._sampled_ready or self._executor is None:
|
|
62
|
+
return
|
|
63
|
+
# 惰性视图:建表前确保 data 视图已注册(count_rows 触发各连接器
|
|
64
|
+
# _ensure_view;CSV 1e6 行计数 ~0.2s,可接受)
|
|
65
|
+
self._inner.count_rows()
|
|
66
|
+
if self._method == "none":
|
|
67
|
+
ddl = (
|
|
68
|
+
f"CREATE OR REPLACE TEMP TABLE {_SAMPLED_TABLE} AS "
|
|
69
|
+
f"SELECT * FROM data LIMIT {self._n}"
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
# SAMPLE 子句不支持预编译参数,n/seed 均为代码内整数(已校验),安全内联
|
|
73
|
+
ddl = (
|
|
74
|
+
f"CREATE OR REPLACE TEMP TABLE {_SAMPLED_TABLE} AS "
|
|
75
|
+
f"SELECT * FROM data USING SAMPLE reservoir({self._n} ROWS) "
|
|
76
|
+
f"REPEATABLE ({self._seed})"
|
|
77
|
+
)
|
|
78
|
+
self._executor.execute_setup(ddl)
|
|
79
|
+
self._sampled_ready = True
|
|
80
|
+
|
|
81
|
+
def _sampled_subquery(self) -> str:
|
|
82
|
+
if self._method == "none":
|
|
83
|
+
return f"(SELECT * FROM data LIMIT {self._n})"
|
|
84
|
+
# SAMPLE 子句不支持预编译参数,n/seed 均为代码内整数(已校验),安全内联
|
|
85
|
+
return (
|
|
86
|
+
f"(SELECT * FROM data USING SAMPLE reservoir({self._n} ROWS) REPEATABLE ({self._seed}))"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _rewrite(self, sql: str) -> str:
|
|
90
|
+
if self._executor is not None:
|
|
91
|
+
self._ensure_sampled_table()
|
|
92
|
+
target = f"FROM {_SAMPLED_TABLE}"
|
|
93
|
+
else:
|
|
94
|
+
target = f"FROM {self._sampled_subquery()}"
|
|
95
|
+
rewritten = _FROM_DATA_RE.sub(target, sql)
|
|
96
|
+
# 守卫:重写后不允许残留裸 FROM data(防未覆盖 SQL 形态,如别名/join)
|
|
97
|
+
if re.search(r"\bFROM data\b(?! USING SAMPLE)", rewritten):
|
|
98
|
+
raise ValueError(f"cannot rewrite sampled SQL: {sql!r}")
|
|
99
|
+
return rewritten
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def source_type(self) -> DataSourceType:
|
|
103
|
+
return self._inner.source_type
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def source_path(self) -> Path | str | None:
|
|
107
|
+
return self._inner.source_path
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def table_name(self) -> str | None:
|
|
111
|
+
return self._inner.table_name
|
|
112
|
+
|
|
113
|
+
def schema(self) -> SchemaInfo:
|
|
114
|
+
return self._inner.schema()
|
|
115
|
+
|
|
116
|
+
def read_batches(self, batch_size: int = 65536) -> Iterator[FrameBatch]:
|
|
117
|
+
return self._inner.read_batches(batch_size)
|
|
118
|
+
|
|
119
|
+
def read_sample(self, n: int, method: SamplingMethod = "random") -> FrameBatch:
|
|
120
|
+
return self._inner.read_sample(n, method)
|
|
121
|
+
|
|
122
|
+
def sql_aggregate(self, sql: str, params: dict[str, object] | None = None) -> FrameBatch:
|
|
123
|
+
return self._inner.sql_aggregate(self._rewrite(sql), params)
|
|
124
|
+
|
|
125
|
+
def count_rows(self) -> int:
|
|
126
|
+
return min(self._n, self._inner.count_rows())
|
|
127
|
+
|
|
128
|
+
def fingerprint(self, mode: FingerprintMode = "full") -> DatasetFingerprint:
|
|
129
|
+
return self._inner.fingerprint(mode)
|
|
130
|
+
|
|
131
|
+
def content_fingerprint(self) -> str:
|
|
132
|
+
return self._inner.content_fingerprint()
|
|
133
|
+
|
|
134
|
+
def stats_fingerprint(self) -> str:
|
|
135
|
+
return self._inner.stats_fingerprint()
|
|
136
|
+
|
|
137
|
+
def warnings(self) -> list[LoadWarning]:
|
|
138
|
+
return self._inner.warnings()
|
|
139
|
+
|
|
140
|
+
def close(self) -> None:
|
|
141
|
+
self._inner.close()
|
|
@@ -12,9 +12,13 @@ import pyarrow as pa
|
|
|
12
12
|
from openpyxl import load_workbook
|
|
13
13
|
|
|
14
14
|
from datasentry_core.connectors.base import FrameBatch
|
|
15
|
+
from datasentry_core.connectors.errors import ConnectorError
|
|
15
16
|
from datasentry_core.connectors.file_based import FileDataHandle
|
|
16
17
|
from datasentry_core.connectors.spec import DataSourceSpec, DataSourceType
|
|
17
18
|
|
|
19
|
+
#: 整 sheet 入内存的行预算(ADR-019 MVP 语义,Step 73/ADR-073 显式化)
|
|
20
|
+
_XLSX_ROW_BUDGET = 1_000_000
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
def _rows_to_table(names: list[str], rows: list[list[object]]) -> pa.Table:
|
|
20
24
|
"""行值列表 → pyarrow 表;混合类型列自动推断失败时回退全字符串(ADR-019)。"""
|
|
@@ -51,7 +55,14 @@ class XlsxDataHandle(FileDataHandle):
|
|
|
51
55
|
sheet = workbook.worksheets[sheet_ref]
|
|
52
56
|
else:
|
|
53
57
|
sheet = workbook[str(sheet_ref)]
|
|
54
|
-
rows =
|
|
58
|
+
rows: list[tuple[object, ...]] = []
|
|
59
|
+
for row in sheet.iter_rows(values_only=True):
|
|
60
|
+
if len(rows) >= _XLSX_ROW_BUDGET:
|
|
61
|
+
raise ConnectorError(
|
|
62
|
+
f"xlsx sheet exceeds row budget {_XLSX_ROW_BUDGET} "
|
|
63
|
+
"(ADR-019/ADR-073); split the sheet or use --sampling"
|
|
64
|
+
)
|
|
65
|
+
rows.append(row)
|
|
55
66
|
finally:
|
|
56
67
|
workbook.close()
|
|
57
68
|
if header_row >= len(rows):
|
|
@@ -44,6 +44,19 @@ class DetectionContext:
|
|
|
44
44
|
self.sample_rows = sample_rows
|
|
45
45
|
self.references: list[TableReference] = references or []
|
|
46
46
|
|
|
47
|
+
def with_handle(self, handle: DataHandle) -> DetectionContext:
|
|
48
|
+
"""返回换句柄的新上下文(Step 71:抽样支撑检测器注入抽样句柄)。"""
|
|
49
|
+
return DetectionContext(
|
|
50
|
+
dataset_id=self.dataset_id,
|
|
51
|
+
table_name=self.table_name,
|
|
52
|
+
columns=self.columns,
|
|
53
|
+
handle=handle,
|
|
54
|
+
config=self.config,
|
|
55
|
+
profile=self.profile,
|
|
56
|
+
sample_rows=self.sample_rows,
|
|
57
|
+
references=self.references,
|
|
58
|
+
)
|
|
59
|
+
|
|
47
60
|
|
|
48
61
|
@runtime_checkable
|
|
49
62
|
class Detector(Protocol):
|
|
@@ -34,6 +34,15 @@ def make_evidence(
|
|
|
34
34
|
data: dict[str, Any] | None = None,
|
|
35
35
|
confidence: float = 1.0,
|
|
36
36
|
) -> Evidence:
|
|
37
|
+
from datasentry_core.reporting.evidence_desc import EvText
|
|
38
|
+
|
|
39
|
+
if isinstance(description, EvText):
|
|
40
|
+
data = {
|
|
41
|
+
**description.base,
|
|
42
|
+
**(data or {}),
|
|
43
|
+
"_text_key": description.key,
|
|
44
|
+
"_params": description.params,
|
|
45
|
+
}
|
|
37
46
|
return Evidence(
|
|
38
47
|
evidence_id=f"ev_{uuid.uuid4().hex[:12]}",
|
|
39
48
|
evidence_type=evidence_type,
|
|
@@ -18,6 +18,7 @@ from datasentry_core.detectors.common import (
|
|
|
18
18
|
)
|
|
19
19
|
from datasentry_core.models.detector import DetectorCapabilities, IssueCandidate
|
|
20
20
|
from datasentry_core.models.enums import EvidenceType, QualityDimension, Severity
|
|
21
|
+
from datasentry_core.reporting.evidence_desc import ev
|
|
21
22
|
|
|
22
23
|
# 字符串日期列判定(列名特征;VARCHAR 列才适用 invalid/impossible/mixed)
|
|
23
24
|
# 注意:时间戳列(created_at 等,物理 TIMESTAMP 或含时间值)不归此组,
|
|
@@ -74,8 +75,11 @@ class InvalidDateDetector(DetectorBase):
|
|
|
74
75
|
detector_id=self.detector_id,
|
|
75
76
|
detector_version=self.detector_version,
|
|
76
77
|
evidence_type=EvidenceType.PATTERN_MATCH,
|
|
77
|
-
description=
|
|
78
|
-
|
|
78
|
+
description=ev(
|
|
79
|
+
"datetime.iso",
|
|
80
|
+
{"count": count, "pattern": _DATETIME_RE},
|
|
81
|
+
count=count,
|
|
82
|
+
),
|
|
79
83
|
)
|
|
80
84
|
],
|
|
81
85
|
raw_score=count,
|
|
@@ -123,8 +127,11 @@ class ImpossibleDateDetector(DetectorBase):
|
|
|
123
127
|
detector_id=self.detector_id,
|
|
124
128
|
detector_version=self.detector_version,
|
|
125
129
|
evidence_type=EvidenceType.PATTERN_MATCH,
|
|
126
|
-
description=
|
|
127
|
-
|
|
130
|
+
description=ev(
|
|
131
|
+
"datetime.invalid",
|
|
132
|
+
{"count": count, "pattern": _DATETIME_RE},
|
|
133
|
+
count=count,
|
|
134
|
+
),
|
|
128
135
|
)
|
|
129
136
|
],
|
|
130
137
|
raw_score=count,
|
|
@@ -168,8 +175,11 @@ class FutureDateDetector(DetectorBase):
|
|
|
168
175
|
detector_id=self.detector_id,
|
|
169
176
|
detector_version=self.detector_version,
|
|
170
177
|
evidence_type=EvidenceType.CONSTRAINT_VIOLATION,
|
|
171
|
-
description=
|
|
172
|
-
|
|
178
|
+
description=ev(
|
|
179
|
+
"datetime.future",
|
|
180
|
+
{"count": count, "max_future_days": 1},
|
|
181
|
+
count=count,
|
|
182
|
+
),
|
|
173
183
|
)
|
|
174
184
|
],
|
|
175
185
|
raw_score=count,
|
|
@@ -215,8 +225,11 @@ class StaleDateDetector(DetectorBase):
|
|
|
215
225
|
detector_id=self.detector_id,
|
|
216
226
|
detector_version=self.detector_version,
|
|
217
227
|
evidence_type=EvidenceType.CONSTRAINT_VIOLATION,
|
|
218
|
-
description=
|
|
219
|
-
|
|
228
|
+
description=ev(
|
|
229
|
+
"datetime.old",
|
|
230
|
+
{"count": count, "max_age_days": 365},
|
|
231
|
+
count=count,
|
|
232
|
+
),
|
|
220
233
|
)
|
|
221
234
|
],
|
|
222
235
|
raw_score=count,
|
|
@@ -283,8 +296,11 @@ class MixedDateFormatDetector(DetectorBase):
|
|
|
283
296
|
detector_id=self.detector_id,
|
|
284
297
|
detector_version=self.detector_version,
|
|
285
298
|
evidence_type=EvidenceType.STATISTICAL_MEASURE,
|
|
286
|
-
description=
|
|
287
|
-
|
|
299
|
+
description=ev(
|
|
300
|
+
"datetime.formats",
|
|
301
|
+
{"formats": {k: round(v, 4) for k, v in formats.items()}},
|
|
302
|
+
len=len(major),
|
|
303
|
+
),
|
|
288
304
|
)
|
|
289
305
|
],
|
|
290
306
|
raw_score=len(major),
|
|
@@ -331,8 +347,11 @@ class DuplicateTimestampDetector(DetectorBase):
|
|
|
331
347
|
detector_id=self.detector_id,
|
|
332
348
|
detector_version=self.detector_version,
|
|
333
349
|
evidence_type=EvidenceType.DUPLICATE_MATCH,
|
|
334
|
-
description=
|
|
335
|
-
|
|
350
|
+
description=ev(
|
|
351
|
+
"datetime.timestamps",
|
|
352
|
+
{"groups": len(counts), "affected": affected},
|
|
353
|
+
len=len(counts),
|
|
354
|
+
),
|
|
336
355
|
)
|
|
337
356
|
],
|
|
338
357
|
raw_score=len(counts),
|
{datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/anomaly_ml.py
RENAMED
|
@@ -153,8 +153,11 @@ class ModelOutlierDetector(DetectorBase):
|
|
|
153
153
|
for column in _infer_columns(context):
|
|
154
154
|
q = quote_ident(column)
|
|
155
155
|
try:
|
|
156
|
+
# SQL 侧 reservoir 抽样(Step 72/ADR-072):物化前限制行数;
|
|
157
|
+
# 行数 < max_samples 时 reservoir 等价全量,语义不变
|
|
156
158
|
table = context.handle.sql_aggregate(
|
|
157
|
-
f"SELECT {q} AS v FROM data WHERE {q} IS NOT NULL"
|
|
159
|
+
f"SELECT {q} AS v FROM data WHERE {q} IS NOT NULL "
|
|
160
|
+
f"USING SAMPLE reservoir({max_samples} ROWS) REPEATABLE ({seed})"
|
|
158
161
|
).table
|
|
159
162
|
except Exception:
|
|
160
163
|
continue
|
{datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/categorical.py
RENAMED
|
@@ -16,6 +16,7 @@ from datasentry_core.detectors.common import (
|
|
|
16
16
|
)
|
|
17
17
|
from datasentry_core.models.detector import DetectorCapabilities, IssueCandidate
|
|
18
18
|
from datasentry_core.models.enums import EvidenceType, QualityDimension, Severity
|
|
19
|
+
from datasentry_core.reporting.evidence_desc import ev
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def _json_safe(value: Any) -> Any:
|
|
@@ -67,8 +68,11 @@ class SuspiciousPlaceholderDetector(DetectorBase):
|
|
|
67
68
|
detector_id=self.detector_id,
|
|
68
69
|
detector_version=self.detector_version,
|
|
69
70
|
evidence_type=EvidenceType.PATTERN_MATCH,
|
|
70
|
-
description=
|
|
71
|
-
|
|
71
|
+
description=ev(
|
|
72
|
+
"categorical.placeholder",
|
|
73
|
+
{"count": count},
|
|
74
|
+
count=count,
|
|
75
|
+
),
|
|
72
76
|
)
|
|
73
77
|
],
|
|
74
78
|
raw_score=count,
|
|
@@ -131,13 +135,16 @@ class RareCategoryDetector(DetectorBase):
|
|
|
131
135
|
detector_id=self.detector_id,
|
|
132
136
|
detector_version=self.detector_version,
|
|
133
137
|
evidence_type=EvidenceType.STATISTICAL_MEASURE,
|
|
134
|
-
description=
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
description=ev(
|
|
139
|
+
"categorical.rare",
|
|
140
|
+
{
|
|
141
|
+
"categories": [
|
|
142
|
+
(_json_safe(v), int(c))
|
|
143
|
+
for v, c in zip(values, counts, strict=True)
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
len=len(values),
|
|
147
|
+
),
|
|
141
148
|
)
|
|
142
149
|
],
|
|
143
150
|
raw_score=len(values),
|
|
@@ -188,8 +195,11 @@ class CategoryExplosionDetector(DetectorBase):
|
|
|
188
195
|
detector_id=self.detector_id,
|
|
189
196
|
detector_version=self.detector_version,
|
|
190
197
|
evidence_type=EvidenceType.STATISTICAL_MEASURE,
|
|
191
|
-
description=
|
|
192
|
-
|
|
198
|
+
description=ev(
|
|
199
|
+
"categorical.high_unique",
|
|
200
|
+
{"unique_ratio": round(ratio, 6), "distinct": distinct},
|
|
201
|
+
ratio=ratio,
|
|
202
|
+
),
|
|
193
203
|
)
|
|
194
204
|
],
|
|
195
205
|
raw_score=ratio,
|
|
@@ -240,8 +250,12 @@ class InconsistentCaseDetector(DetectorBase):
|
|
|
240
250
|
detector_id=self.detector_id,
|
|
241
251
|
detector_version=self.detector_version,
|
|
242
252
|
evidence_type=EvidenceType.STATISTICAL_MEASURE,
|
|
243
|
-
description=
|
|
244
|
-
|
|
253
|
+
description=ev(
|
|
254
|
+
"categorical.case_variant",
|
|
255
|
+
{"groups": list(zip(keys, counts, forms, strict=True))},
|
|
256
|
+
affected=affected,
|
|
257
|
+
len=len(keys),
|
|
258
|
+
),
|
|
245
259
|
)
|
|
246
260
|
],
|
|
247
261
|
raw_score=len(keys),
|
{datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/formula.py
RENAMED
|
@@ -15,6 +15,7 @@ from datasentry_core.detectors.common import (
|
|
|
15
15
|
)
|
|
16
16
|
from datasentry_core.models.detector import DetectorCapabilities, IssueCandidate
|
|
17
17
|
from datasentry_core.models.enums import EvidenceType, QualityDimension, Severity
|
|
18
|
+
from datasentry_core.reporting.evidence_desc import ev
|
|
18
19
|
|
|
19
20
|
FORMULA_INJECTION_PREFIXES: tuple[str, ...] = ("=", "+", "-", "@", "\t", "\r")
|
|
20
21
|
|
|
@@ -54,8 +55,11 @@ class FormulaInjectionDetector(DetectorBase):
|
|
|
54
55
|
detector_id=self.detector_id,
|
|
55
56
|
detector_version=self.detector_version,
|
|
56
57
|
evidence_type=EvidenceType.PATTERN_MATCH,
|
|
57
|
-
description=
|
|
58
|
-
|
|
58
|
+
description=ev(
|
|
59
|
+
"formula.injection",
|
|
60
|
+
{"count": count},
|
|
61
|
+
count=count,
|
|
62
|
+
),
|
|
59
63
|
)
|
|
60
64
|
],
|
|
61
65
|
raw_score=count,
|
{datasentry_core-0.7.0 → datasentry_core-0.8.0}/src/datasentry_core/detectors/initial/missing.py
RENAMED
|
@@ -15,6 +15,7 @@ from datasentry_core.detectors.common import (
|
|
|
15
15
|
)
|
|
16
16
|
from datasentry_core.models.detector import DetectorCapabilities, IssueCandidate
|
|
17
17
|
from datasentry_core.models.enums import EvidenceType, QualityDimension, Severity
|
|
18
|
+
from datasentry_core.reporting.evidence_desc import ev
|
|
18
19
|
|
|
19
20
|
_MISSING_TOKENS = (
|
|
20
21
|
"na",
|
|
@@ -79,13 +80,17 @@ class ExcessiveNullRateDetector(DetectorBase):
|
|
|
79
80
|
detector_id=self.detector_id,
|
|
80
81
|
detector_version=self.detector_version,
|
|
81
82
|
evidence_type=EvidenceType.STATISTICAL_MEASURE,
|
|
82
|
-
description=
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
description=ev(
|
|
84
|
+
"missing.null_ratio",
|
|
85
|
+
{
|
|
86
|
+
"null_ratio": round(ratio, 6),
|
|
87
|
+
"threshold": threshold,
|
|
88
|
+
"nulls": nulls,
|
|
89
|
+
"total": total,
|
|
90
|
+
},
|
|
91
|
+
ratio=ratio,
|
|
92
|
+
threshold=threshold,
|
|
93
|
+
),
|
|
89
94
|
)
|
|
90
95
|
],
|
|
91
96
|
raw_score=ratio,
|
|
@@ -138,8 +143,12 @@ class SuspiciousMissingTokenDetector(DetectorBase):
|
|
|
138
143
|
detector_id=self.detector_id,
|
|
139
144
|
detector_version=self.detector_version,
|
|
140
145
|
evidence_type=EvidenceType.PATTERN_MATCH,
|
|
141
|
-
description=
|
|
142
|
-
|
|
146
|
+
description=ev(
|
|
147
|
+
"missing.standin",
|
|
148
|
+
{"count": count, "ratio": round(ratio, 6)},
|
|
149
|
+
count=count,
|
|
150
|
+
ratio=ratio,
|
|
151
|
+
),
|
|
143
152
|
)
|
|
144
153
|
],
|
|
145
154
|
raw_score=ratio,
|