Chiusura fase di ristrutturazione e modularizzazione del workflow per favorire sviluppo modulare

This commit is contained in:
2026-08-24 13:32:20 +02:00
parent fa2298653b
commit 6062cb010e
96 changed files with 304 additions and 259 deletions
+3
View File
@@ -42,6 +42,9 @@ line-length = 100
[tool.ruff.lint.per-file-ignores]
"tht/cli/__init__.py" = ["E402"]
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["typer.Argument", "typer.Option"]
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
+4 -4
View File
@@ -6,6 +6,7 @@ is not assumed reliable' gains real teeth for the data layer.
"""
import pytest
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
from tht.db.connection import can_create_in_schema, ping, writable_tables
@@ -43,10 +44,9 @@ def test_read_only_role_cannot_insert(ro_url):
by our engine)."""
engine = create_engine(ro_url)
try:
with pytest.raises(Exception):
with engine.begin() as conn:
conn.execute(text('INSERT INTO dw.dim_pazienti VALUES (999, %s, %s)'),
("test", "test"))
with pytest.raises(SQLAlchemyError), engine.begin() as conn:
conn.execute(text('INSERT INTO dw.dim_pazienti VALUES (999, %s, %s)'),
("test", "test"))
finally:
engine.dispose()
+1 -1
View File
@@ -40,7 +40,7 @@ def test_unique_values_for_lsh_returns_most_frequent(admin_engine):
schema = introspect(admin_engine, "testdb", "dw")
# Before classify_all, all text columns are eligible=True by default. Sampling
# only touches text types regardless.
values, skipped, truncated = unique_values_for_lsh(
values, _skipped, _truncated = unique_values_for_lsh(
admin_engine, schema, LshConfig(max_values_per_column=100)
)
# dim_pazienti.citta: Milano, Bergamo, Brescia (3 distinct, all eligible text)
@@ -38,7 +38,7 @@ def test_ablazione_returns_multiple_columns(l2_env):
schema_name = ws.database.db_schema
try:
lsh, minhashes, meta = load_index(index_dir, schema_name)
except Exception as e:
except Exception as e: # noqa: BLE001 - any unusable external index skips this L2 probe
pytest.skip(f"LSH index not built yet (run tht preprocess dwh --steps lsh -c {WORKSPACE}): {e}")
hits = query_index(lsh, minhashes, "ablazione", meta, top_n=20)
+1 -1
View File
@@ -1,7 +1,7 @@
import json
import uuid
from datetime import UTC, datetime
from types import SimpleNamespace
import uuid
from typer.testing import CliRunner
@@ -1,7 +1,7 @@
import uuid
from datetime import UTC, datetime
from pathlib import Path
from types import SimpleNamespace
import uuid
from tht.cli import session_cmd
from tht.decisions import DecisionInput
+4 -2
View File
@@ -1,6 +1,7 @@
from tht.decisions import DecisionType
import typing
from tht.decisions import DecisionType
def test_column_decision_types_exist():
allowed = set(typing.get_args(DecisionType))
@@ -9,8 +10,9 @@ def test_column_decision_types_exist():
def test_f4_emits_column_types():
import yaml
from pathlib import Path
import yaml
wf = yaml.safe_load(Path("workflow.yaml").read_text())
f4 = next(p for p in wf["phases"] if p["id"] == "F4")
assert "column_promoted" in f4["emits"]
+2 -2
View File
@@ -1,7 +1,5 @@
import pytest
from tht.evidence.adapters import FilesystemEvidenceSource, HttpManifestEvidenceSource
from tht.evidence import build_sources
from tht.config import (
ConfigError,
PgvectorDirectConfig,
@@ -12,6 +10,8 @@ from tht.config import (
load_config,
workspace_id_for_config,
)
from tht.evidence import build_sources
from tht.evidence.adapters import FilesystemEvidenceSource, HttpManifestEvidenceSource
def test_direct_vector_passwords_load_from_file_references(monkeypatch, tmp_path):
+4 -1
View File
@@ -209,7 +209,10 @@ def test_model_copy_revalidates_records_and_manifests():
def test_manifest_datetimes_are_aware_and_normalized_to_utc():
with pytest.raises(ValidationError, match="timezone-aware"):
CorpusManifest(created_at=datetime(2026, 7, 12), pipeline_version="evidence-v1")
CorpusManifest(
created_at=datetime(2026, 7, 12), # noqa: DTZ001 - verifies rejection
pipeline_version="evidence-v1",
)
plus_two = datetime(2026, 7, 12, 12, tzinfo=timezone(timedelta(hours=2)))
manifest = CorpusManifest(created_at=plus_two, pipeline_version="evidence-v1")
+1 -1
View File
@@ -3,8 +3,8 @@ from datetime import UTC, datetime
import pytest
from tht.evidence.corpus.normalize import MAX_DOCUMENT_BYTES, PermanentNormalizationError, normalize
from tht.evidence.contracts import AcquiredDocument, SourceObject
from tht.evidence.corpus.normalize import MAX_DOCUMENT_BYTES, PermanentNormalizationError, normalize
def acquired(content: bytes, *, media_type: str = "text/markdown") -> AcquiredDocument:
+20 -17
View File
@@ -2,11 +2,11 @@ from datetime import UTC, datetime, timedelta
import pytest
from tht.evidence.contracts import AcquiredDocument, SourceObject
from tht.evidence.corpus.chunk import ChunkPolicy
from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest
from tht.evidence.corpus.pipeline import CorpusPipeline, PipelineError, PipelineResult
from tht.evidence.corpus.store import CorpusStore
from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest
from tht.evidence.contracts import AcquiredDocument, SourceObject
from tht.ports.vector import VectorCapabilities, VectorHealth
@@ -273,6 +273,7 @@ def test_gc_preserves_vector_dependencies_of_retained_manifests(tmp_path):
def test_active_searcher_without_active_fails_closed_for_evidence(tmp_path):
from types import SimpleNamespace
from tht.evidence.search import active_searcher
class Delegate:
@@ -287,6 +288,7 @@ def test_active_searcher_without_active_fails_closed_for_evidence(tmp_path):
def test_active_searcher_splits_default_and_mixed_kinds_before_global_limit(tmp_path):
from types import SimpleNamespace
from tht.evidence.search import ActiveEvidenceSearcher
store = CorpusStore(tmp_path / "corpus")
@@ -319,6 +321,7 @@ def test_active_searcher_splits_default_and_mixed_kinds_before_global_limit(tmp_
def test_active_evidence_query_holds_lock_against_publish(tmp_path):
import threading
from types import SimpleNamespace
from tht.evidence.search import ActiveEvidenceSearcher
first_pipeline = pipeline(tmp_path, Source([(item("one", "a"), "old")]), vectors=Vectors())
@@ -519,9 +522,9 @@ def test_unchanged_job_reuses_active_generation_without_new_directory(tmp_path):
"hello",
)])
candidate = pipeline(tmp_path, source)
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
args = {"workspace_id": "demo", "workspace_root": tmp_path,
"config_fingerprint": "sha256:" + "1" * 64,
"input_fingerprint": "sha256:" + "2" * 64}
first = candidate.run_as_job(**args)
snapshot = first.manifest.metadata["source_snapshot"]["fs:one"]
assert snapshot == {
@@ -546,9 +549,9 @@ def test_job_source_snapshot_change_forces_publish_with_same_fingerprint(tmp_pat
metadata={"media_type": "text/markdown", "size": 5, "label": "original"},
)
vectors = Vectors()
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
args = {"workspace_id": "demo", "workspace_root": tmp_path,
"config_fingerprint": "sha256:" + "1" * 64,
"input_fingerprint": "sha256:" + "2" * 64}
first = pipeline(tmp_path, Source([(original, "hello")]), vectors=vectors).run_as_job(**args)
updates = {
"uri": "file:///safe/renamed.md",
@@ -567,9 +570,9 @@ def test_job_source_snapshot_change_forces_publish_with_same_fingerprint(tmp_pat
def test_job_binding_change_forces_publish(tmp_path, fingerprint_name):
vectors = Vectors()
source_object = item("one", "a")
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
args = {"workspace_id": "demo", "workspace_root": tmp_path,
"config_fingerprint": "sha256:" + "1" * 64,
"input_fingerprint": "sha256:" + "2" * 64}
first = pipeline(tmp_path, Source([(source_object, "hello")]), vectors=vectors).run_as_job(**args)
args[fingerprint_name] = "sha256:" + "3" * 64
source = Source([(source_object, "hello")])
@@ -587,9 +590,9 @@ def test_job_incomplete_active_contract_never_noops(tmp_path, damage):
vectors = Vectors()
source_object = item("one", "a")
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
args = {"workspace_id": "demo", "workspace_root": tmp_path,
"config_fingerprint": "sha256:" + "1" * 64,
"input_fingerprint": "sha256:" + "2" * 64}
candidate = pipeline(tmp_path, Source([(source_object, "hello")]), vectors=vectors)
first = candidate.run_as_job(**args)
if damage == "legacy_metadata":
@@ -628,9 +631,9 @@ def test_job_corrupt_canonical_document_or_chunk_never_noops(tmp_path, damage):
modified_at=datetime(2026, 1, 1, tzinfo=UTC),
metadata={"media_type": "text/markdown", "size": 11, "owner": "docs"},
)
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
args = {"workspace_id": "demo", "workspace_root": tmp_path,
"config_fingerprint": "sha256:" + "1" * 64,
"input_fingerprint": "sha256:" + "2" * 64}
candidate = pipeline(
tmp_path, Source([(source_object, "hello world")]), vectors=vectors,
policy=ChunkPolicy(version="chunk-v1", max_chars=6),
+6 -3
View File
@@ -1,6 +1,7 @@
import pytest
import os
import pytest
from tht.evidence.corpus.models import CorpusManifest
from tht.evidence.corpus.store import CorpusStore, UnsafeCorpusPath
@@ -111,9 +112,10 @@ def test_published_inventory_excludes_staged_and_invalid_newer_directories(tmp_p
def test_owned_copy_uses_validated_descriptor_bytes_when_source_is_replaced(tmp_path, monkeypatch):
from tht.evidence.corpus.models import CanonicalDocument
import hashlib
from tht.evidence.corpus.models import CanonicalDocument
content = "active bytes"
document = CanonicalDocument(
document_id="doc:" + "c" * 64, source_id="fs:copy", source_uri="file:///copy",
@@ -140,9 +142,10 @@ def test_owned_copy_uses_validated_descriptor_bytes_when_source_is_replaced(tmp_
def test_materialized_snapshot_uses_identified_manifest_when_active_changes(tmp_path):
from tht.evidence.corpus.models import CanonicalDocument
import hashlib
from tht.evidence.corpus.models import CanonicalDocument
def doc(content, fingerprint):
return CanonicalDocument(
document_id="doc:" + hashlib.sha256(content.encode()).hexdigest(),
+7 -7
View File
@@ -6,7 +6,7 @@ cte_plan_doc.json entry, and the last CteTestRecord for the CTE. --json output m
pristine (only valid JSON on stdout).
"""
import json
from datetime import datetime
from datetime import UTC, datetime
from typer.testing import CliRunner
@@ -18,7 +18,7 @@ from tht.session.store import create_session
def _db():
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
return DatabaseConfig(database="testdb", user="u", password="p", schema="public")
def _patch_cfg(monkeypatch, tmp_path):
@@ -46,7 +46,7 @@ def test_info_happy_path_json(tmp_path, monkeypatch):
sid, sdir = _make_session(tmp_path, ["a", "b"])
(sdir / "ctes" / "b.sql").write_text("WITH b AS (SELECT 1)")
append_cte_test(sdir, CteTestRecord(
name="b", ts=datetime(2025, 1, 1, 12, 0), sql_hash="h1", status="ok",
name="b", ts=datetime(2025, 1, 1, 12, 0, tzinfo=UTC), sql_hash="h1", status="ok",
columns=["x"], row_sample=1, execution_ms=5, preview_rows=[[1]],
))
_patch_cfg(monkeypatch, tmp_path)
@@ -105,10 +105,10 @@ def test_info_last_test_is_most_recent_record(tmp_path, monkeypatch):
sid, sdir = _make_session(tmp_path, ["a"])
(sdir / "ctes" / "a.sql").write_text("WITH a AS (SELECT 1)")
append_cte_test(sdir, CteTestRecord(
name="a", ts=datetime(2025, 1, 1), sql_hash="old", status="ok",
name="a", ts=datetime(2025, 1, 1, tzinfo=UTC), sql_hash="old", status="ok",
))
append_cte_test(sdir, CteTestRecord(
name="a", ts=datetime(2025, 1, 2), sql_hash="new", status="ok",
name="a", ts=datetime(2025, 1, 2, tzinfo=UTC), sql_hash="new", status="ok",
))
_patch_cfg(monkeypatch, tmp_path)
@@ -134,7 +134,7 @@ def test_info_missing_plan_exit_1(tmp_path, monkeypatch):
def test_info_name_not_in_plan_exit_1(tmp_path, monkeypatch):
sid, sdir = _make_session(tmp_path, ["a"])
sid, _sdir = _make_session(tmp_path, ["a"])
_patch_cfg(monkeypatch, tmp_path)
res = CliRunner().invoke(cte_app, ["info", "zzz", "--session", sid, "--json"])
assert res.exit_code == 1
@@ -142,7 +142,7 @@ def test_info_name_not_in_plan_exit_1(tmp_path, monkeypatch):
def test_info_missing_sql_file_exit_1(tmp_path, monkeypatch):
sid, sdir = _make_session(tmp_path, ["a"])
sid, _sdir = _make_session(tmp_path, ["a"])
_patch_cfg(monkeypatch, tmp_path)
res = CliRunner().invoke(cte_app, ["info", "a", "--session", sid, "--json"])
assert res.exit_code == 1
+1 -1
View File
@@ -10,7 +10,7 @@ from tht.session.store import create_session
def _db():
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
return DatabaseConfig(database="testdb", user="u", password="p", schema="public")
def _patch_cfg(monkeypatch, tmp_path):
+1 -1
View File
@@ -17,7 +17,7 @@ from tht.session.store import create_session
def _db():
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
return DatabaseConfig(database="testdb", user="u", password="p", schema="public")
def _patch_cfg(monkeypatch, tmp_path):
+6 -6
View File
@@ -7,7 +7,7 @@ and the ledger I/O (load/append, tolerant of JSON-array and JSONL formats).
Pure logic, no DB.
"""
import json
from datetime import date, datetime
from datetime import UTC, date, datetime
from decimal import Decimal
import pytest
@@ -82,10 +82,10 @@ def test_build_test_sql_single_cte():
# --- ledger I/O: load_cte_tests / append_cte_test ---------------------------
def _record(**kw) -> CteTestRecord:
base = dict(
name="ablazione_q", ts=datetime(2025, 1, 1, 12, 0), sql_hash="abc123",
status="ok", columns=["x"], row_sample=5, execution_ms=42,
)
base = {
"name": "ablazione_q", "ts": datetime(2025, 1, 1, 12, 0, tzinfo=UTC), "sql_hash": "abc123",
"status": "ok", "columns": ["x"], "row_sample": 5, "execution_ms": 42,
}
base.update(kw)
return CteTestRecord(**base)
@@ -147,7 +147,7 @@ def test_jsonable_passes_through_native_types():
def test_jsonable_coerces_decimal_and_date_to_str():
assert _jsonable(Decimal("12.34")) == "12.34"
assert _jsonable(date(2025, 1, 1)) == "2025-01-01"
assert _jsonable(datetime(2025, 1, 1, 12, 0, 0)) == "2025-01-01 12:00:00"
assert _jsonable(datetime(2025, 1, 1, 12, 0, 0)) == "2025-01-01 12:00:00" # noqa: DTZ001
def test_jsonable_truncates_long_strings():
+1 -1
View File
@@ -17,7 +17,7 @@ def _concurrent_append_worker(session, subject, start, ready, done):
try:
append_decision(session, type="concept_clarified", subject=subject)
done.put((subject, None))
except Exception as error: # pragma: no cover - surfaced through the parent assertion
except Exception as error: # noqa: BLE001 # pragma: no cover - sent to parent
done.put((subject, repr(error)))
+2 -1
View File
@@ -57,8 +57,9 @@ def test_empty_session_returns_empty_list(tmp_path):
def test_decision_type_literal_includes_retracted():
"""decision_retracted e' un tipo valido (pydantic lo accetta)."""
from datetime import UTC, datetime
from tht.decisions import DecisionRecord
from datetime import datetime, UTC
d = DecisionRecord(
seq=1, ts=datetime.now(UTC), type="decision_retracted",
subject="phase:4", retracts=1,
-1
View File
@@ -5,7 +5,6 @@ from typer.testing import CliRunner
from tht.cli import app
runner = CliRunner()
+1 -1
View File
@@ -1,12 +1,12 @@
import pytest
from sqlalchemy.exc import OperationalError
from tht.adapters.dwh import PostgresDwhAdapter
from tht.config import DatabaseConfig, RestConfig
from tht.db.sampling import distinct_values_rest, sample_column_rest
from tht.execute import ExecutionError
from tht.ports import DistinctValues, DwhAdapter
from tht.rest.client import RestError
from tht.adapters.dwh import PostgresDwhAdapter
def postgres_factory():
+2 -2
View File
@@ -5,10 +5,10 @@ import pytest
from tht.execute import ExecResult, PlanSummary
from tht.mschema.models import PhysicalSchema
from tht.ports.dwh import (
DistinctValues,
DwhAdapter,
DwhCapabilities,
DwhHealth,
DistinctValues,
UnsupportedCapability,
)
@@ -55,10 +55,10 @@ def test_contract_types_are_public_and_capabilities_are_immutable():
def test_all_contract_types_are_exported_from_public_package():
from tht.ports import DistinctValues as PublicDistinctValues
from tht.ports import DwhAdapter as PublicDwhAdapter
from tht.ports import DwhCapabilities as PublicDwhCapabilities
from tht.ports import DwhHealth as PublicDwhHealth
from tht.ports import DistinctValues as PublicDistinctValues
from tht.ports import UnsupportedCapability as PublicUnsupportedCapability
result = PublicDistinctValues(values=["a"], truncated=True)
+23 -15
View File
@@ -6,13 +6,16 @@ from typer.testing import CliRunner
from tht.cli import app
from tht.config import load_config
from tht.jobs.dwh_pipeline import DwhPreprocessPipeline
from tht.jobs.dwh_pipeline import active_generation_dir, config_dwh_binding, fingerprint
from tht.jobs.dwh_pipeline import resolve_dwh_snapshot
from tht.jobs.dwh_pipeline import lease_dwh_snapshot
from tht.jobs.dwh_pipeline import (
DwhPreprocessPipeline,
active_generation_dir,
config_dwh_binding,
fingerprint,
lease_dwh_snapshot,
resolve_dwh_snapshot,
)
from tht.jobs.locking import _lock_name
FP = "sha256:" + hashlib.sha256(b"test").hexdigest()
@@ -104,9 +107,8 @@ def test_unowned_reads_fail_closed_without_creating_any_files(tmp_path):
cfg = snapshot_config(tmp_path)
with pytest.raises(Exception, match="not initialized"):
resolve_dwh_snapshot(cfg)
with pytest.raises(Exception, match="not initialized"):
with lease_dwh_snapshot(cfg):
pass
with pytest.raises(Exception, match="not initialized"), lease_dwh_snapshot(cfg):
pass
assert not (tmp_path / ".tht-dwh").exists()
@@ -125,7 +127,7 @@ def test_writer_claim_allows_only_lock_and_empty_generations(tmp_path):
pipeline = DwhPreprocessPipeline(
workspace_id="demo", workspace_root=root.parent,
config_fingerprint=FP, input_fingerprint=FP,
introspect=lambda output: calls.append("called"),
introspect=lambda output, calls=calls: calls.append("called"),
build_lsh=lambda physical, output: None,
)
with pytest.raises(Exception, match="unbound"):
@@ -188,6 +190,7 @@ def test_owner_publication_remains_on_locked_root_when_path_is_swapped(
monkeypatch, tmp_path,
):
import pytest
import tht.jobs.dwh_pipeline as module
real_replace = module.os.replace
@@ -211,7 +214,7 @@ def test_owner_publication_remains_on_locked_root_when_path_is_swapped(
introspect=lambda output: (_ for _ in ()).throw(AssertionError("callback called")),
build_lsh=lambda physical, output: None,
)
with pytest.raises(Exception):
with pytest.raises(Exception, match="root"):
pipeline.run()
assert swapped
assert (moved / "OWNER.json").is_file()
@@ -394,7 +397,7 @@ def test_missing_active_with_generations_and_symlink_owner_marker_fail_closed(tm
def _capture_error(operation):
try:
return operation()
except Exception as error:
except Exception as error: # noqa: BLE001 - helper returns the exact injected failure
return error
@@ -513,6 +516,7 @@ def test_unsafe_lsh_filename_is_rejected(tmp_path):
def test_active_fsync_failure_restores_previous_pointer(monkeypatch, tmp_path):
import os
import tht.jobs.dwh_pipeline as module
def build(physical, output):
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json"):
@@ -585,7 +589,7 @@ def test_snapshot_root_swap_after_lease_never_reads_replacement(monkeypatch, tmp
try:
with lease_dwh_snapshot(snapshot_config(tmp_path)) as snapshot:
assert snapshot.physical.read_text() == "trusted"
except Exception as error:
except Exception as error: # noqa: BLE001 - either safe refusal path is acceptable
assert "ACTIVE" in str(error) or "root" in str(error)
assert swapped
assert (replacement / "sentinel").read_text() == "replacement-secret"
@@ -623,7 +627,9 @@ def test_snapshot_copies_each_validated_artifact_once_without_reopen(monkeypatch
def test_reconcile_mismatch_closes_active_generation_fd(monkeypatch, tmp_path):
import os
from types import SimpleNamespace
import pytest
import tht.jobs.dwh_pipeline as module
pipeline = DwhPreprocessPipeline(
@@ -637,7 +643,7 @@ def test_reconcile_mismatch_closes_active_generation_fd(monkeypatch, tmp_path):
real_active = module._active_generation_fd
def mismatched_active(root_fd, binding):
generation, generation_fd = real_active(root_fd, binding)
_generation, generation_fd = real_active(root_fd, binding)
return "f" * 32, generation_fd
monkeypatch.setattr(module, "_active_generation_fd", mismatched_active)
@@ -676,9 +682,10 @@ def test_pipeline_releases_materialized_snapshot_after_every_run(tmp_path):
def test_corrupt_resume_checkpoint_releases_materialized_snapshot(tmp_path):
import tht.jobs.dwh_pipeline as module
import pytest
import tht.jobs.dwh_pipeline as module
pipeline = DwhPreprocessPipeline(
workspace_id="demo", workspace_root=tmp_path,
config_fingerprint=FP, input_fingerprint=FP,
@@ -699,9 +706,10 @@ def test_corrupt_resume_checkpoint_releases_materialized_snapshot(tmp_path):
def test_job_spec_construction_failure_releases_materialized_snapshot(monkeypatch, tmp_path):
import tht.jobs.dwh_pipeline as module
import pytest
import tht.jobs.dwh_pipeline as module
pipeline = DwhPreprocessPipeline(
workspace_id="demo", workspace_root=tmp_path,
config_fingerprint=FP, input_fingerprint=FP,
@@ -1,13 +1,11 @@
from datetime import UTC, datetime
import hashlib
import inspect
from datetime import UTC, datetime
from pathlib import Path
from types import SimpleNamespace
import pytest
from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
from tht.evidence.corpus.store import CorpusStore
from tht.decisions import DecisionRecord
from tht.evidence import (
acquire,
@@ -25,6 +23,8 @@ from tht.evidence.contracts import (
EvidenceSourceErrorCategory,
SourceObject,
)
from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
from tht.evidence.corpus.store import CorpusStore
from tht.session.models import Candidate, SchemaLinking
-1
View File
@@ -1,6 +1,5 @@
from pathlib import Path
HARNESS_ROOT = Path(__file__).resolve().parents[1]
LEGACY_PATHS = (
"tht/ports/evidence.py",
+6 -2
View File
@@ -154,7 +154,7 @@ def test_datetimes_must_be_aware_and_are_normalized_to_utc():
source_id="source:a",
uri="https://host/a",
fingerprint="etag:abc",
modified_at=datetime(2026, 7, 12),
modified_at=datetime(2026, 7, 12), # noqa: DTZ001 - verifies rejection
)
source = SourceObject(
@@ -236,4 +236,8 @@ def test_model_copy_revalidates_source_and_acquired_records():
with pytest.raises(ValidationError, match="namespaced"):
source.model_copy(update={"source_id": "invalid"})
with pytest.raises(ValidationError, match="timezone-aware"):
acquired.model_copy(update={"acquired_at": datetime(2026, 7, 12)})
acquired.model_copy(
update={
"acquired_at": datetime(2026, 7, 12) # noqa: DTZ001 - verifies rejection
}
)
+1 -1
View File
@@ -46,7 +46,7 @@ def test_union_query_accepts_limit():
def test_with_cte_query_accepts_limit():
sql = "WITH cte AS (SELECT 1) SELECT * FROM cte"
out, injected = _inject_limit(sql, 10)
_out, injected = _inject_limit(sql, 10)
assert injected is True
+1
View File
@@ -70,6 +70,7 @@ def test_retrieve_empty_on_missing_dir(tmp_path):
def test_concept_formula_decision_types_exist():
import typing
from tht.decisions import DecisionType
args = typing.get_args(DecisionType)
assert "concept_formula_approved" in args
+6 -5
View File
@@ -1,6 +1,7 @@
import threading
import socket
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import ClassVar
import pytest
@@ -12,8 +13,8 @@ class Handler(BaseHTTPRequestHandler):
etag_requests = 0
etag_body_responses = 0
redirect_target = "/redirected-v1"
redirect_request_validators = []
final_request_validators = []
redirect_request_validators: ClassVar[list[str | None]] = []
final_request_validators: ClassVar[list[tuple[str, str | None]]] = []
def do_GET(self):
if self.path.startswith("/etag"):
@@ -198,7 +199,6 @@ class FakeSocket:
class FakeResponse:
status_code = 200
headers = {}
is_redirect = False
def __init__(self, *, peer="127.0.0.1", stream_error=None, location=None):
@@ -214,6 +214,7 @@ class FakeResponse:
self.is_redirect = False
self.status_code = 200
self.headers = {}
self.headers = {}
def iter_content(self, chunk_size):
if self.stream_error:
@@ -265,7 +266,7 @@ def test_http_closes_response_when_streaming_fails():
source = HttpManifestEvidenceSource(
["https://example.test/doc"], allow_private_hosts=True
)
response = FakeResponse(stream_error=socket.timeout("read timed out"))
response = FakeResponse(stream_error=TimeoutError("read timed out"))
source._session = FakeSession(response)
with pytest.raises(EvidenceSourceError):
list(source.discover())
+5 -3
View File
@@ -37,9 +37,11 @@ def test_same_workspace_and_job_are_exclusive_across_processes(tmp_path):
def test_evidence_and_dwh_jobs_have_distinct_locks(tmp_path):
with WorkspaceJobLock(tmp_path, "demo", "evidence"):
with WorkspaceJobLock(tmp_path, "demo", "dwh"):
pass
with (
WorkspaceJobLock(tmp_path, "demo", "evidence"),
WorkspaceJobLock(tmp_path, "demo", "dwh"),
):
pass
def test_lock_keys_cannot_escape_lock_directory(tmp_path):
+2 -1
View File
@@ -1,11 +1,12 @@
import json
import os
import pytest
from pydantic import ValidationError
import tht.jobs.runner as runner_module
from tht.jobs.models import JobSpec
from tht.jobs.runner import CorruptCheckpointError, StageArtifacts, run_job
import tht.jobs.runner as runner_module
def _spec(tmp_path, **updates):
+2 -6
View File
@@ -3,7 +3,7 @@ import hashlib
import pytest
from tht.jobs.dwh_pipeline import DwhPreprocessPipeline
from tht.jobs.runner import CorruptCheckpointError
FP = "sha256:" + hashlib.sha256(b"test").hexdigest()
@@ -67,12 +67,8 @@ def test_resume_rejects_a_different_stage_selection(tmp_path):
introspect=lambda output: output.write_text("catalog"),
build_lsh=lambda physical, output: None,
)
try:
with pytest.raises(CorruptCheckpointError, match="incompatible"):
pipeline.run(("lsh",), resume_run_id=failed.run_id)
except Exception as error:
assert "incompatible" in str(error)
else:
raise AssertionError("resume with different stages must fail")
def test_resume_rejects_tampered_succeeded_stage_artifact(tmp_path):
+1 -1
View File
@@ -1,5 +1,5 @@
from tht.session.store import create_session
from tht.session.models import SessionManifest
from tht.session.store import create_session
def test_manifest_persists_pi_fields(tmp_path):
+7 -7
View File
@@ -5,18 +5,18 @@ fonte delle memory. L'hit di search_similar deve bastare per ricostruire la deci
completa. Per questo memory_vector_records mette subject/detail/rationale nel metadata
del VectorRecord (pack_metadata li serializza nel jsonb via **record.metadata).
"""
from datetime import datetime
from datetime import UTC, datetime
from tht.memory import MemoryRecord, memory_vector_records
def _record(**kw) -> MemoryRecord:
base = dict(
id="mem-x", ts=datetime(2025, 1, 1), session_id="s", decision_seq=1,
type="concept_clarified", subject="paziente attivo", detail="flag_attivo = TRUE",
rationale="perche' serve", question_context="dammi pazienti",
tables=[], concepts=["paziente attivo"],
)
base = {
"id": "mem-x", "ts": datetime(2025, 1, 1, tzinfo=UTC), "session_id": "s", "decision_seq": 1,
"type": "concept_clarified", "subject": "paziente attivo", "detail": "flag_attivo = TRUE",
"rationale": "perche' serve", "question_context": "dammi pazienti",
"tables": [], "concepts": ["paziente attivo"],
}
base.update(kw)
return MemoryRecord(**base)
+8 -8
View File
@@ -6,7 +6,7 @@ la rende un passo del workflow. Questi test fissano il contratto harness-side:
- i candidati rifiutati al gate (memory_promotion_declined, detail "seq:<n>") non
vengono riproposti da reusable_promotions/preview_promotions.
"""
from datetime import datetime
from datetime import UTC, datetime
from tht.decisions import DecisionRecord, append_decision
from tht.memory import (
@@ -23,7 +23,7 @@ from tht.workflow import load_workflow
def test_promotion_decision_types_are_valid():
for t in ("memory_promoted", "memory_promotion_declined"):
d = DecisionRecord(
seq=1, ts=datetime(2026, 1, 1), type=t, subject="fact_x", detail="seq:5"
seq=1, ts=datetime(2026, 1, 1, tzinfo=UTC), type=t, subject="fact_x", detail="seq:5"
)
assert d.type == t
@@ -36,14 +36,14 @@ def test_promotion_decision_types_min_phase_is_f8():
def _manifest() -> SessionManifest:
return SessionManifest(
id="s1", created_at=datetime(2026, 1, 1), question="domanda originale",
id="s1", created_at=datetime(2026, 1, 1, tzinfo=UTC), question="domanda originale",
database="db", schema="public",
)
def test_declined_promotion_seqs_parses_seq_detail():
d = DecisionRecord(
seq=9, ts=datetime(2026, 1, 1), type="memory_promotion_declined",
seq=9, ts=datetime(2026, 1, 1, tzinfo=UTC), type="memory_promotion_declined",
subject="fact_x", detail="seq:5",
)
assert declined_promotion_seqs([d]) == {5}
@@ -51,9 +51,9 @@ def test_declined_promotion_seqs_parses_seq_detail():
def test_declined_promotion_seqs_ignores_malformed_and_other_types():
ds = [
DecisionRecord(seq=1, ts=datetime(2026, 1, 1),
DecisionRecord(seq=1, ts=datetime(2026, 1, 1, tzinfo=UTC),
type="memory_promotion_declined", subject="x", detail=""),
DecisionRecord(seq=2, ts=datetime(2026, 1, 1),
DecisionRecord(seq=2, ts=datetime(2026, 1, 1, tzinfo=UTC),
type="table_promoted", subject="x", detail="seq:3"),
]
assert declined_promotion_seqs(ds) == set()
@@ -100,11 +100,11 @@ def test_only_concept_clarified_is_proposed_or_promoted(tmp_path):
def test_legacy_table_records_are_not_published_as_memory_vectors():
records = [
MemoryRecord(
id="mem-0001", ts=datetime(2026, 1, 1), session_id="s1",
id="mem-0001", ts=datetime(2026, 1, 1, tzinfo=UTC), session_id="s1",
decision_seq=1, type="table_promoted", subject="fact_a",
),
MemoryRecord(
id="mem-0002", ts=datetime(2026, 1, 1), session_id="s1",
id="mem-0002", ts=datetime(2026, 1, 1, tzinfo=UTC), session_id="s1",
decision_seq=2, type="concept_clarified", subject="paziente attivo",
detail="flag_attivo = TRUE",
),
-1
View File
@@ -7,7 +7,6 @@ from pathlib import Path
import pytest
THT_ROOT = Path(__file__).resolve().parents[1] / "tht"
DOMAIN_PACKAGES = ("evidence", "memory")
+5 -5
View File
@@ -4,7 +4,7 @@ Wide text (lettere di dimissione, note, anamnesi) is excluded everywhere; data
comes only from numerics, enums, temporals, booleans, and short text. Annotation
override wins over the physical classification.
"""
from datetime import datetime
from datetime import UTC, datetime
from tht.config import EligibilityConfig
from tht.mschema.eligibility import classify_all, classify_column, effective_eligibility
@@ -70,16 +70,16 @@ def test_array_type_is_wide_text():
def _schema_with(**columns) -> PhysicalSchema:
return PhysicalSchema(
database="db", schema="dw", introspected_at=datetime(2025, 1, 1),
database="db", schema="dw", introspected_at=datetime(2025, 1, 1, tzinfo=UTC),
tables={"t": TablePhysical(columns={k: ColumnPhysical(**v) for k, v in columns.items()})},
)
def test_classify_all_marks_wide_text_and_clears_examples():
schema = _schema_with(
note=dict(type="text", examples=["a" * 500, "b" * 400]),
cod=dict(type="varchar(10)", examples=["X", "Y"]),
etl_last_update=dict(type="timestamp", examples=[]),
note={"type": "text", "examples": ["a" * 500, "b" * 400]},
cod={"type": "varchar(10)", "examples": ["X", "Y"]},
etl_last_update={"type": "timestamp", "examples": []},
)
classify_all(schema, CFG)
cols = schema.tables["t"].columns
+2 -2
View File
@@ -3,7 +3,7 @@
Catches port breaks in the render layer (markdown reviewer report, mschema-text
ThothAI style, schema-dict for AV-SQL). Pure logic, no I/O.
"""
from datetime import datetime
from datetime import UTC, datetime
from tht.mschema.models import (
Annotations,
@@ -20,7 +20,7 @@ def _fake_schema() -> PhysicalSchema:
return PhysicalSchema(
database="testdb",
schema="dw",
introspected_at=datetime(2025, 1, 1, 0, 0, 0),
introspected_at=datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC),
tables={
"dim_pazienti": TablePhysical(
comment="Anagrafica pazienti",
+1 -1
View File
@@ -4,9 +4,9 @@ from types import SimpleNamespace
from typer.testing import CliRunner
from tht.config import EmbeddingsConfig
from tht.cli import ollama_cmd
from tht.cli.ollama_cmd import ensure_ollama, ollama_app
from tht.config import EmbeddingsConfig
def _cfg(**kw):
+1 -1
View File
@@ -1,4 +1,4 @@
from tht.decisions import append_decision, DecisionRecord
from tht.decisions import DecisionRecord, append_decision
from tht.phase import current_phase, effective_decisions
+1 -1
View File
@@ -81,7 +81,7 @@ def test_successful_reopen_records_ledger_and_runs_teardown(tmp_path, monkeypatc
calls = []
class _Report:
deleted_files = []
deleted_files = ()
def _spy(_repository, snapshot, phase):
# By the time teardown runs, the ledger already holds the reopen decision.
@@ -9,7 +9,6 @@ from tht.pi_skill_projection import (
render_projection,
)
BASELINE_SHA256 = "626a794071c095a4f20fffabb3bab901f05c101590adbdc58e45adfae56f3219"
+2 -1
View File
@@ -1,4 +1,5 @@
import json
from tht.cli.sql_cmd import promoted_columns_for
@@ -21,6 +22,6 @@ def test_promoted_columns_for(tmp_path):
}))
class Cfg:
class paths: # noqa: N801
class paths:
sessions = tmp_path
assert promoted_columns_for(Cfg, sid) == {"dim_patient.cod_paz"}
@@ -6,10 +6,10 @@ import yaml
from pydantic import SecretStr
from typer.testing import CliRunner
from tht.evidence.adapters import HttpManifestEvidenceSource
from tht.evidence import build_sources
from tht.cli import app
from tht.config import ConfigError, load_config
from tht.evidence import build_sources
from tht.evidence.adapters import HttpManifestEvidenceSource
SIGNED_CANARY = "SIGNED-CANARY-QUERY"
ACCESS_CANARY = "ACCESS-CANARY"
+3 -1
View File
@@ -99,7 +99,9 @@ def test_s3_rejects_leading_slash_prefix_empty_and_control_keys():
S3EvidenceSource(bucket="evidence", prefix="/clinical", client=Client())
for key in ("", "clinical/a\x00.md", "clinical/a\x7f.md"):
client = Client()
client.list_objects_v2 = lambda **kwargs: {"Contents": [{"Key": key, "ETag": '"x"'}]}
client.list_objects_v2 = lambda key=key, **kwargs: {
"Contents": [{"Key": key, "ETag": '"x"'}]
}
with pytest.raises(EvidenceSourceError):
list(S3EvidenceSource(bucket="evidence", prefix="clinical/", client=client).discover())
+3 -3
View File
@@ -1,5 +1,5 @@
import json
from datetime import datetime
from datetime import UTC, datetime
from typer.testing import CliRunner
@@ -9,7 +9,7 @@ from tht.mschema.models import ColumnPhysical, PhysicalSchema, TablePhysical
def _write_catalog(tmp_path):
phys = PhysicalSchema(
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
database="d", schema="s", introspected_at=datetime(2026, 1, 1, tzinfo=UTC),
tables={
"dim_patient": TablePhysical(
comment="Anagrafica",
@@ -48,7 +48,7 @@ def _write_family_catalog(tmp_path):
)
phys = PhysicalSchema(
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
database="d", schema="s", introspected_at=datetime(2026, 1, 1, tzinfo=UTC),
tables={
"fact_sost_impianto_pmk": _t("Sost PMK"),
"fact_sost_impianto_crt_d": _t("Sost CRT-D"),
@@ -1,16 +1,16 @@
from datetime import datetime
from datetime import UTC, datetime
from typer.testing import CliRunner
from tht.cli import app
from tht.mschema.models import ColumnPhysical, PhysicalSchema, TablePhysical
from tht.config import ExamplesConfig
from tht.cli.schema_cmd import _add_examples
from tht.config import ExamplesConfig
from tht.mschema.models import ColumnPhysical, PhysicalSchema, TablePhysical
def _write_catalog(tmp_path):
phys = PhysicalSchema(
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
database="d", schema="s", introspected_at=datetime(2026, 1, 1, tzinfo=UTC),
tables={
"dim_patient": TablePhysical(
comment="Anagrafica",
@@ -48,7 +48,7 @@ def test_introspect_fresh_root_initializes_through_writer_job(tmp_path, monkeypa
cfg = _write_config(tmp_path)
physical = PhysicalSchema(
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
database="d", schema="s", introspected_at=datetime(2026, 1, 1, tzinfo=UTC),
tables={"dim_patient": TablePhysical(columns={"id": ColumnPhysical(type="bigint")})},
)
@@ -90,7 +90,7 @@ def test_render_without_catalog_guides_fallback(tmp_path):
def test_examples_skip_one_unreadable_column_and_continue(caplog):
physical = PhysicalSchema(
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
database="d", schema="s", introspected_at=datetime(2026, 1, 1, tzinfo=UTC),
tables={"t": TablePhysical(columns={
"bad": ColumnPhysical(type="text"), "good": ColumnPhysical(type="text")
})},
+11 -6
View File
@@ -1,20 +1,25 @@
"""Tests for `tht session documents --json` and build_documents."""
import json
from datetime import datetime
from datetime import UTC, datetime
from typer.testing import CliRunner
from tht.cli.session_cmd import session_app
from tht.config import DatabaseConfig
from tht.decisions import DecisionRecord
from tht.session.models import SessionSnapshot
from tht.session.store import build_documents, build_snapshot_documents, create_session, new_session_manifest
from tht.cli.session_cmd import session_app
from tht.session.store import (
build_documents,
build_snapshot_documents,
create_session,
new_session_manifest,
)
def _db():
return DatabaseConfig(
database="testdb", user="u", password="p", # noqa: S106
**{"schema": "public"},
database="testdb", user="u", password="p",
schema="public",
)
@@ -53,7 +58,7 @@ def test_build_documents_includes_existing_artifacts_only(tmp_path):
def _decision(seq: int, type_: str, subject: str, detail: str = "", rationale: str = ""):
return DecisionRecord(
seq=seq,
ts=datetime(2026, 1, 1),
ts=datetime(2026, 1, 1, tzinfo=UTC),
type=type_,
subject=subject,
detail=detail,
+2 -2
View File
@@ -14,8 +14,8 @@ def _make_db():
return DatabaseConfig(
database="testdb",
user="testuser",
password="testpass", # noqa: S106
**{"schema": "public"},
password="testpass",
schema="public",
)
+2 -2
View File
@@ -17,8 +17,8 @@ from tht.session.store import (
def _db():
return DatabaseConfig(
database="testdb", user="u", password="p", # noqa: S106
**{"schema": "public"},
database="testdb", user="u", password="p",
schema="public",
)
+4 -4
View File
@@ -8,15 +8,15 @@ import json
from typer.testing import CliRunner
from tht.config import DatabaseConfig
from tht.cli.session_cmd import session_app
from tht.config import DatabaseConfig
from tht.session.store import _extract_name, _summarize, load_session
def _db():
return DatabaseConfig(
database="testdb", user="u", password="p", # noqa: S106
**{"schema": "public"},
database="testdb", user="u", password="p",
schema="public",
)
@@ -41,7 +41,7 @@ def test_extract_name_is_a_3_to_5_word_italian_summary():
def test_extract_name_falls_back_to_summarize_on_failure(monkeypatch):
import tht.session.store as store
from tht.session import store
def boom(*a, **k):
raise RuntimeError("yake down")
+2 -3
View File
@@ -7,12 +7,11 @@ from tht.decisions import DecisionInput
from tht.session.filesystem_repository import FilesystemSessionRepository
from tht.session.models import PrincipalContext, SessionManifest, local_principal
from tht.session.repository import build_session_repository, resolve_principal
from tht.session.store import SessionError
from tht.session.store import create_session
from tht.session.store import SessionError, create_session
def _db() -> DatabaseConfig:
return DatabaseConfig(database="testdb", schema="public", user="u", password="p") # noqa: S106
return DatabaseConfig(database="testdb", schema="public", user="u", password="p")
def _config(tmp_path):
@@ -1,7 +1,7 @@
import uuid
from tht.decisions import DecisionInput
from tht.phase import current_phase, cte_plan, next_cte
from tht.phase import cte_plan, current_phase, next_cte
from tht.session.filesystem_repository import FilesystemSessionRepository
from tht.session.models import PrincipalContext, SessionManifest
from tht.session.store import persist_verified_finalization
+1 -1
View File
@@ -10,7 +10,7 @@ from tht.session.store import create_session, set_schema_linking
def _db():
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
return DatabaseConfig(database="testdb", user="u", password="p", schema="public")
def test_writes_and_revalidates(tmp_path):
+1 -1
View File
@@ -9,7 +9,7 @@ from tht.session.store import create_session
def _db():
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
return DatabaseConfig(database="testdb", user="u", password="p", schema="public")
def _patch_cfg(monkeypatch, tmp_path):
-1
View File
@@ -6,7 +6,6 @@ Pure-logic tests (no DB needed):
"""
import json
from tht.execute.limit import inject_limit_offset
+2 -2
View File
@@ -17,12 +17,12 @@ from tht.sqlcheck import validate_sql
def _schema() -> PhysicalSchema:
"""A tiny known schema for the object-existence checks."""
from datetime import datetime
from datetime import UTC, datetime
return PhysicalSchema(
database="db",
schema="dw",
introspected_at=datetime(2025, 1, 1),
introspected_at=datetime(2025, 1, 1, tzinfo=UTC),
tables={
"dim_pazienti": TablePhysical(
columns={
+2 -2
View File
@@ -8,8 +8,8 @@ from tht.session.store import create_session, set_schema_linking, sync_schema_li
def _db():
return DatabaseConfig(
database="testdb", user="u", password="p", # noqa: S106
**{"schema": "public"},
database="testdb", user="u", password="p",
schema="public",
)
+2 -1
View File
@@ -64,7 +64,8 @@ def test_empty_hits_returns_empty():
def test_value_grounded_decision_type_exists():
# D14a adds the value_grounded decision type so the gate can record the
# reviewer's choice of which column(s) anchor a cited value.
from tht.decisions import DecisionType
import typing
from tht.decisions import DecisionType
args = typing.get_args(DecisionType)
assert "value_grounded" in args
@@ -1,15 +1,15 @@
"""Executable baseline for persisted workflow behavior touched by the refactor."""
import hashlib
from dataclasses import asdict
from datetime import UTC, datetime
import hashlib
import pytest
from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
from tht.evidence.corpus.store import CorpusStore
from tht.decisions import DecisionRecord, append_decision
from tht.evidence import project_session
from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
from tht.evidence.corpus.store import CorpusStore
from tht.phase import current_phase, effective_decisions
from tht.session.models import Candidate, SchemaLinking
from tht.workflow import load_workflow
+1 -1
View File
@@ -1,8 +1,8 @@
"""Direct PostgreSQL implementation of the DWH port."""
from tht.config import DatabaseConfig
from sqlalchemy.exc import OperationalError, SQLAlchemyError
from tht.config import DatabaseConfig
from tht.db import execute, sampling
from tht.db.connection import can_create_in_schema, make_engine, ping, writable_tables
from tht.db.introspect import introspect
+1 -1
View File
@@ -1,8 +1,8 @@
"""Thoth/PostgREST implementation of the DWH port."""
from tht.config import DatabaseIdentityConfig, RestConfig
from tht.db.introspect import introspect_rest
from tht.db import sampling
from tht.db.introspect import introspect_rest
from tht.execute import ExecResult, ExecutionError, PlanSummary
from tht.mschema.models import PhysicalSchema
from tht.ports.dwh import DistinctValues, DwhCapabilities, DwhHealth
+1
View File
@@ -1,6 +1,7 @@
from pathlib import Path
import typer
from tht.adapters.factory import build_dwh
from tht.cli.config_cmd import CONFIG_OPT
from tht.config import ConfigError, load_config
+1 -1
View File
@@ -16,7 +16,7 @@ def _extract_lsh_values(dwh, physical, annotations, limit):
continue
try:
distinct = dwh.distinct_values(table_name, column_name, limit=limit)
except Exception as exc:
except Exception as exc: # noqa: BLE001 - an unreadable DWH column is non-fatal
skipped.append(SkippedColumn(table_name, column_name, f"errore: {exc}"))
continue
vals = [str(value) for value in distinct.values if value not in (None, "")]
+1 -1
View File
@@ -459,8 +459,8 @@ def solved_search_cmd(
from rich.table import Table
from tht.cli.vector_cmd import make_embedder, open_searcher
from tht.ports.vector import VectorReadUnavailable, VectorStoreError
from tht.memory import search_solved_questions
from tht.ports.vector import VectorReadUnavailable, VectorStoreError
from tht.vectorstore.embeddings import EmbeddingsError
cfg = _load_config_or_exit(config)
+7 -8
View File
@@ -12,8 +12,8 @@ import json
import typer
from tht.phase import (
auto_advance_eligible,
advance_problems,
auto_advance_eligible,
current_phase,
)
from tht.workflow import load_workflow
@@ -93,12 +93,11 @@ def advance_cmd(
if cur > wf.max_phase:
typer.secho("Sessione già alla fase terminale.", fg=typer.colors.YELLOW)
raise typer.Exit(0)
if auto:
if not auto_advance_eligible(snapshot):
problems = advance_problems(snapshot, cur)
for p in problems:
typer.echo(p)
raise typer.Exit(6) # needs human confirmation (gate contract)
if auto and not auto_advance_eligible(snapshot):
problems = advance_problems(snapshot, cur)
for p in problems:
typer.echo(p)
raise typer.Exit(6) # needs human confirmation (gate contract)
session_repository(cfg).append_decisions(
session, [{"type": "phase_approved", "subject": f"phase:{cur}"}]
)
@@ -164,7 +163,7 @@ def _cfg():
ws = os.environ.get("THT_WORKSPACE") or os.environ.get("THT_CONFIG")
config_path = Path(ws) if ws else Path("config/tht.yaml")
try:
from tht.cli.schema_cmd import _load_config_or_exit # noqa: F401 (portato in Onda 1.4)
from tht.cli.schema_cmd import _load_config_or_exit
return _load_config_or_exit(config_path)
except ImportError:
+2 -2
View File
@@ -96,9 +96,9 @@ def run_from_config(config: Path, *, dry_run: bool = False, resume: str | None =
from tht.adapters.factory import build_vector_store
from tht.cli.schema_cmd import _load_config_or_exit
from tht.cli.vector_cmd import make_embedder
from tht.evidence import build_preprocessing_pipeline, build_sources
from tht.evidence.corpus.chunk import ChunkPolicy
from tht.evidence.corpus.store import CorpusStore
from tht.evidence import build_preprocessing_pipeline, build_sources
cfg = _load_config_or_exit(config)
if cfg.embeddings is None:
@@ -130,9 +130,9 @@ def gc_from_config(config: Path, *, dry_run: bool = False):
from tht.adapters.factory import build_vector_store
from tht.cli.schema_cmd import _load_config_or_exit
from tht.cli.vector_cmd import make_embedder
from tht.evidence import build_preprocessing_pipeline, build_sources
from tht.evidence.corpus.chunk import ChunkPolicy
from tht.evidence.corpus.store import CorpusStore
from tht.evidence import build_preprocessing_pipeline, build_sources
cfg = _load_config_or_exit(config)
if cfg.embeddings is None:
+5 -5
View File
@@ -303,7 +303,7 @@ def _suggest_fk_result(physical, annotations, *, sql_inputs: list[tuple[str, str
@schema_app.command("check")
def check_cmd(
config: Path = CONFIG_OPT,
annotations: Path | None = typer.Option(None, "--annotations"), # noqa: B008
annotations: Path | None = typer.Option(None, "--annotations"),
reviewed_candidates: str | None = typer.Option(None, "--reviewed-candidates"),
json_output: bool = typer.Option(False, "--json"),
) -> None:
@@ -411,11 +411,11 @@ _GENERIC_PK_NAMES = {"id", "key", "code"}
@schema_app.command("suggest-fks")
def suggest_fks_cmd(
config: Path = CONFIG_OPT,
from_sql: list[Path] = typer.Option( # noqa: B008
from_sql: list[Path] = typer.Option(
None, "--from-sql",
help="Directory o file .sql approvati da cui minare i join reali (ripetibile).",
),
assume: list[str] = typer.Option( # noqa: B008
assume: list[str] = typer.Option(
None, "--assume",
help="Disambigua una PK con piu' proprietari: col=tabella_ref "
"(es. cod_paz=dim_patient). Ripetibile.",
@@ -548,10 +548,10 @@ def render_cmd(
format: str = typer.Option(
"markdown", "--format", "-f", help="Formato: markdown | mschema-text | schema-dict"
),
tables: list[str] = typer.Option( # noqa: B008
tables: list[str] = typer.Option(
None, "--table", "-t", help="Limita alle tabelle indicate (ripetibile)."
),
output: Path = typer.Option(None, "--output", "-o", help="File di output (default stdout)."), # noqa: B008
output: Path = typer.Option(None, "--output", "-o", help="File di output (default stdout)."),
) -> None:
"""Serializza mschema (physical + annotations) nel formato richiesto."""
import json
+1 -1
View File
@@ -258,9 +258,9 @@ def pack_cmd(
build_retrieval_entries,
validate_corpus_workspace,
)
from tht.memory import SOLVED_KIND
from tht.ports.vector import VectorReadUnavailable, VectorStoreError
from tht.search import combined_search, schema_tables
from tht.memory import SOLVED_KIND
from tht.vectorstore.embeddings import EmbeddingsError
cfg = _load_config_or_exit(config)
+3 -3
View File
@@ -515,12 +515,12 @@ def finalize_cmd(session_id: str = typer.Argument(...), config: Path = CONFIG_OP
promoted_tables_for,
)
from tht.ctetest import CteError, CteTestRecord, _iter_json_objects
from tht.evidence import project_session
from tht.execute import ExecutionError
from tht.execute.warnings import plan_warnings, runtime_warnings, static_warnings
from tht.report import extract_reviewer_notes, render_validation_report
from tht.evidence import project_session
from tht.phase import cte_plan as effective_cte_plan
from tht.phase import effective_decisions
from tht.report import extract_reviewer_notes, render_validation_report
from tht.session.models import SchemaLinking
from tht.sqlcheck import validate_sql
@@ -656,7 +656,7 @@ def finalize_cmd(session_id: str = typer.Argument(...), config: Path = CONFIG_OP
"Coppia domanda->SQL gia' aggiornata nel vectordb (nessun upsert).",
fg=typer.colors.CYAN,
)
except Exception as e:
except Exception as e: # noqa: BLE001 - solved-question indexing is explicitly best effort
typer.secho(
f"ATTENZIONE: coppia domanda->SQL non indicizzata ({e}). "
f"Recupera con `tht memory solved-index {session_id}`.",
+3 -1
View File
@@ -5,10 +5,12 @@ from sqlalchemy import Engine
from tht.execute import (
ExecResult,
PlanSummary,
explain as _explain,
require_positive_int,
run_controlled,
)
from tht.execute import (
explain as _explain,
)
DEFAULT_TIMEOUT_MS = 30_000
+5 -3
View File
@@ -45,9 +45,11 @@ def fetch_chain_pem(host: str, port: int = 443, timeout: int = 30) -> list[str]:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
with socket.create_connection((host, port), timeout=timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls:
certs = _unverified_chain(tls)
with (
socket.create_connection((host, port), timeout=timeout) as sock,
ctx.wrap_socket(sock, server_hostname=host) as tls,
):
certs = _unverified_chain(tls)
except (OSError, ssl.SSLError) as e:
raise CaFetchError(
f"Impossibile connettersi a {host}:{port} per recuperare i certificati: {e}"
+3 -3
View File
@@ -92,7 +92,7 @@ def add_examples(engine: Engine, physical: PhysicalSchema, cfg: ExamplesConfig)
''')
try:
rows = conn.execute(q, {"lim": cfg.max_per_column}).fetchall()
except Exception as e: # colonna non leggibile: si salta, non si interrompe
except Exception as e: # noqa: BLE001 - skip any unreadable DWH column
logger.warning("Campionamento saltato per %s.%s: %s", table_name, column_name, e)
continue
column.examples = [str(r[0]) for r in rows]
@@ -164,7 +164,7 @@ def unique_values_for_lsh(
''')
try:
rows = conn.execute(q, {"lim": cfg.max_values_per_column}).fetchall()
except Exception as e:
except Exception as e: # noqa: BLE001 - skip any unreadable DWH column
skipped.append(SkippedColumn(table_name, column_name, f"errore: {e}"))
continue
vals = [str(r[0]) for r in rows]
@@ -205,7 +205,7 @@ def unique_values_for_lsh_rest(
rows = client.top_values(
schema, table_name, column_name, cfg.max_values_per_column
)
except Exception as e:
except Exception as e: # noqa: BLE001 - skip any unreadable REST column
skipped.append(SkippedColumn(table_name, column_name, f"errore: {e}"))
continue
vals = [str(r["value"]) for r in rows if r["value"] not in (None, "")]
+3 -4
View File
@@ -24,16 +24,15 @@ from tht.evidence.search import (
from tht.evidence.session import project_session
from tht.evidence.sources import build_sources
__all__ = [
"AcquiredDocument",
"ActiveEvidenceSearcher",
"CorpusWorkspaceMismatchError",
"EvidenceEmbedder",
"EvidenceSource",
"EvidenceSourceError",
"EvidenceSourceErrorCategory",
"EvidenceEmbedder",
"SourceObject",
"ActiveEvidenceSearcher",
"CorpusWorkspaceMismatchError",
"acquire",
"active_searcher",
"build_preprocessing_pipeline",
+4 -1
View File
@@ -7,7 +7,10 @@ from datetime import UTC, datetime
from urllib.parse import quote, urlsplit
from tht.evidence.contracts import (
AcquiredDocument, EvidenceSourceError, EvidenceSourceErrorCategory, SourceObject,
AcquiredDocument,
EvidenceSourceError,
EvidenceSourceErrorCategory,
SourceObject,
)
-1
View File
@@ -15,7 +15,6 @@ from tht.evidence.contracts import (
validate_safe_metadata,
)
_NAMESPACED_ID = re.compile(r"^[a-z][a-z0-9_-]*:[A-Za-z0-9._:-]+$")
_SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$")
+1 -2
View File
@@ -10,9 +10,8 @@ from pydantic import JsonValue, TypeAdapter, ValidationError
from yaml.events import AliasEvent
from yaml.nodes import MappingNode
from tht.evidence.corpus.models import CanonicalDocument
from tht.evidence.contracts import AcquiredDocument, canonical_provenance_uri
from tht.evidence.corpus.models import CanonicalDocument
MAX_DOCUMENT_BYTES = 10 * 1024 * 1024
_CHARSET = re.compile(r"(?:^|;)\s*charset\s*=\s*[\"']?([^;\s\"']+)", re.IGNORECASE)
+13 -11
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import hashlib
import json
import logging
import re
import uuid
from collections.abc import Mapping, Sequence
@@ -11,17 +12,16 @@ from dataclasses import asdict, dataclass, field
from datetime import UTC
from pathlib import Path
import tht.evidence.acquisition as evidence_acquisition
from tht.evidence.contracts import EvidenceSource, SourceObject, canonical_provenance_uri
from tht.evidence.corpus.chunk import ChunkPolicy, chunk
from tht.evidence.corpus.models import CanonicalChunk, CanonicalDocument, CorpusManifest
from tht.evidence.corpus.normalize import normalize
from tht.evidence.corpus.store import CorpusStore
import tht.evidence.acquisition as evidence_acquisition
from tht.evidence.contracts import EvidenceSource, SourceObject, canonical_provenance_uri
from tht.ports.vector import VectorStore, VectorWriteRecord
from tht.vectorstore.records import VectorRecord
from tht.jobs.models import JobSpec
from tht.jobs.runner import JobContext, StageArtifacts, run_job, seal_stage_artifacts
from tht.ports.vector import VectorStore, VectorWriteRecord
from tht.vectorstore.records import VectorRecord
EVIDENCE_STAGE_IDS = (
"discover",
@@ -33,6 +33,8 @@ EVIDENCE_STAGE_IDS = (
"retention_cleanup",
)
logger = logging.getLogger(__name__)
class PipelineError(RuntimeError):
"""Credential-free failure at the preprocessing boundary."""
@@ -212,14 +214,14 @@ class CorpusPipeline:
if purge_vector:
try:
self.vector_store.delete_generation("evidence", generation, self.workspace_id)
except Exception:
except Exception: # noqa: BLE001 - retention reports per-generation failures
failures.append({"generation": generation, "error": "vector cleanup failed"})
continue
try:
if purge_filesystem:
self.store.discard(generation)
evicted.append(generation)
except Exception:
except Exception: # noqa: BLE001 - retention reports per-generation failures
failures.append({"generation": generation, "error": "filesystem cleanup failed"})
return {"status": "partial" if failures else "succeeded", "dry_run": dry_run,
"active_generation": self.store.active_generation(), "evicted": evicted,
@@ -378,7 +380,7 @@ class CorpusPipeline:
try:
active_assets_valid = active_assets_are_valid(previous)
except Exception:
except Exception: # noqa: BLE001 - any corrupt active asset disables reuse
active_assets_valid = False
reusable = (
active_assets_valid
@@ -538,7 +540,7 @@ class CorpusPipeline:
try:
self.vector_store.delete_generation("evidence", generation, self.workspace_id)
except Exception:
pass
logger.debug("Failed to clean the compensated vector generation", exc_info=True)
write(context, "compensated.json", {"generation": generation})
def rotate_compensated_generation(context: JobContext) -> None:
@@ -786,12 +788,12 @@ class CorpusPipeline:
try:
self.store.discard(generation)
except Exception:
pass
logger.debug("Failed to discard the unpublished evidence generation", exc_info=True)
if vector_written:
try:
self.vector_store.delete_generation("evidence", generation, self.workspace_id)
except Exception:
pass
logger.debug("Failed to delete the unpublished vector generation", exc_info=True)
@staticmethod
def _vector_record(
+5 -6
View File
@@ -2,22 +2,21 @@
from __future__ import annotations
import json
import fcntl
import hashlib
import json
import os
import re
import stat
import shutil
import uuid
import hashlib
import stat
import threading
import uuid
from contextlib import contextmanager
from datetime import UTC, datetime
from pathlib import Path
from contextlib import contextmanager
from tht.evidence.corpus.models import CorpusManifest
_GENERATION = re.compile(r"^gen:[0-9a-f]{32}$")
+2 -2
View File
@@ -46,7 +46,7 @@ class ConceptFormula(BaseModel):
return f"---\n{fm}---\n{self.sql}\n"
@classmethod
def parse(cls, text: str) -> "ConceptFormula":
def parse(cls, text: str) -> ConceptFormula:
if not text.startswith("---\n"):
raise ValueError("frontmatter mancante (atteso '---\\n' iniziale)")
try:
@@ -55,7 +55,7 @@ class ConceptFormula(BaseModel):
raise ValueError("frontmatter malformato") from e
meta = yaml.safe_load(fm)
if not isinstance(meta, dict):
raise ValueError("frontmatter non valido")
raise TypeError("frontmatter non valido")
return cls.model_validate({**meta, "sql": body.strip("\n")})
+1 -1
View File
@@ -2,10 +2,10 @@
from typing import Protocol
from tht.evidence.contracts import EvidenceSource
from tht.evidence.corpus.chunk import ChunkPolicy
from tht.evidence.corpus.pipeline import CorpusPipeline
from tht.evidence.corpus.store import CorpusStore
from tht.evidence.contracts import EvidenceSource
from tht.ports.vector import VectorStore
+3 -2
View File
@@ -9,6 +9,7 @@ import re
import stat
from pathlib import Path
from types import TracebackType
from typing import Self
class JobAlreadyRunningError(RuntimeError):
@@ -34,7 +35,7 @@ class WorkspaceJobLock:
)
self._fd: int | None = None
def acquire(self) -> "WorkspaceJobLock":
def acquire(self) -> WorkspaceJobLock:
if self._fd is not None:
raise RuntimeError("job lock is already held by this object")
root_fd = os.open(self.path.parents[2], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
@@ -85,7 +86,7 @@ class WorkspaceJobLock:
finally:
os.close(fd)
def __enter__(self) -> "WorkspaceJobLock":
def __enter__(self) -> Self:
return self.acquire()
def __exit__(
+11 -5
View File
@@ -7,8 +7,14 @@ from datetime import UTC, datetime
from pathlib import Path
from typing import Literal, Self
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_serializer,
field_validator,
model_validator,
)
_JOB_KEY = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
_RUN_ID = re.compile(r"^[0-9a-f]{32}$")
@@ -95,7 +101,7 @@ class JobSpec(_FrozenModel):
data.update(update)
return type(self).model_validate(data)
def with_resume(self, run_id: str) -> "JobSpec":
def with_resume(self, run_id: str) -> JobSpec:
return self.model_copy(update={"resume_run_id": run_id})
@@ -121,7 +127,7 @@ class StageRun(_FrozenModel):
)
@model_validator(mode="after")
def state_shape(self) -> "StageRun":
def state_shape(self) -> StageRun:
if self.status == "pending" and any(
value is not None for value in (
self.started_at, self.finished_at, self.error, self.effect_state,
@@ -179,7 +185,7 @@ class JobRun(_FrozenModel):
_resumed_from = field_validator("resumed_from")(_validate_run_id)
@model_validator(mode="after")
def ledger_shape(self) -> "JobRun":
def ledger_shape(self) -> JobRun:
names = [stage.name for stage in self.stages]
if len(names) != len(set(names)):
raise ValueError("stage identifiers must be unique")
+4 -4
View File
@@ -2,12 +2,12 @@
from __future__ import annotations
import json
import hashlib
import json
import os
import uuid
import stat
import shutil
import stat
import uuid
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from pathlib import Path
@@ -325,7 +325,7 @@ def run_job(
_persist(checkpoint_path, run)
try:
stage_result = stage_callable(context)
except Exception:
except Exception: # noqa: BLE001 - stage failures are persisted as terminal reports
failed = stage.model_copy(
update={
"status": "failed",
+1 -1
View File
@@ -122,7 +122,7 @@ def index_solved_question_best_effort(
store=store_factory(),
embedder=embedder_factory(),
)
except Exception as error:
except Exception as error: # noqa: BLE001 - callers receive a best-effort outcome
return SolvedIndexOutcome(upserted=None, error=str(error))
return SolvedIndexOutcome(upserted=upserted)
+2 -2
View File
@@ -115,8 +115,8 @@ def to_markdown(physical: PhysicalSchema, annotations: Annotations | None = None
lines = [
f"# Schema {physical.db_schema} ({physical.database})",
"",
f"Introspezione: {physical.introspected_at.isoformat()} — "
f"{len(physical.tables)} tabelle",
(f"Introspezione: {physical.introspected_at.isoformat()} — "
f"{len(physical.tables)} tabelle"),
]
for table_name, table in physical.tables.items():
lines += ["", f"## {table_name}", ""]
+1 -2
View File
@@ -1,9 +1,8 @@
"""Deterministic builder for the single Pi-facing Thoth session skill."""
import argparse
from pathlib import Path
import sys
from pathlib import Path
HARNESS_ROOT = Path(__file__).resolve().parents[1]
SKILL_ROOT = HARNESS_ROOT / ".pi" / "skills" / "tht-sessione"
+4 -4
View File
@@ -1,18 +1,18 @@
"""Stable interfaces implemented by Thoth infrastructure adapters."""
from tht.ports.dwh import (
DistinctValues,
DwhAdapter,
DwhCapabilities,
DwhHealth,
DistinctValues,
UnsupportedCapability,
)
from tht.ports.vector import (
VectorCapabilities,
VectorHealth,
VectorHit,
VectorRecord,
VectorReadUnavailable,
VectorRecord,
VectorStore,
VectorStoreError,
VectorWriteRecord,
@@ -20,16 +20,16 @@ from tht.ports.vector import (
)
__all__ = [
"DistinctValues",
"DwhAdapter",
"DwhCapabilities",
"DwhHealth",
"DistinctValues",
"UnsupportedCapability",
"VectorCapabilities",
"VectorHealth",
"VectorHit",
"VectorRecord",
"VectorReadUnavailable",
"VectorRecord",
"VectorStore",
"VectorStoreError",
"VectorWriteRecord",
+1 -1
View File
@@ -40,7 +40,7 @@ class RestClient:
try:
body = resp.json()
detail = body.get("message") or body.get("details") or resp.text
except Exception:
except (requests.exceptions.JSONDecodeError, AttributeError, TypeError):
detail = resp.text
return f"DWH REST rpc {fn} → HTTP {resp.status_code}: {detail}"
+2 -2
View File
@@ -9,8 +9,8 @@ import re
import shutil
import tempfile
import uuid
from collections.abc import Sequence
from pathlib import Path
from typing import Sequence
import portalocker
import yaml
@@ -147,7 +147,7 @@ class FilesystemSessionRepository:
return {}
data = json.loads(path.read_text())
if not isinstance(data, dict):
raise ValueError(f"Invalid preferences: {path}")
raise TypeError(f"Invalid preferences: {path}")
return data
def set_preferences(self, preferences: dict) -> None:
+1 -1
View File
@@ -8,7 +8,7 @@ from typing import Literal, Self
import portalocker
import yaml
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, ConfigDict, Field
from tht.decisions import DecisionRecord
+2 -2
View File
@@ -6,13 +6,13 @@ import hashlib
import json
import re
import uuid
from collections.abc import Iterator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import UTC, datetime
from importlib.resources import files
from importlib.resources.abc import Traversable
from pathlib import Path
from typing import Iterator, Sequence
from sqlalchemy import Engine, create_engine, text
from sqlalchemy.engine import URL, make_url
@@ -204,7 +204,7 @@ class PostgresSessionRepository:
self._runtime_role = runtime_role
@classmethod
def from_config(cls, config, principal: PrincipalContext) -> "PostgresSessionRepository":
def from_config(cls, config, principal: PrincipalContext) -> PostgresSessionRepository:
query = {"sslmode": config.sslmode}
if config.sslrootcert is not None:
query["sslrootcert"] = str(config.sslrootcert)
+2 -1
View File
@@ -3,7 +3,8 @@
from __future__ import annotations
import os
from typing import Protocol, Sequence
from collections.abc import Sequence
from typing import Protocol
from tht.decisions import DecisionInput, DecisionRecord
from tht.session.models import PrincipalContext, SessionManifest, SessionSnapshot
+2 -2
View File
@@ -55,7 +55,7 @@ def _extract_name(question: str) -> str:
try:
extractor = yake.KeywordExtractor(lan="it", n=1, top=8, dedupLim=0.9)
ranked = [k for k, _ in extractor.extract_keywords(q)]
except Exception:
except Exception: # noqa: BLE001 - keyword extraction has a deterministic fallback
return _summarize(question)
seen: set[str] = set()
picked: list[str] = []
@@ -117,7 +117,7 @@ def create_session(
from tht.workflow import load_workflow
schema_version = load_workflow().schema_version
except Exception:
except Exception: # noqa: BLE001 - legacy sessions may predate workflow metadata
schema_version = None
manifest = SessionManifest(
id=session_id, created_at=now, question=question,
+1 -1
View File
@@ -87,7 +87,7 @@ def generate_task_doc(
wf = load_workflow()
name = wf.phase_name(phase)
header = f"## Task: fase {phase} ({name})"
except Exception:
except Exception: # noqa: BLE001 - task documents retain a phase-only fallback
header = f"## Task: fase {phase}"
parts.append(header)
+7 -6
View File
@@ -4,11 +4,12 @@
"""Core LSH (MinHash) per la ricerca di valori simili nei campi del database."""
import logging
from typing import Dict, List, Tuple
from datasketch import MinHash, MinHashLSH
from tqdm import tqdm
logger = logging.getLogger(__name__)
def create_minhash(signature_size: int, string: str, n_gram: int) -> MinHash:
m = MinHash(num_perm=signature_size)
@@ -33,7 +34,7 @@ NAME_LIKE_TOKENS: tuple[str, ...] = (
def skip_column(
column_name: str,
column_values: List[str],
column_values: list[str],
max_total_chars: int = 50000,
max_avg_length: int = 20,
name_tokens: tuple[str, ...] = NAME_LIKE_TOKENS,
@@ -51,20 +52,20 @@ def jaccard_similarity(m1: MinHash, m2: MinHash) -> float:
def create_lsh_index(
unique_values: Dict[str, Dict[str, List[str]]],
unique_values: dict[str, dict[str, list[str]]],
signature_size: int,
n_gram: int,
threshold: float,
verbose: bool = True,
) -> Tuple[MinHashLSH, Dict[str, Tuple[MinHash, str, str, str]]]:
) -> tuple[MinHashLSH, dict[str, tuple[MinHash, str, str, str]]]:
lsh = MinHashLSH(threshold=threshold, num_perm=signature_size)
minhashes: Dict[str, Tuple[MinHash, str, str, str]] = {}
minhashes: dict[str, tuple[MinHash, str, str, str]] = {}
total = sum(
len(column_values)
for table_values in unique_values.values()
for column_values in table_values.values()
)
logging.info("Total unique values: %s", total)
logger.info("Total unique values: %s", total)
progress_bar = tqdm(total=total, desc="Creating LSH") if verbose else None
for table_name, table_values in unique_values.items():
+4 -3
View File
@@ -82,9 +82,10 @@ def _collect_decision_mins(phases: list[PhaseSpec]) -> dict[str, int]:
dtype = value
else:
continue
if isinstance(dtype, str):
if dtype not in mins or phase_num < mins[dtype]:
mins[dtype] = phase_num
if isinstance(dtype, str) and (
dtype not in mins or phase_num < mins[dtype]
):
mins[dtype] = phase_num
else:
scan(value, phase_num)
elif isinstance(node, list):