kontra 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kontra/__init__.py +1871 -0
- kontra/api/__init__.py +22 -0
- kontra/api/compare.py +340 -0
- kontra/api/decorators.py +153 -0
- kontra/api/results.py +2121 -0
- kontra/api/rules.py +681 -0
- kontra/cli/__init__.py +0 -0
- kontra/cli/commands/__init__.py +1 -0
- kontra/cli/commands/config.py +153 -0
- kontra/cli/commands/diff.py +450 -0
- kontra/cli/commands/history.py +196 -0
- kontra/cli/commands/profile.py +289 -0
- kontra/cli/commands/validate.py +468 -0
- kontra/cli/constants.py +6 -0
- kontra/cli/main.py +48 -0
- kontra/cli/renderers.py +304 -0
- kontra/cli/utils.py +28 -0
- kontra/config/__init__.py +34 -0
- kontra/config/loader.py +127 -0
- kontra/config/models.py +49 -0
- kontra/config/settings.py +797 -0
- kontra/connectors/__init__.py +0 -0
- kontra/connectors/db_utils.py +251 -0
- kontra/connectors/detection.py +323 -0
- kontra/connectors/handle.py +368 -0
- kontra/connectors/postgres.py +127 -0
- kontra/connectors/sqlserver.py +226 -0
- kontra/engine/__init__.py +0 -0
- kontra/engine/backends/duckdb_session.py +227 -0
- kontra/engine/backends/duckdb_utils.py +18 -0
- kontra/engine/backends/polars_backend.py +47 -0
- kontra/engine/engine.py +1205 -0
- kontra/engine/executors/__init__.py +15 -0
- kontra/engine/executors/base.py +50 -0
- kontra/engine/executors/database_base.py +528 -0
- kontra/engine/executors/duckdb_sql.py +607 -0
- kontra/engine/executors/postgres_sql.py +162 -0
- kontra/engine/executors/registry.py +69 -0
- kontra/engine/executors/sqlserver_sql.py +163 -0
- kontra/engine/materializers/__init__.py +14 -0
- kontra/engine/materializers/base.py +42 -0
- kontra/engine/materializers/duckdb.py +110 -0
- kontra/engine/materializers/factory.py +22 -0
- kontra/engine/materializers/polars_connector.py +131 -0
- kontra/engine/materializers/postgres.py +157 -0
- kontra/engine/materializers/registry.py +138 -0
- kontra/engine/materializers/sqlserver.py +160 -0
- kontra/engine/result.py +15 -0
- kontra/engine/sql_utils.py +611 -0
- kontra/engine/sql_validator.py +609 -0
- kontra/engine/stats.py +194 -0
- kontra/engine/types.py +138 -0
- kontra/errors.py +533 -0
- kontra/logging.py +85 -0
- kontra/preplan/__init__.py +5 -0
- kontra/preplan/planner.py +253 -0
- kontra/preplan/postgres.py +179 -0
- kontra/preplan/sqlserver.py +191 -0
- kontra/preplan/types.py +24 -0
- kontra/probes/__init__.py +20 -0
- kontra/probes/compare.py +400 -0
- kontra/probes/relationship.py +283 -0
- kontra/reporters/__init__.py +0 -0
- kontra/reporters/json_reporter.py +190 -0
- kontra/reporters/rich_reporter.py +11 -0
- kontra/rules/__init__.py +35 -0
- kontra/rules/base.py +186 -0
- kontra/rules/builtin/__init__.py +40 -0
- kontra/rules/builtin/allowed_values.py +156 -0
- kontra/rules/builtin/compare.py +188 -0
- kontra/rules/builtin/conditional_not_null.py +213 -0
- kontra/rules/builtin/conditional_range.py +310 -0
- kontra/rules/builtin/contains.py +138 -0
- kontra/rules/builtin/custom_sql_check.py +182 -0
- kontra/rules/builtin/disallowed_values.py +140 -0
- kontra/rules/builtin/dtype.py +203 -0
- kontra/rules/builtin/ends_with.py +129 -0
- kontra/rules/builtin/freshness.py +240 -0
- kontra/rules/builtin/length.py +193 -0
- kontra/rules/builtin/max_rows.py +35 -0
- kontra/rules/builtin/min_rows.py +46 -0
- kontra/rules/builtin/not_null.py +121 -0
- kontra/rules/builtin/range.py +222 -0
- kontra/rules/builtin/regex.py +143 -0
- kontra/rules/builtin/starts_with.py +129 -0
- kontra/rules/builtin/unique.py +124 -0
- kontra/rules/condition_parser.py +203 -0
- kontra/rules/execution_plan.py +455 -0
- kontra/rules/factory.py +103 -0
- kontra/rules/predicates.py +25 -0
- kontra/rules/registry.py +24 -0
- kontra/rules/static_predicates.py +120 -0
- kontra/scout/__init__.py +9 -0
- kontra/scout/backends/__init__.py +17 -0
- kontra/scout/backends/base.py +111 -0
- kontra/scout/backends/duckdb_backend.py +359 -0
- kontra/scout/backends/postgres_backend.py +519 -0
- kontra/scout/backends/sqlserver_backend.py +577 -0
- kontra/scout/dtype_mapping.py +150 -0
- kontra/scout/patterns.py +69 -0
- kontra/scout/profiler.py +801 -0
- kontra/scout/reporters/__init__.py +39 -0
- kontra/scout/reporters/json_reporter.py +165 -0
- kontra/scout/reporters/markdown_reporter.py +152 -0
- kontra/scout/reporters/rich_reporter.py +144 -0
- kontra/scout/store.py +208 -0
- kontra/scout/suggest.py +200 -0
- kontra/scout/types.py +652 -0
- kontra/state/__init__.py +29 -0
- kontra/state/backends/__init__.py +79 -0
- kontra/state/backends/base.py +348 -0
- kontra/state/backends/local.py +480 -0
- kontra/state/backends/postgres.py +1010 -0
- kontra/state/backends/s3.py +543 -0
- kontra/state/backends/sqlserver.py +969 -0
- kontra/state/fingerprint.py +166 -0
- kontra/state/types.py +1061 -0
- kontra/version.py +1 -0
- kontra-0.5.2.dist-info/METADATA +122 -0
- kontra-0.5.2.dist-info/RECORD +124 -0
- kontra-0.5.2.dist-info/WHEEL +5 -0
- kontra-0.5.2.dist-info/entry_points.txt +2 -0
- kontra-0.5.2.dist-info/licenses/LICENSE +17 -0
- kontra-0.5.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# src/kontra/state/backends/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
State storage backends.
|
|
4
|
+
|
|
5
|
+
Backends provide pluggable persistence for validation state:
|
|
6
|
+
- LocalStore: Filesystem storage in .kontra/state/
|
|
7
|
+
- S3Store: S3-compatible object storage
|
|
8
|
+
- PostgresStore: PostgreSQL database
|
|
9
|
+
- SQLServerStore: SQL Server database
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .base import StateBackend
|
|
13
|
+
from .local import LocalStore
|
|
14
|
+
|
|
15
|
+
# Default store factory
|
|
16
|
+
_default_store: LocalStore | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_default_store() -> LocalStore:
|
|
20
|
+
"""
|
|
21
|
+
Get the default state store.
|
|
22
|
+
|
|
23
|
+
Uses .kontra/state/ in the current working directory.
|
|
24
|
+
Lazily initialized on first call.
|
|
25
|
+
"""
|
|
26
|
+
global _default_store
|
|
27
|
+
if _default_store is None:
|
|
28
|
+
_default_store = LocalStore()
|
|
29
|
+
return _default_store
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_store(backend: str = "local") -> StateBackend:
|
|
33
|
+
"""
|
|
34
|
+
Get a state store by backend identifier.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
backend: Backend identifier. Options:
|
|
38
|
+
- "local" or "": LocalStore (default)
|
|
39
|
+
- "s3://bucket/prefix": S3Store
|
|
40
|
+
- "postgres://..." or "postgresql://...": PostgresStore
|
|
41
|
+
- "mssql://..." or "sqlserver://...": SQLServerStore
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A StateBackend instance
|
|
45
|
+
|
|
46
|
+
Environment Variables:
|
|
47
|
+
For S3:
|
|
48
|
+
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL, AWS_REGION
|
|
49
|
+
|
|
50
|
+
For PostgreSQL:
|
|
51
|
+
PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE, DATABASE_URL
|
|
52
|
+
|
|
53
|
+
For SQL Server:
|
|
54
|
+
MSSQL_HOST, MSSQL_PORT, MSSQL_USER, MSSQL_PASSWORD, MSSQL_DATABASE, MSSQL_DRIVER
|
|
55
|
+
"""
|
|
56
|
+
if not backend or backend == "local":
|
|
57
|
+
return get_default_store()
|
|
58
|
+
|
|
59
|
+
if backend.startswith("s3://"):
|
|
60
|
+
from .s3 import S3Store
|
|
61
|
+
return S3Store(backend)
|
|
62
|
+
|
|
63
|
+
if backend.startswith("postgres://") or backend.startswith("postgresql://"):
|
|
64
|
+
from .postgres import PostgresStore
|
|
65
|
+
return PostgresStore(backend)
|
|
66
|
+
|
|
67
|
+
if backend.startswith("mssql://") or backend.startswith("sqlserver://"):
|
|
68
|
+
from .sqlserver import SQLServerStore
|
|
69
|
+
return SQLServerStore(backend)
|
|
70
|
+
|
|
71
|
+
raise ValueError(f"Unknown state backend: {backend}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"StateBackend",
|
|
76
|
+
"LocalStore",
|
|
77
|
+
"get_default_store",
|
|
78
|
+
"get_store",
|
|
79
|
+
]
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# src/kontra/state/backends/base.py
|
|
2
|
+
"""
|
|
3
|
+
StateBackend protocol definition.
|
|
4
|
+
|
|
5
|
+
All state storage implementations must conform to this protocol.
|
|
6
|
+
|
|
7
|
+
v0.5 adds:
|
|
8
|
+
- Normalized schema (kontra_runs, kontra_rule_results)
|
|
9
|
+
- Annotations (kontra_annotations) with append-only semantics
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from kontra.state.types import Annotation, RunSummary, ValidationState
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StateBackend(ABC):
|
|
23
|
+
"""
|
|
24
|
+
Abstract base class for state storage backends.
|
|
25
|
+
|
|
26
|
+
Implementations provide persistence for ValidationState objects,
|
|
27
|
+
enabling history tracking and comparison across runs.
|
|
28
|
+
|
|
29
|
+
Design principles:
|
|
30
|
+
- Immutable writes: Each save creates a new record
|
|
31
|
+
- Query by contract: States are indexed by contract fingerprint
|
|
32
|
+
- Time-ordered: History is returned newest-first
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def save(self, state: "ValidationState") -> None:
|
|
37
|
+
"""
|
|
38
|
+
Save a validation state.
|
|
39
|
+
|
|
40
|
+
The state is immutable once saved. Each call creates a new record
|
|
41
|
+
identified by (contract_fingerprint, run_at).
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
state: The ValidationState to persist
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
IOError: If the save fails
|
|
48
|
+
"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def get_latest(self, contract_fingerprint: str) -> Optional["ValidationState"]:
|
|
53
|
+
"""
|
|
54
|
+
Get the most recent state for a contract.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The most recent ValidationState, or None if no history exists
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def get_history(
|
|
66
|
+
self,
|
|
67
|
+
contract_fingerprint: str,
|
|
68
|
+
limit: int = 10,
|
|
69
|
+
) -> List["ValidationState"]:
|
|
70
|
+
"""
|
|
71
|
+
Get recent history for a contract.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
75
|
+
limit: Maximum number of states to return
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of ValidationState objects, newest first
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
def get_at(
|
|
83
|
+
self,
|
|
84
|
+
contract_fingerprint: str,
|
|
85
|
+
timestamp: datetime,
|
|
86
|
+
) -> Optional["ValidationState"]:
|
|
87
|
+
"""
|
|
88
|
+
Get state at or before a specific timestamp.
|
|
89
|
+
|
|
90
|
+
Default implementation uses get_history and filters.
|
|
91
|
+
Backends may override with more efficient queries.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
95
|
+
timestamp: The target timestamp
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The ValidationState at or before timestamp, or None
|
|
99
|
+
"""
|
|
100
|
+
history = self.get_history(contract_fingerprint, limit=100)
|
|
101
|
+
for state in history:
|
|
102
|
+
if state.run_at <= timestamp:
|
|
103
|
+
return state
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def get_previous(
|
|
107
|
+
self,
|
|
108
|
+
contract_fingerprint: str,
|
|
109
|
+
before: datetime,
|
|
110
|
+
) -> Optional["ValidationState"]:
|
|
111
|
+
"""
|
|
112
|
+
Get the state immediately before a timestamp.
|
|
113
|
+
|
|
114
|
+
Useful for comparing current run to previous run.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
118
|
+
before: Get state before this timestamp
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The most recent ValidationState before timestamp, or None
|
|
122
|
+
"""
|
|
123
|
+
history = self.get_history(contract_fingerprint, limit=100)
|
|
124
|
+
for state in history:
|
|
125
|
+
if state.run_at < before:
|
|
126
|
+
return state
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def delete_old(
|
|
130
|
+
self,
|
|
131
|
+
contract_fingerprint: str,
|
|
132
|
+
keep_count: int = 100,
|
|
133
|
+
) -> int:
|
|
134
|
+
"""
|
|
135
|
+
Delete old states, keeping the most recent ones.
|
|
136
|
+
|
|
137
|
+
Default implementation does nothing. Backends may override
|
|
138
|
+
to implement retention policies.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
142
|
+
keep_count: Number of recent states to keep
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Number of states deleted
|
|
146
|
+
"""
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
def list_contracts(self) -> List[str]:
|
|
150
|
+
"""
|
|
151
|
+
List all contract fingerprints with stored state.
|
|
152
|
+
|
|
153
|
+
Default implementation returns empty list. Backends may override.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of contract fingerprint strings
|
|
157
|
+
"""
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
def get_run_summaries(
|
|
161
|
+
self,
|
|
162
|
+
contract_fingerprint: str,
|
|
163
|
+
limit: int = 20,
|
|
164
|
+
since: Optional[datetime] = None,
|
|
165
|
+
failed_only: bool = False,
|
|
166
|
+
) -> List["RunSummary"]:
|
|
167
|
+
"""
|
|
168
|
+
Get lightweight run summaries for history listing.
|
|
169
|
+
|
|
170
|
+
More efficient than get_history() as it doesn't load rule details.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
174
|
+
limit: Maximum number of summaries to return
|
|
175
|
+
since: Only return runs after this timestamp
|
|
176
|
+
failed_only: Only return failed runs
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of RunSummary objects, newest first
|
|
180
|
+
"""
|
|
181
|
+
# Default implementation converts from full states
|
|
182
|
+
from kontra.state.types import RunSummary
|
|
183
|
+
|
|
184
|
+
states = self.get_history(contract_fingerprint, limit=limit * 2)
|
|
185
|
+
summaries = []
|
|
186
|
+
|
|
187
|
+
for i, state in enumerate(states):
|
|
188
|
+
if since and state.run_at < since:
|
|
189
|
+
continue
|
|
190
|
+
if failed_only and state.summary.passed:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
run_id = str(state.id) if state.id else f"run_{i}"
|
|
194
|
+
summaries.append(RunSummary.from_validation_state(state, run_id))
|
|
195
|
+
|
|
196
|
+
if len(summaries) >= limit:
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
return summaries
|
|
200
|
+
|
|
201
|
+
# -------------------------------------------------------------------------
|
|
202
|
+
# Annotation Methods (v0.5)
|
|
203
|
+
# -------------------------------------------------------------------------
|
|
204
|
+
#
|
|
205
|
+
# Annotations are append-only records that agents/humans can attach to
|
|
206
|
+
# validation runs. Kontra never reads annotations during validation or diff.
|
|
207
|
+
#
|
|
208
|
+
# Default implementations do nothing. Database backends override with
|
|
209
|
+
# actual persistence logic.
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _attach_annotations_to_state(
|
|
213
|
+
state: "ValidationState",
|
|
214
|
+
annotations: List["Annotation"],
|
|
215
|
+
) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Attach annotations to a ValidationState, grouping by rule_result_id.
|
|
218
|
+
|
|
219
|
+
Modifies state in-place:
|
|
220
|
+
- Sets state.annotations to run-level annotations (rule_result_id is None)
|
|
221
|
+
- Sets rule.annotations for each rule result
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
state: The ValidationState to modify
|
|
225
|
+
annotations: List of annotations to attach
|
|
226
|
+
"""
|
|
227
|
+
# Group annotations by rule_result_id
|
|
228
|
+
run_annotations: List["Annotation"] = []
|
|
229
|
+
rule_annotations: Dict[int, List["Annotation"]] = {}
|
|
230
|
+
|
|
231
|
+
for ann in annotations:
|
|
232
|
+
if ann.rule_result_id is None:
|
|
233
|
+
run_annotations.append(ann)
|
|
234
|
+
else:
|
|
235
|
+
rule_annotations.setdefault(ann.rule_result_id, []).append(ann)
|
|
236
|
+
|
|
237
|
+
state.annotations = run_annotations
|
|
238
|
+
for rule in state.rules:
|
|
239
|
+
if rule.id is not None:
|
|
240
|
+
rule.annotations = rule_annotations.get(rule.id, [])
|
|
241
|
+
else:
|
|
242
|
+
rule.annotations = []
|
|
243
|
+
|
|
244
|
+
def save_annotation(self, annotation: "Annotation") -> int:
|
|
245
|
+
"""
|
|
246
|
+
Save an annotation.
|
|
247
|
+
|
|
248
|
+
Annotations are append-only. Each save creates a new record.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
annotation: The Annotation to persist
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
The database-assigned ID of the new annotation
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
IOError: If the save fails
|
|
258
|
+
ValueError: If annotation references non-existent run/rule
|
|
259
|
+
"""
|
|
260
|
+
raise NotImplementedError(
|
|
261
|
+
f"{self.__class__.__name__} does not support annotations"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def get_annotations(
|
|
265
|
+
self,
|
|
266
|
+
run_id: int,
|
|
267
|
+
rule_result_id: Optional[int] = None,
|
|
268
|
+
) -> List["Annotation"]:
|
|
269
|
+
"""
|
|
270
|
+
Get annotations for a run or specific rule result.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
run_id: The run ID to get annotations for
|
|
274
|
+
rule_result_id: If provided, filter to annotations on this rule
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
List of Annotation objects, newest first
|
|
278
|
+
"""
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
def get_annotations_for_contract(
|
|
282
|
+
self,
|
|
283
|
+
contract_fingerprint: str,
|
|
284
|
+
rule_id: Optional[str] = None,
|
|
285
|
+
annotation_type: Optional[str] = None,
|
|
286
|
+
limit: int = 20,
|
|
287
|
+
) -> List["Annotation"]:
|
|
288
|
+
"""
|
|
289
|
+
Get annotations across all runs for a contract.
|
|
290
|
+
|
|
291
|
+
This is the cross-run query for agent memory - "what annotations exist
|
|
292
|
+
for this rule across all past runs?"
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
296
|
+
rule_id: If provided, filter to annotations on this rule
|
|
297
|
+
annotation_type: If provided, filter by annotation type
|
|
298
|
+
limit: Maximum number of annotations to return
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of Annotation objects, newest first, with rule_id populated
|
|
302
|
+
"""
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
def get_run_with_annotations(
|
|
306
|
+
self,
|
|
307
|
+
contract_fingerprint: str,
|
|
308
|
+
run_id: Optional[int] = None,
|
|
309
|
+
) -> Optional["ValidationState"]:
|
|
310
|
+
"""
|
|
311
|
+
Get a validation state with its annotations loaded.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
315
|
+
run_id: Specific run ID. If None, gets the latest run.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
ValidationState with annotations populated, or None
|
|
319
|
+
"""
|
|
320
|
+
# Default: get latest and attach empty annotations
|
|
321
|
+
state = self.get_latest(contract_fingerprint) if run_id is None else None
|
|
322
|
+
if state:
|
|
323
|
+
state.annotations = []
|
|
324
|
+
for rule in state.rules:
|
|
325
|
+
rule.annotations = []
|
|
326
|
+
return state
|
|
327
|
+
|
|
328
|
+
def get_history_with_annotations(
|
|
329
|
+
self,
|
|
330
|
+
contract_fingerprint: str,
|
|
331
|
+
limit: int = 10,
|
|
332
|
+
) -> List["ValidationState"]:
|
|
333
|
+
"""
|
|
334
|
+
Get recent history with annotations loaded.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
contract_fingerprint: The contract's fingerprint hash
|
|
338
|
+
limit: Maximum number of states to return
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
List of ValidationState objects with annotations, newest first
|
|
342
|
+
"""
|
|
343
|
+
states = self.get_history(contract_fingerprint, limit=limit)
|
|
344
|
+
for state in states:
|
|
345
|
+
state.annotations = []
|
|
346
|
+
for rule in state.rules:
|
|
347
|
+
rule.annotations = []
|
|
348
|
+
return states
|