blocklog 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. blocklog/__init__.py +78 -0
  2. blocklog/_global.py +20 -0
  3. blocklog/_init_fn.py +95 -0
  4. blocklog/api/approval.py +182 -0
  5. blocklog/api/compliance.py +162 -0
  6. blocklog/api/decisions.py +177 -0
  7. blocklog/api/incidents.py +306 -0
  8. blocklog/api/replay.py +285 -0
  9. blocklog/api/traces.py +137 -0
  10. blocklog/api/verify.py +100 -0
  11. blocklog/approval.py +119 -0
  12. blocklog/async_client.py +28 -0
  13. blocklog/batching/buffer.py +22 -0
  14. blocklog/client.py +194 -0
  15. blocklog/compliance.py +95 -0
  16. blocklog/config.py +17 -0
  17. blocklog/context/managers.py +15 -0
  18. blocklog/context/vars.py +13 -0
  19. blocklog/decorators/__init__.py +4 -0
  20. blocklog/decorators/agent.py +219 -0
  21. blocklog/decorators/tool.py +206 -0
  22. blocklog/incident.py +89 -0
  23. blocklog/integrations/langchain.py +129 -0
  24. blocklog/integrations/langgraph.py +3 -0
  25. blocklog/integrations/openai_agents.py +3 -0
  26. blocklog/managers/__init__.py +3 -0
  27. blocklog/managers/decision.py +336 -0
  28. blocklog/middleware/hooks.py +11 -0
  29. blocklog/models/events.py +33 -0
  30. blocklog/models/responses.py +18 -0
  31. blocklog/replay.py +64 -0
  32. blocklog/signing/canonical.py +5 -0
  33. blocklog/signing/ed25519.py +25 -0
  34. blocklog/transport/auth.py +8 -0
  35. blocklog/transport/httpx_async.py +39 -0
  36. blocklog/transport/httpx_sync.py +36 -0
  37. blocklog/transport/retry.py +26 -0
  38. blocklog/verify.py +72 -0
  39. blocklog-0.2.0.dist-info/METADATA +272 -0
  40. blocklog-0.2.0.dist-info/RECORD +43 -0
  41. blocklog-0.2.0.dist-info/WHEEL +5 -0
  42. blocklog-0.2.0.dist-info/licenses/LICENSE +21 -0
  43. blocklog-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,336 @@
1
+ """
2
+ blocklog.managers.decision
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ The ``decision()`` context manager — the highest-priority surface of the SDK.
5
+
6
+ Usage (Layer 1)::
7
+
8
+ import blocklog
9
+
10
+ blocklog.init(api_key="blk_...")
11
+
12
+ with blocklog.decision(type="BUY", asset="TSLA", confidence=0.87) as d:
13
+ price = fetch_price("TSLA")
14
+ d.record_input(price=price, signal="momentum_crossover")
15
+
16
+ order = place_order("TSLA", qty=100)
17
+ d.record_output(order_id=order.id, filled_at=order.price)
18
+
19
+ if order.value > 500_000:
20
+ d.request_approval(reason="Trade exceeds threshold")
21
+
22
+ # After the block:
23
+ print(d.id) # UUID of the recorded decision
24
+ print(d.verified) # True if blockchain-anchored
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import traceback as _traceback
30
+ from contextlib import contextmanager
31
+ from datetime import datetime, timezone
32
+ from typing import Any, Generator
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class DecisionContext:
38
+ """Live handle to a decision being recorded.
39
+
40
+ Obtained from the ``decision()`` context manager. Do not instantiate
41
+ directly.
42
+
43
+ Attributes
44
+ ----------
45
+ id : str | None
46
+ The backend-assigned UUID for this decision. Available after the
47
+ context body starts executing (set during ``__enter__``).
48
+ decision_type : str
49
+ The decision type you passed in (e.g. ``"BUY"``, ``"TRADE"``,
50
+ ``"APPROVE"``).
51
+ status : str
52
+ ``"open"`` while inside the ``with`` block, ``"complete"`` on
53
+ clean exit, ``"error"`` on exception.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ decision_type: str,
60
+ asset: str | None = None,
61
+ confidence: float | None = None,
62
+ metadata: dict[str, Any] | None = None,
63
+ agent_id: str | None = None,
64
+ trace_id: str | None = None,
65
+ ) -> None:
66
+ self.decision_type = decision_type
67
+ self.asset = asset
68
+ self.confidence = confidence
69
+ self.metadata: dict[str, Any] = metadata or {}
70
+ self.agent_id = agent_id
71
+ self._trace_id = trace_id
72
+
73
+ self.id: str | None = None
74
+ self.status: str = "open"
75
+ self._inputs: list[dict] = []
76
+ self._outputs: list[dict] = []
77
+ self._tags: list[str] = []
78
+ self._started_at: datetime = datetime.now(timezone.utc)
79
+ self._approval_requested: bool = False
80
+
81
+ # ------------------------------------------------------------------
82
+ # Public methods available inside the ``with`` block
83
+ # ------------------------------------------------------------------
84
+
85
+ def record_input(self, **kwargs: Any) -> "DecisionContext":
86
+ """Record structured inputs that fed into this decision.
87
+
88
+ Call this before the AI model / logic runs.
89
+
90
+ Parameters
91
+ ----------
92
+ **kwargs
93
+ Arbitrary key-value pairs describing the inputs. Will be
94
+ stored verbatim and appear in replays and forensic timelines.
95
+
96
+ Examples
97
+ --------
98
+ >>> d.record_input(price=412.50, volume=1_200_000, signal="rsi_oversold")
99
+ """
100
+ self._inputs.append({"recorded_at": _now_iso(), **kwargs})
101
+ self._send_event("DECISION_INPUT", kwargs)
102
+ return self
103
+
104
+ def record_output(self, **kwargs: Any) -> "DecisionContext":
105
+ """Record structured outputs / results of this decision.
106
+
107
+ Call this after the AI model / logic produces a result.
108
+
109
+ Parameters
110
+ ----------
111
+ **kwargs
112
+ Arbitrary key-value pairs describing the outputs.
113
+
114
+ Examples
115
+ --------
116
+ >>> d.record_output(order_id="ord_88", filled_at=413.10, qty=100)
117
+ """
118
+ self._outputs.append({"recorded_at": _now_iso(), **kwargs})
119
+ self._send_event("DECISION_OUTPUT", kwargs)
120
+ return self
121
+
122
+ def tag(self, *tags: str) -> "DecisionContext":
123
+ """Attach one or more string labels to this decision.
124
+
125
+ Tags appear in the dashboard and can be used to filter/search.
126
+
127
+ Examples
128
+ --------
129
+ >>> d.tag("high-value", "requires-review", "momentum-strategy")
130
+ """
131
+ self._tags.extend(tags)
132
+ return self
133
+
134
+ def request_approval(
135
+ self,
136
+ reason: str,
137
+ reviewer: str | None = None,
138
+ ) -> "DecisionContext":
139
+ """Request human approval for this decision (HITL).
140
+
141
+ This is a **non-blocking** call. It records the approval request
142
+ against the decision and notifies the reviewer (via backend
143
+ webhooks / email, as configured in your Blocklog workspace).
144
+ Execution continues; it is the caller's responsibility to gate
145
+ further actions on approval status.
146
+
147
+ Parameters
148
+ ----------
149
+ reason:
150
+ Human-readable explanation for why approval is needed.
151
+ reviewer:
152
+ Optional email / identifier of the intended reviewer.
153
+
154
+ Examples
155
+ --------
156
+ >>> if order_value > 500_000:
157
+ ... d.request_approval(
158
+ ... reason="Trade exceeds $500k threshold",
159
+ ... reviewer="risk-team@fund.com",
160
+ ... )
161
+ """
162
+ self._approval_requested = True
163
+ try:
164
+ from blocklog._global import get_client
165
+ client = get_client()
166
+ client.approval.request(
167
+ decision_id=self.id,
168
+ reason=reason,
169
+ reviewer=reviewer,
170
+ )
171
+ except Exception as exc: # noqa: BLE001
172
+ logger.warning("blocklog: approval.request() failed: %s", exc)
173
+ return self
174
+
175
+ def verify(self) -> dict[str, Any]:
176
+ """Immediately verify this decision against the blockchain anchor.
177
+
178
+ Returns
179
+ -------
180
+ dict
181
+ Verification result from ``GET /decisions/{id}/verify``.
182
+ """
183
+ if not self.id:
184
+ raise RuntimeError("Decision has not been committed yet.")
185
+ from blocklog._global import get_client
186
+ return get_client().decisions.verify(self.id)
187
+
188
+ # ------------------------------------------------------------------
189
+ # Internal helpers
190
+ # ------------------------------------------------------------------
191
+
192
+ def _send_event(self, event_type: str, payload: dict) -> None:
193
+ """Fire an event log to the ingest endpoint (best-effort)."""
194
+ try:
195
+ from blocklog._global import get_client
196
+ from blocklog.context.vars import get_context
197
+
198
+ ctx = get_context()
199
+ client = get_client()
200
+ client.event(
201
+ event_type,
202
+ payload={
203
+ "decision_id": self.id,
204
+ "decision_type": self.decision_type,
205
+ "asset": self.asset,
206
+ **payload,
207
+ },
208
+ trace_id=str(ctx.trace_id) if ctx else self._trace_id,
209
+ session_id=str(ctx.session_id) if ctx else None,
210
+ actor_id=self.agent_id or (ctx.agent_id if ctx else None),
211
+ actor_type="agent",
212
+ )
213
+ except Exception as exc: # noqa: BLE001
214
+ logger.debug("blocklog: event send failed (%s): %s", event_type, exc)
215
+
216
+ def _commit(self) -> None:
217
+ """Create the decision record in the backend."""
218
+ try:
219
+ from blocklog._global import get_client
220
+ from blocklog.context.vars import get_context
221
+
222
+ ctx = get_context()
223
+ client = get_client()
224
+ result = client.decisions.create(
225
+ decision_type=self.decision_type,
226
+ asset=self.asset,
227
+ confidence=self.confidence,
228
+ metadata={
229
+ **self.metadata,
230
+ "tags": self._tags,
231
+ "started_at": self._started_at.isoformat(),
232
+ },
233
+ trace_id=str(ctx.trace_id) if ctx else self._trace_id,
234
+ session_id=str(ctx.session_id) if ctx else None,
235
+ agent_id=self.agent_id or (ctx.agent_id if ctx else None),
236
+ )
237
+ self.id = str(result.get("id", result.get("decision_id", "")))
238
+ except Exception as exc: # noqa: BLE001
239
+ logger.warning("blocklog: decision.create() failed: %s", exc)
240
+
241
+ def _complete(self) -> None:
242
+ self.status = "complete"
243
+ self._send_event("DECISION_COMPLETE", {
244
+ "inputs": self._inputs,
245
+ "outputs": self._outputs,
246
+ "tags": self._tags,
247
+ "approval_requested": self._approval_requested,
248
+ "completed_at": _now_iso(),
249
+ })
250
+
251
+ def _error(self, exc: BaseException) -> None:
252
+ self.status = "error"
253
+ self._send_event("DECISION_ERROR", {
254
+ "error_type": type(exc).__name__,
255
+ "error_message": str(exc),
256
+ "traceback": _traceback.format_exc(),
257
+ "tags": self._tags,
258
+ "failed_at": _now_iso(),
259
+ })
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Public factory
264
+ # ---------------------------------------------------------------------------
265
+
266
+ @contextmanager
267
+ def decision(
268
+ *,
269
+ type: str, # noqa: A002
270
+ asset: str | None = None,
271
+ confidence: float | None = None,
272
+ metadata: dict[str, Any] | None = None,
273
+ agent_id: str | None = None,
274
+ trace_id: str | None = None,
275
+ ) -> Generator[DecisionContext, None, None]:
276
+ """Context manager for recording an AI decision.
277
+
278
+ Creates a decision record in Blocklog, lets you annotate it with
279
+ inputs/outputs, then automatically closes it on exit — cleanly or
280
+ with an error event if an exception is raised.
281
+
282
+ Parameters
283
+ ----------
284
+ type:
285
+ Decision type identifier. Use a short, uppercase string that is
286
+ meaningful in your domain (``"BUY"``, ``"SELL"``, ``"APPROVE"``,
287
+ ``"REJECT"``, ``"ROUTE"``, ``"SUMMARISE"``…).
288
+ asset:
289
+ Optional asset or resource this decision is about
290
+ (``"TSLA"``, ``"customer_123"``, ``"invoice_456"``…).
291
+ confidence:
292
+ Optional model confidence score between 0 and 1.
293
+ metadata:
294
+ Arbitrary extra fields stored with the decision record.
295
+ agent_id:
296
+ Override the agent identity for this decision. Normally
297
+ inherited from the surrounding ``@agent`` context.
298
+ trace_id:
299
+ Override the trace ID. Normally inherited automatically.
300
+
301
+ Yields
302
+ ------
303
+ DecisionContext
304
+ A live handle you can use to call ``record_input()``,
305
+ ``record_output()``, ``tag()``, ``request_approval()``, etc.
306
+
307
+ Examples
308
+ --------
309
+ >>> with blocklog.decision(type="BUY", asset="TSLA", confidence=0.87) as d:
310
+ ... d.record_input(price=412.50, signal="momentum")
311
+ ... order = place_order("TSLA", qty=100)
312
+ ... d.record_output(order_id=order.id)
313
+ """
314
+ ctx = DecisionContext(
315
+ decision_type=type,
316
+ asset=asset,
317
+ confidence=confidence,
318
+ metadata=metadata,
319
+ agent_id=agent_id,
320
+ trace_id=trace_id,
321
+ )
322
+ ctx._commit()
323
+ try:
324
+ yield ctx
325
+ ctx._complete()
326
+ except BaseException as exc:
327
+ ctx._error(exc)
328
+ raise
329
+
330
+
331
+ # ---------------------------------------------------------------------------
332
+ # Helpers
333
+ # ---------------------------------------------------------------------------
334
+
335
+ def _now_iso() -> str:
336
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,11 @@
1
+ from collections.abc import Callable
2
+
3
+
4
+ Hook = Callable[[dict], dict]
5
+
6
+
7
+ def apply_hooks(payload: dict, hooks: list[Hook]) -> dict:
8
+ current = payload
9
+ for hook in hooks:
10
+ current = hook(current)
11
+ return current
@@ -0,0 +1,33 @@
1
+ from datetime import datetime, timezone
2
+ from uuid import UUID, uuid4
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class EventEnvelope(BaseModel):
8
+ event_type: str
9
+ payload: dict
10
+ source: str = "python-sdk"
11
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
12
+ idempotency_key: str | None = None
13
+ trace_id: UUID | None = None
14
+ session_id: UUID | None = None
15
+ workflow_id: UUID | None = None
16
+ parent_event_id: UUID | None = None
17
+ root_event_id: UUID | None = None
18
+ span_id: str | None = None
19
+ attempt_no: int = 1
20
+ causality_type: str | None = None
21
+ schema_version: str = "1.0"
22
+ event_version: str = "1.0"
23
+ actor_type: str | None = None
24
+ actor_id: str | None = None
25
+ agent_metadata: dict = Field(default_factory=dict)
26
+
27
+
28
+ class SessionContext(BaseModel):
29
+ trace_id: UUID = Field(default_factory=uuid4)
30
+ session_id: UUID = Field(default_factory=uuid4)
31
+ workflow_id: UUID | None = None
32
+ agent_id: str | None = None
33
+ source: str = "python-sdk"
@@ -0,0 +1,18 @@
1
+ from datetime import datetime
2
+ from uuid import UUID
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class IngestResponse(BaseModel):
8
+ log_id: str
9
+ company_id: str
10
+ event_type: str
11
+ source: str
12
+ idempotency_key: str | None = None
13
+ timestamp: datetime | None = None
14
+ created_at: datetime
15
+ trace_id: UUID | None = None
16
+ session_id: UUID | None = None
17
+ workflow_id: UUID | None = None
18
+ parent_event_id: UUID | None = None
blocklog/replay.py ADDED
@@ -0,0 +1,64 @@
1
+ """
2
+ blocklog.replay
3
+ ~~~~~~~~~~~~~~~
4
+ Module-level factory for forensic replay sessions.
5
+
6
+ Usage (Layer 1)::
7
+
8
+ import blocklog
9
+
10
+ session = blocklog.replay("trace-abc-123")
11
+
12
+ # Explore what happened
13
+ timeline = session.timeline()
14
+ root_cause = session.root_cause()
15
+ graph = session.causal_graph()
16
+ stale = session.staleness()
17
+
18
+ # What would have happened differently?
19
+ cf = session.counterfactual(token_id="tok_x", modified_inputs={"price": 400})
20
+
21
+ # Compare against another run
22
+ diff = session.compare(other_trace_id="trace-def-456")
23
+ """
24
+ from __future__ import annotations
25
+
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ if TYPE_CHECKING:
29
+ from blocklog.api.replay import ReplaySession
30
+
31
+
32
+ def replay(
33
+ trace_id: str,
34
+ *,
35
+ token_id: str | None = None,
36
+ metadata: dict[str, Any] | None = None,
37
+ ) -> "ReplaySession":
38
+ """Create a forensic replay session for a trace.
39
+
40
+ Parameters
41
+ ----------
42
+ trace_id:
43
+ The trace to replay. All events associated with this trace will
44
+ be reconstructed into a timeline, causal graph, and analysis.
45
+ token_id:
46
+ Execution token ID to bind to the session (required for
47
+ counterfactual simulations).
48
+ metadata:
49
+ Optional extra metadata for the replay session.
50
+
51
+ Returns
52
+ -------
53
+ ReplaySession
54
+ A session handle with methods for timeline, root_cause,
55
+ causal_graph, staleness, divergence, counterfactual, and compare.
56
+
57
+ Examples
58
+ --------
59
+ >>> session = blocklog.replay("trace-abc")
60
+ >>> cause = session.root_cause()
61
+ >>> print(cause["description"])
62
+ """
63
+ from blocklog._global import get_client
64
+ return get_client().replay.create(trace_id=trace_id, token_id=token_id, metadata=metadata)
@@ -0,0 +1,5 @@
1
+ import json
2
+
3
+
4
+ def canonical_json(payload: dict) -> bytes:
5
+ return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
@@ -0,0 +1,25 @@
1
+ from hashlib import sha256
2
+
3
+ from .canonical import canonical_json
4
+
5
+
6
+ def hash_sign(payload: dict, private_key: str | None = None) -> str:
7
+ """Generate a deterministic hash signature for a payload.
8
+
9
+ This is NOT cryptographic Ed25519 signing. It generates a deterministic
10
+ SHA256 hash of the payload combined with a seed/key for tamper-evidence
11
+ purposes. For true cryptographic signing, use the cryptography library.
12
+
13
+ Args:
14
+ payload: The payload to hash
15
+ private_key: Optional seed/key for the hash (defaults to "blocklog")
16
+
17
+ Returns:
18
+ Hexadecimal SHA256 hash string
19
+ """
20
+ seed = private_key or "blocklog"
21
+ return sha256(seed.encode("utf-8") + canonical_json(payload)).hexdigest()
22
+
23
+
24
+ # Backward compatibility alias
25
+ pseudo_sign = hash_sign
@@ -0,0 +1,8 @@
1
+ def build_headers(api_key: str, extra: dict[str, str] | None = None) -> dict[str, str]:
2
+ headers = {
3
+ "Content-Type": "application/json",
4
+ "X-API-Key": api_key,
5
+ }
6
+ if extra:
7
+ headers.update(extra)
8
+ return headers
@@ -0,0 +1,39 @@
1
+ import asyncio
2
+
3
+ try:
4
+ import httpx
5
+ except ModuleNotFoundError: # pragma: no cover - exercised in local fallback mode
6
+ httpx = None
7
+ import requests
8
+ else:
9
+ requests = None
10
+
11
+ from .auth import build_headers
12
+
13
+
14
+ class AsyncTransport:
15
+ def __init__(self, *, base_url: str, api_key: str, timeout: float) -> None:
16
+ self.base_url = base_url.rstrip("/")
17
+ self.api_key = api_key
18
+ self.timeout = timeout
19
+ self.client = httpx.AsyncClient(timeout=timeout) if httpx is not None else None
20
+
21
+ async def request(self, method: str, path: str, *, json: dict | None = None, headers: dict[str, str] | None = None):
22
+ if self.client is not None:
23
+ response = await self.client.request(
24
+ method,
25
+ f"{self.base_url}{path}",
26
+ json=json,
27
+ headers=build_headers(self.api_key, headers),
28
+ )
29
+ else:
30
+ response = await asyncio.to_thread(
31
+ requests.request,
32
+ method,
33
+ f"{self.base_url}{path}",
34
+ json=json,
35
+ headers=build_headers(self.api_key, headers),
36
+ timeout=self.timeout,
37
+ )
38
+ response.raise_for_status()
39
+ return response.json()
@@ -0,0 +1,36 @@
1
+ try:
2
+ import httpx
3
+ except ModuleNotFoundError: # pragma: no cover - exercised in local fallback mode
4
+ httpx = None
5
+ import requests
6
+ else:
7
+ requests = None
8
+
9
+ from .auth import build_headers
10
+
11
+
12
+ class SyncTransport:
13
+ def __init__(self, *, base_url: str, api_key: str, timeout: float) -> None:
14
+ self.base_url = base_url.rstrip("/")
15
+ self.api_key = api_key
16
+ self.timeout = timeout
17
+ self.client = httpx.Client(timeout=timeout) if httpx is not None else None
18
+
19
+ def request(self, method: str, path: str, *, json: dict | None = None, headers: dict[str, str] | None = None):
20
+ if self.client is not None:
21
+ response = self.client.request(
22
+ method,
23
+ f"{self.base_url}{path}",
24
+ json=json,
25
+ headers=build_headers(self.api_key, headers),
26
+ )
27
+ else:
28
+ response = requests.request(
29
+ method,
30
+ f"{self.base_url}{path}",
31
+ json=json,
32
+ headers=build_headers(self.api_key, headers),
33
+ timeout=self.timeout,
34
+ )
35
+ response.raise_for_status()
36
+ return response.json()
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from random import random
5
+ from time import sleep
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class RetryPolicy:
10
+ max_retries: int = 3
11
+ base_delay: float = 0.25
12
+
13
+ def backoff(self, attempt: int) -> float:
14
+ return self.base_delay * (2 ** attempt) + random() * 0.1
15
+
16
+ def run(self, fn):
17
+ last_error = None
18
+ for attempt in range(self.max_retries):
19
+ try:
20
+ return fn()
21
+ except Exception as exc: # noqa: BLE001
22
+ last_error = exc
23
+ if attempt == self.max_retries - 1:
24
+ raise
25
+ sleep(self.backoff(attempt))
26
+ raise RuntimeError("retry failed") from last_error
blocklog/verify.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ blocklog.verify
3
+ ~~~~~~~~~~~~~~~
4
+ Module-level namespace for cryptographic verification.
5
+
6
+ Usage (Layer 1)::
7
+
8
+ import blocklog
9
+
10
+ result = blocklog.verify.log("log-uuid-here")
11
+ result = blocklog.verify.batch("batch-uuid-here")
12
+ result = blocklog.verify.decision("dec-uuid-here")
13
+
14
+ print(result["status"]) # "verified"
15
+ print(result["time_attestation"])
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+
22
+ def log(log_id: str) -> dict[str, Any]:
23
+ """Verify a single log entry against its Merkle proof.
24
+
25
+ Parameters
26
+ ----------
27
+ log_id:
28
+ UUID of the log entry to verify.
29
+
30
+ Returns
31
+ -------
32
+ dict
33
+ Keys: ``status``, ``merkle_proof``, ``batch_anchor``,
34
+ ``time_attestation``, ``details``.
35
+ """
36
+ from blocklog._global import get_client
37
+ return get_client().verify.log(log_id)
38
+
39
+
40
+ def batch(batch_id: str) -> dict[str, Any]:
41
+ """Verify an entire batch against its blockchain anchor.
42
+
43
+ Parameters
44
+ ----------
45
+ batch_id:
46
+ ID of the batch to verify.
47
+
48
+ Returns
49
+ -------
50
+ dict
51
+ Keys: ``status``, ``anchor_tx``, ``timestamp``,
52
+ ``time_attestation``, ``details``.
53
+ """
54
+ from blocklog._global import get_client
55
+ return get_client().verify.batch(batch_id)
56
+
57
+
58
+ def decision(decision_id: str) -> dict[str, Any]:
59
+ """Verify all evidence attached to a specific decision.
60
+
61
+ Parameters
62
+ ----------
63
+ decision_id:
64
+ UUID of the decision to verify.
65
+
66
+ Returns
67
+ -------
68
+ dict
69
+ Verification summary including Merkle and blockchain evidence.
70
+ """
71
+ from blocklog._global import get_client
72
+ return get_client().verify.decision(decision_id)