evalgate-sdk 3.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. evalgate_sdk/__init__.py +707 -0
  2. evalgate_sdk/_version.py +3 -0
  3. evalgate_sdk/assertions.py +1362 -0
  4. evalgate_sdk/auto.py +247 -0
  5. evalgate_sdk/batch.py +174 -0
  6. evalgate_sdk/cache.py +111 -0
  7. evalgate_sdk/ci_context.py +123 -0
  8. evalgate_sdk/cli/__init__.py +111 -0
  9. evalgate_sdk/cli/api.py +261 -0
  10. evalgate_sdk/cli/cli_constants.py +20 -0
  11. evalgate_sdk/cli/commands.py +1041 -0
  12. evalgate_sdk/cli/config.py +228 -0
  13. evalgate_sdk/cli/env.py +43 -0
  14. evalgate_sdk/cli/formatters/types.py +132 -0
  15. evalgate_sdk/cli/golden_commands.py +322 -0
  16. evalgate_sdk/cli/manifest.py +301 -0
  17. evalgate_sdk/cli/new_commands.py +435 -0
  18. evalgate_sdk/cli/policy_packs.py +103 -0
  19. evalgate_sdk/cli/profiles.py +12 -0
  20. evalgate_sdk/cli/regression_gate.py +312 -0
  21. evalgate_sdk/cli/render/__init__.py +1 -0
  22. evalgate_sdk/cli/render/snippet.py +18 -0
  23. evalgate_sdk/cli/render/sort.py +29 -0
  24. evalgate_sdk/cli/report/__init__.py +1 -0
  25. evalgate_sdk/cli/report/build_check_report.py +209 -0
  26. evalgate_sdk/cli/traces.py +186 -0
  27. evalgate_sdk/cli/workspace.py +63 -0
  28. evalgate_sdk/client.py +609 -0
  29. evalgate_sdk/cluster.py +359 -0
  30. evalgate_sdk/collector.py +161 -0
  31. evalgate_sdk/constants.py +6 -0
  32. evalgate_sdk/context.py +151 -0
  33. evalgate_sdk/errors.py +236 -0
  34. evalgate_sdk/export.py +238 -0
  35. evalgate_sdk/formatters/__init__.py +11 -0
  36. evalgate_sdk/formatters/github.py +51 -0
  37. evalgate_sdk/formatters/human.py +68 -0
  38. evalgate_sdk/formatters/json_fmt.py +11 -0
  39. evalgate_sdk/formatters/pr_comment.py +80 -0
  40. evalgate_sdk/golden.py +426 -0
  41. evalgate_sdk/integrations/__init__.py +1 -0
  42. evalgate_sdk/integrations/anthropic.py +99 -0
  43. evalgate_sdk/integrations/autogen.py +62 -0
  44. evalgate_sdk/integrations/crewai.py +61 -0
  45. evalgate_sdk/integrations/langchain.py +100 -0
  46. evalgate_sdk/integrations/openai.py +155 -0
  47. evalgate_sdk/integrations/openai_eval.py +221 -0
  48. evalgate_sdk/local.py +144 -0
  49. evalgate_sdk/logger.py +123 -0
  50. evalgate_sdk/matchers.py +62 -0
  51. evalgate_sdk/otel.py +256 -0
  52. evalgate_sdk/pagination.py +145 -0
  53. evalgate_sdk/py.typed +0 -0
  54. evalgate_sdk/pytest_plugin.py +96 -0
  55. evalgate_sdk/reason_codes.py +103 -0
  56. evalgate_sdk/regression.py +196 -0
  57. evalgate_sdk/replay_decision.py +115 -0
  58. evalgate_sdk/runtime/__init__.py +50 -0
  59. evalgate_sdk/runtime/adapters/__init__.py +1 -0
  60. evalgate_sdk/runtime/adapters/config_to_dsl.py +270 -0
  61. evalgate_sdk/runtime/adapters/testsuite_to_dsl.py +213 -0
  62. evalgate_sdk/runtime/context.py +68 -0
  63. evalgate_sdk/runtime/eval.py +318 -0
  64. evalgate_sdk/runtime/execution_mode.py +170 -0
  65. evalgate_sdk/runtime/executor.py +92 -0
  66. evalgate_sdk/runtime/registry.py +125 -0
  67. evalgate_sdk/runtime/run_report.py +249 -0
  68. evalgate_sdk/runtime/types.py +143 -0
  69. evalgate_sdk/snapshot.py +219 -0
  70. evalgate_sdk/streaming.py +124 -0
  71. evalgate_sdk/synthesize.py +226 -0
  72. evalgate_sdk/testing.py +128 -0
  73. evalgate_sdk/types.py +666 -0
  74. evalgate_sdk/utils/__init__.py +1 -0
  75. evalgate_sdk/utils/input_hash.py +42 -0
  76. evalgate_sdk/workflows.py +264 -0
  77. evalgate_sdk-3.3.1.dist-info/METADATA +608 -0
  78. evalgate_sdk-3.3.1.dist-info/RECORD +80 -0
  79. evalgate_sdk-3.3.1.dist-info/WHEEL +4 -0
  80. evalgate_sdk-3.3.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,249 @@
1
+ """Deterministic RunReport serialization (T4).
2
+
3
+ Port of the TypeScript SDK's ``run-report.ts``.
4
+ Provides a stable report format for downstream processing (explain, diff, history).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import platform
11
+ import sys
12
+ from dataclasses import asdict, dataclass, field
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ RUN_REPORT_SCHEMA_VERSION = "1"
17
+
18
+
19
+ @dataclass
20
+ class RunResult:
21
+ """Individual test result."""
22
+
23
+ test_id: str
24
+ test_name: str
25
+ file_path: str
26
+ position: dict[str, int]
27
+ input: str
28
+ passed: bool
29
+ score: float
30
+ duration_ms: float
31
+ metadata: dict[str, Any] | None = None
32
+ tags: list[str] = field(default_factory=list)
33
+ assertions: list[dict[str, Any]] = field(default_factory=list)
34
+
35
+
36
+ @dataclass
37
+ class RunFailure:
38
+ """Failure or error information."""
39
+
40
+ test_id: str
41
+ test_name: str
42
+ file_path: str
43
+ position: dict[str, int]
44
+ classification: str # "failed" | "error" | "timeout"
45
+ message: str
46
+ timestamp: str = ""
47
+ error_envelope: dict[str, Any] | None = None
48
+
49
+
50
+ @dataclass
51
+ class RunSummary:
52
+ """Execution summary statistics."""
53
+
54
+ total: int = 0
55
+ passed: int = 0
56
+ failed: int = 0
57
+ errors: int = 0
58
+ timeouts: int = 0
59
+ pass_rate: float = 0.0
60
+ average_score: float = 0.0
61
+ total_duration_ms: float = 0.0
62
+ success: bool = True
63
+
64
+
65
+ @dataclass
66
+ class RunConfig:
67
+ """Execution configuration."""
68
+
69
+ executor_type: str = "local"
70
+ max_parallel: int | None = None
71
+ default_timeout: int = 30_000
72
+ environment: dict[str, str] = field(default_factory=dict)
73
+
74
+
75
+ @dataclass
76
+ class RunReport:
77
+ """Main run report structure."""
78
+
79
+ schema_version: str = RUN_REPORT_SCHEMA_VERSION
80
+ run_id: str = ""
81
+ started_at: str = ""
82
+ finished_at: str = ""
83
+ runtime: dict[str, str] = field(default_factory=dict)
84
+ results: list[RunResult] = field(default_factory=list)
85
+ failures: list[RunFailure] = field(default_factory=list)
86
+ summary: RunSummary = field(default_factory=RunSummary)
87
+ config: RunConfig = field(default_factory=RunConfig)
88
+
89
+ def to_json(self) -> str:
90
+ """Serialize to deterministic JSON string."""
91
+ return json.dumps(asdict(self), indent=2, default=str)
92
+
93
+
94
+ class RunReportBuilder:
95
+ """Builder for creating deterministic RunReport instances."""
96
+
97
+ def __init__(
98
+ self,
99
+ run_id: str,
100
+ runtime_info: dict[str, str],
101
+ ) -> None:
102
+ self._report = RunReport(
103
+ run_id=run_id,
104
+ started_at=datetime.now(timezone.utc).isoformat(),
105
+ runtime=runtime_info,
106
+ config=RunConfig(
107
+ environment={
108
+ "python_version": sys.version.split()[0],
109
+ "platform": platform.system().lower(),
110
+ "arch": platform.machine(),
111
+ },
112
+ ),
113
+ )
114
+ self._scores: list[float] = []
115
+
116
+ def add_result(
117
+ self,
118
+ test_id: str,
119
+ test_name: str,
120
+ file_path: str,
121
+ position: dict[str, int],
122
+ input: str,
123
+ *,
124
+ passed: bool,
125
+ score: float,
126
+ duration_ms: float = 0.0,
127
+ metadata: dict[str, Any] | None = None,
128
+ tags: list[str] | None = None,
129
+ assertions: list[dict[str, Any]] | None = None,
130
+ classification: str = "passed",
131
+ error: str | None = None,
132
+ error_envelope: dict[str, Any] | None = None,
133
+ ) -> None:
134
+ """Add a test result to the report."""
135
+ result = RunResult(
136
+ test_id=test_id,
137
+ test_name=test_name,
138
+ file_path=file_path,
139
+ position=position,
140
+ input=input,
141
+ passed=passed,
142
+ score=score,
143
+ duration_ms=duration_ms,
144
+ metadata=metadata,
145
+ tags=tags or [],
146
+ assertions=assertions or [],
147
+ )
148
+ self._report.results.append(result)
149
+
150
+ # Update summary
151
+ s = self._report.summary
152
+ s.total += 1
153
+ s.total_duration_ms += duration_ms
154
+
155
+ if passed:
156
+ s.passed += 1
157
+ elif classification == "error":
158
+ s.errors += 1
159
+ s.success = False
160
+ elif classification == "timeout":
161
+ s.timeouts += 1
162
+ s.success = False
163
+ else:
164
+ s.failed += 1
165
+ s.success = False
166
+
167
+ s.pass_rate = (s.passed / s.total * 100) if s.total > 0 else 0.0
168
+ if score > 0:
169
+ self._scores.append(score)
170
+ s.average_score = (sum(self._scores) / len(self._scores)) if self._scores else 0.0
171
+
172
+ # Add to failures if needed
173
+ if not passed or classification in ("error", "timeout"):
174
+ failure = RunFailure(
175
+ test_id=test_id,
176
+ test_name=test_name,
177
+ file_path=file_path,
178
+ position=position,
179
+ classification=classification if classification in ("error", "timeout") else "failed",
180
+ message=error or "Test failed",
181
+ timestamp=datetime.now(timezone.utc).isoformat(),
182
+ error_envelope=error_envelope,
183
+ )
184
+ self._report.failures.append(failure)
185
+
186
+ def set_config(self, **kwargs: Any) -> None:
187
+ """Update execution configuration fields."""
188
+ for key, value in kwargs.items():
189
+ if hasattr(self._report.config, key):
190
+ setattr(self._report.config, key, value)
191
+
192
+ def build(self) -> RunReport:
193
+ """Finalize and return the complete report."""
194
+ # Sort for determinism
195
+ self._report.results.sort(key=lambda r: r.test_id)
196
+ self._report.failures.sort(key=lambda f: f.test_id)
197
+ self._report.finished_at = datetime.now(timezone.utc).isoformat()
198
+ return self._report
199
+
200
+ def to_json(self) -> str:
201
+ """Build and serialize to JSON."""
202
+ return self.build().to_json()
203
+
204
+ async def write_to_file(self, file_path: str) -> None:
205
+ """Write report to file."""
206
+ from pathlib import Path
207
+
208
+ Path(file_path).write_text(self.to_json(), encoding="utf-8")
209
+
210
+
211
+ def create_run_report(
212
+ run_id: str,
213
+ runtime_info: dict[str, str],
214
+ ) -> RunReportBuilder:
215
+ """Create a new RunReport builder."""
216
+ return RunReportBuilder(run_id, runtime_info)
217
+
218
+
219
+ def _filter_dataclass_fields(cls: type, d: dict[str, Any]) -> dict[str, Any]:
220
+ """Keep only keys that match dataclass field names — prevents TypeError on extras."""
221
+ import dataclasses
222
+
223
+ valid = {f.name for f in dataclasses.fields(cls)}
224
+ return {k: v for k, v in d.items() if k in valid}
225
+
226
+
227
+ def parse_run_report(json_str: str) -> RunReport:
228
+ """Parse a RunReport from a JSON string."""
229
+ data = json.loads(json_str)
230
+ version = data.get("schema_version", "")
231
+ if version != RUN_REPORT_SCHEMA_VERSION:
232
+ raise ValueError(f"Unsupported RunReport schema version: {version}")
233
+
234
+ summary = RunSummary(**_filter_dataclass_fields(RunSummary, data.get("summary", {})))
235
+ config = RunConfig(**_filter_dataclass_fields(RunConfig, data.get("config", {})))
236
+ results = [RunResult(**_filter_dataclass_fields(RunResult, r)) for r in data.get("results", [])]
237
+ failures = [RunFailure(**_filter_dataclass_fields(RunFailure, f)) for f in data.get("failures", [])]
238
+
239
+ return RunReport(
240
+ schema_version=data["schema_version"],
241
+ run_id=data["run_id"],
242
+ started_at=data.get("started_at", ""),
243
+ finished_at=data.get("finished_at", ""),
244
+ runtime=data.get("runtime", {}),
245
+ results=results,
246
+ failures=failures,
247
+ summary=summary,
248
+ config=config,
249
+ )
@@ -0,0 +1,143 @@
1
+ """Core types for the runtime foundation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Literal
7
+
8
+
9
+ @dataclass
10
+ class DependsOn:
11
+ """Dependency hints for impact analysis."""
12
+
13
+ prompts: list[str] = field(default_factory=list)
14
+ datasets: list[str] = field(default_factory=list)
15
+ tools: list[str] = field(default_factory=list)
16
+ code: list[str] = field(default_factory=list)
17
+
18
+
19
+ @dataclass
20
+ class SpecOptions:
21
+ timeout_ms: int = 30_000
22
+ retries: int = 0
23
+ tags: list[str] = field(default_factory=list)
24
+ skip: bool = False
25
+ only: bool = False
26
+ description: str | None = None
27
+ budget: str | None = None
28
+ model: str | None = None
29
+ metadata: dict[str, Any] | None = None
30
+ depends_on: DependsOn | None = None
31
+
32
+
33
+ @dataclass
34
+ class SpecConfig:
35
+ name: str
36
+ executor: Any = None
37
+ options: SpecOptions = field(default_factory=SpecOptions)
38
+ description: str | None = None
39
+ suite: str | None = None
40
+ tags: list[str] | None = None
41
+ timeout: int | None = None
42
+ retries: int | None = None
43
+ budget: str | None = None
44
+ model: str | None = None
45
+ metadata: dict[str, Any] | None = None
46
+ depends_on: DependsOn | None = None
47
+
48
+
49
+ @dataclass
50
+ class EvalSpec:
51
+ id: str
52
+ name: str
53
+ executor: Any
54
+ options: SpecOptions = field(default_factory=SpecOptions)
55
+ file_path: str | None = None
56
+ suite: str | None = None
57
+ description: str | None = None
58
+ position: dict[str, int] | None = None
59
+ tags: list[str] = field(default_factory=list)
60
+ metadata: dict[str, Any] | None = None
61
+ config: dict[str, Any] | None = None
62
+ mode: Literal["normal", "skip", "only"] = "normal"
63
+
64
+
65
+ @dataclass
66
+ class EvalContext:
67
+ input: Any = None
68
+ metadata: dict[str, Any] = field(default_factory=dict)
69
+ options: dict[str, Any] = field(default_factory=dict)
70
+ trace_id: str | None = None
71
+
72
+
73
+ @dataclass
74
+ class EvalResult:
75
+ passed: bool
76
+ score: float = 0.0
77
+ assertions: list[Any] = field(default_factory=list)
78
+ metadata: dict[str, Any] = field(default_factory=dict)
79
+ error: str | None = None
80
+ duration_ms: float = 0.0
81
+ status: Literal["passed", "failed", "error", "timeout"] = "passed"
82
+ output: str | None = None
83
+ tokens: int | None = None
84
+
85
+
86
+ @dataclass
87
+ class ExecutorCapabilities:
88
+ supports_async: bool = True
89
+ supports_timeout: bool = True
90
+ supports_retries: bool = True
91
+ supports_parallel: bool = False
92
+
93
+
94
+ @dataclass
95
+ class ExecutionErrorEnvelope:
96
+ error_type: str
97
+ message: str
98
+ stack: str | None = None
99
+ retryable: bool = False
100
+
101
+
102
+ @dataclass
103
+ class RuntimeHealth:
104
+ status: Literal["healthy", "degraded", "unhealthy"] = "healthy"
105
+ spec_count: int = 0
106
+ memory_estimate_mb: float = 0.0
107
+ uptime_ms: float = 0.0
108
+
109
+
110
+ # ── Error classes ────────────────────────────────────────────────────
111
+
112
+
113
+ class EvalRuntimeError(Exception):
114
+ """Base error for runtime operations."""
115
+
116
+ pass
117
+
118
+
119
+ class SpecRegistrationError(EvalRuntimeError):
120
+ """Raised when a spec fails to register."""
121
+
122
+ pass
123
+
124
+
125
+ class SpecExecutionError(EvalRuntimeError):
126
+ """Raised when a spec fails to execute."""
127
+
128
+ pass
129
+
130
+
131
+ class RuntimeError(EvalRuntimeError):
132
+ """Raised for general runtime errors."""
133
+
134
+ pass
135
+
136
+
137
+ class EvalExecutionError(EvalRuntimeError):
138
+ """Raised during eval execution with context."""
139
+
140
+ def __init__(self, message: str, spec_id: str, cause: Exception | None = None) -> None:
141
+ super().__init__(message)
142
+ self.spec_id = spec_id
143
+ self.cause = cause
@@ -0,0 +1,219 @@
1
+ """Snapshot testing — save, load, compare LLM outputs against golden snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass
14
+ class SnapshotMetadata:
15
+ name: str
16
+ created_at: str
17
+ content_hash: str
18
+ version: int = 1
19
+
20
+
21
+ @dataclass
22
+ class SnapshotData:
23
+ name: str
24
+ output: str
25
+ metadata: SnapshotMetadata
26
+ tags: list[str] = field(default_factory=list)
27
+
28
+
29
+ @dataclass
30
+ class SnapshotComparison:
31
+ name: str
32
+ matches: bool
33
+ similarity: float
34
+ current_output: str
35
+ snapshot_output: str
36
+ diff_lines: list[str] = field(default_factory=list)
37
+
38
+
39
+ _DEFAULT_DIR = ".snapshots"
40
+
41
+
42
+ def _safe_name(name: str) -> str:
43
+ """Sanitize snapshot name for filesystem use."""
44
+ safe = re.sub(r"[^\w\-.]", "_", name)
45
+ if ".." in safe or safe.startswith("/") or safe.startswith("\\"):
46
+ raise ValueError(f"Invalid snapshot name: {name}")
47
+ return safe
48
+
49
+
50
+ def _content_hash(text: str) -> str:
51
+ return hashlib.sha256(text.encode()).hexdigest()
52
+
53
+
54
+ def _similarity(a: str, b: str) -> float:
55
+ """Line-by-line similarity ratio."""
56
+ lines_a = a.splitlines()
57
+ lines_b = b.splitlines()
58
+ if not lines_a and not lines_b:
59
+ return 1.0
60
+ total = max(len(lines_a), len(lines_b))
61
+ matches = sum(1 for la, lb in zip(lines_a, lines_b, strict=False) if la == lb)
62
+ return matches / total if total > 0 else 1.0
63
+
64
+
65
+ class SnapshotManager:
66
+ """Manage snapshot files on disk.
67
+
68
+ Usage::
69
+
70
+ mgr = SnapshotManager()
71
+ snap = mgr.save("my-test", "Hello world output")
72
+ comparison = mgr.compare("my-test", "Hello world output v2")
73
+ """
74
+
75
+ def __init__(self, directory: str = _DEFAULT_DIR) -> None:
76
+ self._dir = Path(directory)
77
+
78
+ def _path(self, name: str) -> Path:
79
+ return self._dir / f"{_safe_name(name)}.json"
80
+
81
+ def save(self, name: str, output: str, tags: list[str] | None = None) -> SnapshotData:
82
+ """Save a snapshot to disk."""
83
+ self._dir.mkdir(parents=True, exist_ok=True)
84
+ snap = SnapshotData(
85
+ name=name,
86
+ output=output,
87
+ metadata=SnapshotMetadata(
88
+ name=name,
89
+ created_at=datetime.now(timezone.utc).isoformat(),
90
+ content_hash=_content_hash(output),
91
+ ),
92
+ tags=tags or [],
93
+ )
94
+ path = self._path(name)
95
+ path.write_text(
96
+ json.dumps(
97
+ {
98
+ "name": snap.name,
99
+ "output": snap.output,
100
+ "metadata": {
101
+ "name": snap.metadata.name,
102
+ "created_at": snap.metadata.created_at,
103
+ "content_hash": snap.metadata.content_hash,
104
+ "version": snap.metadata.version,
105
+ },
106
+ "tags": snap.tags,
107
+ },
108
+ indent=2,
109
+ ),
110
+ encoding="utf-8",
111
+ )
112
+ return snap
113
+
114
+ def load(self, name: str) -> SnapshotData | None:
115
+ """Load a snapshot from disk."""
116
+ path = self._path(name)
117
+ if not path.exists():
118
+ return None
119
+ raw = json.loads(path.read_text(encoding="utf-8"))
120
+ return SnapshotData(
121
+ name=raw["name"],
122
+ output=raw["output"],
123
+ metadata=SnapshotMetadata(**raw["metadata"]),
124
+ tags=raw.get("tags", []),
125
+ )
126
+
127
+ def compare(self, name: str, current_output: str) -> SnapshotComparison:
128
+ """Compare current output against a saved snapshot."""
129
+ existing = self.load(name)
130
+ if existing is None:
131
+ self.save(name, current_output)
132
+ return SnapshotComparison(
133
+ name=name,
134
+ matches=True,
135
+ similarity=1.0,
136
+ current_output=current_output,
137
+ snapshot_output=current_output,
138
+ )
139
+
140
+ matches = existing.output == current_output
141
+ sim = _similarity(existing.output, current_output)
142
+
143
+ diff: list[str] = []
144
+ old_lines = existing.output.splitlines()
145
+ new_lines = current_output.splitlines()
146
+ for i in range(max(len(old_lines), len(new_lines))):
147
+ old = old_lines[i] if i < len(old_lines) else ""
148
+ new = new_lines[i] if i < len(new_lines) else ""
149
+ if old != new:
150
+ diff.append(f"L{i + 1}: -{old!r} +{new!r}")
151
+
152
+ return SnapshotComparison(
153
+ name=name,
154
+ matches=matches,
155
+ similarity=sim,
156
+ current_output=current_output,
157
+ snapshot_output=existing.output,
158
+ diff_lines=diff,
159
+ )
160
+
161
+ def delete(self, name: str) -> bool:
162
+ path = self._path(name)
163
+ if path.exists():
164
+ path.unlink()
165
+ return True
166
+ return False
167
+
168
+ def list_snapshots(self) -> list[SnapshotData]:
169
+ if not self._dir.exists():
170
+ return []
171
+ results: list[SnapshotData] = []
172
+ for p in sorted(self._dir.glob("*.json")):
173
+ try:
174
+ raw = json.loads(p.read_text(encoding="utf-8"))
175
+ results.append(
176
+ SnapshotData(
177
+ name=raw["name"],
178
+ output=raw["output"],
179
+ metadata=SnapshotMetadata(**raw["metadata"]),
180
+ tags=raw.get("tags", []),
181
+ )
182
+ )
183
+ except (json.JSONDecodeError, KeyError):
184
+ continue
185
+ return results
186
+
187
+
188
+ # Module-level convenience functions using a default manager
189
+
190
+ _default_manager: SnapshotManager | None = None
191
+
192
+
193
+ def _get_manager(directory: str | None = None) -> SnapshotManager:
194
+ global _default_manager
195
+ if directory is not None:
196
+ return SnapshotManager(directory)
197
+ if _default_manager is None:
198
+ _default_manager = SnapshotManager()
199
+ return _default_manager
200
+
201
+
202
+ def snapshot(output: str, name: str, *, directory: str | None = None, tags: list[str] | None = None) -> SnapshotData:
203
+ return _get_manager(directory).save(name, output, tags)
204
+
205
+
206
+ def load_snapshot(name: str, *, directory: str | None = None) -> SnapshotData | None:
207
+ return _get_manager(directory).load(name)
208
+
209
+
210
+ def compare_with_snapshot(name: str, current_output: str, *, directory: str | None = None) -> SnapshotComparison:
211
+ return _get_manager(directory).compare(name, current_output)
212
+
213
+
214
+ def delete_snapshot(name: str, *, directory: str | None = None) -> bool:
215
+ return _get_manager(directory).delete(name)
216
+
217
+
218
+ def list_snapshots(*, directory: str | None = None) -> list[SnapshotData]:
219
+ return _get_manager(directory).list_snapshots()