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.
- blocklog/__init__.py +78 -0
- blocklog/_global.py +20 -0
- blocklog/_init_fn.py +95 -0
- blocklog/api/approval.py +182 -0
- blocklog/api/compliance.py +162 -0
- blocklog/api/decisions.py +177 -0
- blocklog/api/incidents.py +306 -0
- blocklog/api/replay.py +285 -0
- blocklog/api/traces.py +137 -0
- blocklog/api/verify.py +100 -0
- blocklog/approval.py +119 -0
- blocklog/async_client.py +28 -0
- blocklog/batching/buffer.py +22 -0
- blocklog/client.py +194 -0
- blocklog/compliance.py +95 -0
- blocklog/config.py +17 -0
- blocklog/context/managers.py +15 -0
- blocklog/context/vars.py +13 -0
- blocklog/decorators/__init__.py +4 -0
- blocklog/decorators/agent.py +219 -0
- blocklog/decorators/tool.py +206 -0
- blocklog/incident.py +89 -0
- blocklog/integrations/langchain.py +129 -0
- blocklog/integrations/langgraph.py +3 -0
- blocklog/integrations/openai_agents.py +3 -0
- blocklog/managers/__init__.py +3 -0
- blocklog/managers/decision.py +336 -0
- blocklog/middleware/hooks.py +11 -0
- blocklog/models/events.py +33 -0
- blocklog/models/responses.py +18 -0
- blocklog/replay.py +64 -0
- blocklog/signing/canonical.py +5 -0
- blocklog/signing/ed25519.py +25 -0
- blocklog/transport/auth.py +8 -0
- blocklog/transport/httpx_async.py +39 -0
- blocklog/transport/httpx_sync.py +36 -0
- blocklog/transport/retry.py +26 -0
- blocklog/verify.py +72 -0
- blocklog-0.2.0.dist-info/METADATA +272 -0
- blocklog-0.2.0.dist-info/RECORD +43 -0
- blocklog-0.2.0.dist-info/WHEEL +5 -0
- blocklog-0.2.0.dist-info/licenses/LICENSE +21 -0
- blocklog-0.2.0.dist-info/top_level.txt +1 -0
blocklog/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
blocklog — Infrastructure for AI Decision-Making
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Record every decision your AI agents make.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
import blocklog
|
|
10
|
+
|
|
11
|
+
blocklog.init(api_key="blk_...")
|
|
12
|
+
|
|
13
|
+
@blocklog.agent(name="stock-trader")
|
|
14
|
+
def run():
|
|
15
|
+
with blocklog.decision(type="BUY", asset="TSLA") as d:
|
|
16
|
+
price = fetch_price("TSLA")
|
|
17
|
+
d.record_input(price=price)
|
|
18
|
+
|
|
19
|
+
order = place_order("TSLA", qty=100)
|
|
20
|
+
d.record_output(order_id=order.id)
|
|
21
|
+
|
|
22
|
+
Public API
|
|
23
|
+
----------
|
|
24
|
+
``blocklog.init()`` — configure the SDK
|
|
25
|
+
``@blocklog.agent`` — trace an AI agent function or class
|
|
26
|
+
``@blocklog.tool`` — record a tool call with inputs/outputs
|
|
27
|
+
``blocklog.decision()`` — context manager for AI decision recording
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
# ── Tier 1: Getting Started ───────────────────────────────────────────────────
|
|
32
|
+
from blocklog._init_fn import init
|
|
33
|
+
from blocklog.decorators.agent import agent
|
|
34
|
+
from blocklog.decorators.tool import tool
|
|
35
|
+
from blocklog.managers.decision import decision, DecisionContext
|
|
36
|
+
|
|
37
|
+
# ── Tier 2: Governance & Investigation ────────────────────────────────────────
|
|
38
|
+
from blocklog import approval # noqa: E402
|
|
39
|
+
from blocklog.replay import replay # noqa: E402
|
|
40
|
+
|
|
41
|
+
__version__ = "0.2.0"
|
|
42
|
+
|
|
43
|
+
# Expose only the minimum concepts required to understand Blocklog.
|
|
44
|
+
# Advanced features (incident, verify, compliance, clients) are hidden
|
|
45
|
+
# from quickstart autocomplete but remain accessible via their submodules.
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Tier 1
|
|
48
|
+
"init",
|
|
49
|
+
"agent",
|
|
50
|
+
"tool",
|
|
51
|
+
"decision",
|
|
52
|
+
"DecisionContext",
|
|
53
|
+
# Tier 2
|
|
54
|
+
"approval",
|
|
55
|
+
"replay",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
def __getattr__(name: str):
|
|
59
|
+
# Backward compatibility for direct client access (hidden from autocomplete)
|
|
60
|
+
if name == "BlocklogClient":
|
|
61
|
+
from blocklog.client import BlocklogClient
|
|
62
|
+
return BlocklogClient
|
|
63
|
+
if name == "AsyncBlocklogClient":
|
|
64
|
+
from blocklog.async_client import AsyncBlocklogClient
|
|
65
|
+
return AsyncBlocklogClient
|
|
66
|
+
if name == "BlocklogConfig":
|
|
67
|
+
from blocklog.config import BlocklogConfig
|
|
68
|
+
return BlocklogConfig
|
|
69
|
+
if name == "agent_session":
|
|
70
|
+
from blocklog.context.managers import agent_session
|
|
71
|
+
return agent_session
|
|
72
|
+
if name == "get_context":
|
|
73
|
+
from blocklog.context.vars import get_context
|
|
74
|
+
return get_context
|
|
75
|
+
if name == "set_context":
|
|
76
|
+
from blocklog.context.vars import set_context
|
|
77
|
+
return set_context
|
|
78
|
+
raise AttributeError(f"module 'blocklog' has no attribute {name!r}")
|
blocklog/_global.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Optional, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from blocklog.client import BlocklogClient
|
|
5
|
+
|
|
6
|
+
_global_client: Optional["BlocklogClient"] = None
|
|
7
|
+
|
|
8
|
+
def get_client() -> "BlocklogClient":
|
|
9
|
+
if _global_client is None:
|
|
10
|
+
raise RuntimeError(
|
|
11
|
+
"Blocklog API key not configured.\n\n"
|
|
12
|
+
"Use:\n"
|
|
13
|
+
"from blocklog import init\n"
|
|
14
|
+
"init(api_key=\"YOUR_API_KEY\")"
|
|
15
|
+
)
|
|
16
|
+
return _global_client
|
|
17
|
+
|
|
18
|
+
def set_client(client: "BlocklogClient") -> None:
|
|
19
|
+
global _global_client
|
|
20
|
+
_global_client = client
|
blocklog/_init_fn.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
blocklog._init_fn
|
|
3
|
+
~~~~~~~~~~~~~~~~~
|
|
4
|
+
Implements the top-level ``blocklog.init()`` call.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
import blocklog
|
|
9
|
+
blocklog.init(api_key="blk_...")
|
|
10
|
+
|
|
11
|
+
# or via environment variable BLOCKLOG_API_KEY
|
|
12
|
+
blocklog.init()
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from blocklog.client import BlocklogClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def init(
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
*,
|
|
25
|
+
base_url: str | None = None,
|
|
26
|
+
signing_key: str | None = None,
|
|
27
|
+
timeout: float | None = None,
|
|
28
|
+
max_retries: int | None = None,
|
|
29
|
+
debug: bool = False,
|
|
30
|
+
) -> "BlocklogClient":
|
|
31
|
+
"""Initialise the Blocklog SDK.
|
|
32
|
+
|
|
33
|
+
Call once at application startup — typically right after your other
|
|
34
|
+
infrastructure initialisation (logging, config loading, etc.).
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
api_key:
|
|
39
|
+
Your Blocklog API key. Falls back to the ``BLOCKLOG_API_KEY``
|
|
40
|
+
environment variable when omitted.
|
|
41
|
+
base_url:
|
|
42
|
+
Override the default API base URL. Useful for self-hosted
|
|
43
|
+
deployments. Falls back to ``BLOCKLOG_BASE_URL``.
|
|
44
|
+
signing_key:
|
|
45
|
+
Ed25519 private key used to sign log payloads for tamper-evidence.
|
|
46
|
+
Falls back to ``BLOCKLOG_SDK_SIGNING_KEY``.
|
|
47
|
+
timeout:
|
|
48
|
+
Per-request timeout in seconds (default: 10).
|
|
49
|
+
max_retries:
|
|
50
|
+
Number of automatic retries on transient failures (default: 3).
|
|
51
|
+
debug:
|
|
52
|
+
When ``True``, logs every outbound request to stderr.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
BlocklogClient
|
|
57
|
+
The configured global client instance. You normally don't need
|
|
58
|
+
to store this — all module-level helpers (``decision``,
|
|
59
|
+
``approval``, ``incident``, ``replay``, ``verify``,
|
|
60
|
+
``compliance``) pick it up automatically.
|
|
61
|
+
|
|
62
|
+
Examples
|
|
63
|
+
--------
|
|
64
|
+
>>> import blocklog
|
|
65
|
+
>>> blocklog.init(api_key="blk_live_...")
|
|
66
|
+
|
|
67
|
+
>>> # Environment-variable driven (CI / production)
|
|
68
|
+
>>> blocklog.init()
|
|
69
|
+
"""
|
|
70
|
+
from blocklog._global import set_client
|
|
71
|
+
from blocklog.client import BlocklogClient
|
|
72
|
+
from blocklog.config import BlocklogConfig
|
|
73
|
+
|
|
74
|
+
overrides: dict = {}
|
|
75
|
+
if api_key is not None:
|
|
76
|
+
overrides["api_key"] = api_key
|
|
77
|
+
if base_url is not None:
|
|
78
|
+
overrides["base_url"] = base_url
|
|
79
|
+
if signing_key is not None:
|
|
80
|
+
overrides["signing_key"] = signing_key
|
|
81
|
+
if timeout is not None:
|
|
82
|
+
overrides["timeout"] = timeout
|
|
83
|
+
if max_retries is not None:
|
|
84
|
+
overrides["max_retries"] = max_retries
|
|
85
|
+
|
|
86
|
+
config = BlocklogConfig(**overrides)
|
|
87
|
+
|
|
88
|
+
if debug:
|
|
89
|
+
import logging
|
|
90
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
91
|
+
logging.getLogger("blocklog").setLevel(logging.DEBUG)
|
|
92
|
+
|
|
93
|
+
client = BlocklogClient(config)
|
|
94
|
+
set_client(client)
|
|
95
|
+
return client
|
blocklog/api/approval.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
blocklog.api.approval
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Layer 2 client for Human-in-the-Loop (HITL) approval workflows.
|
|
5
|
+
|
|
6
|
+
Available via ``client.approval.*``.
|
|
7
|
+
|
|
8
|
+
Backend endpoints
|
|
9
|
+
-----------------
|
|
10
|
+
- POST /api/v1/hitl/approve
|
|
11
|
+
- POST /api/v1/hitl/reject
|
|
12
|
+
- POST /api/v1/hitl/escalate
|
|
13
|
+
- GET /api/v1/hitl/overrides
|
|
14
|
+
- GET /api/v1/hitl/overrides/{id}
|
|
15
|
+
- GET /api/v1/hitl/audit-trail
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from blocklog.client import BlocklogClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApprovalClient:
|
|
26
|
+
"""Manage human approval workflows (HITL).
|
|
27
|
+
|
|
28
|
+
Accessed as ``client.approval``.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> client.approval.request(
|
|
33
|
+
... decision_id="dec_abc123",
|
|
34
|
+
... reason="Trade exceeds $500k threshold",
|
|
35
|
+
... reviewer="risk-team@fund.com",
|
|
36
|
+
... )
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, client: "BlocklogClient") -> None:
|
|
40
|
+
self._client = client
|
|
41
|
+
|
|
42
|
+
def request(
|
|
43
|
+
self,
|
|
44
|
+
decision_id: str | None = None,
|
|
45
|
+
*,
|
|
46
|
+
reason: str,
|
|
47
|
+
reviewer: str | None = None,
|
|
48
|
+
log_id: str | None = None,
|
|
49
|
+
metadata: dict[str, Any] | None = None,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
"""Request human approval for a decision or log entry.
|
|
52
|
+
|
|
53
|
+
This is a **non-blocking** operation. It registers the approval
|
|
54
|
+
request in Blocklog and triggers any configured webhooks or
|
|
55
|
+
notification rules. Execution continues immediately.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
decision_id:
|
|
60
|
+
UUID of the decision requiring approval.
|
|
61
|
+
reason:
|
|
62
|
+
Human-readable explanation of why approval is needed.
|
|
63
|
+
reviewer:
|
|
64
|
+
Email or identifier of the intended reviewer.
|
|
65
|
+
log_id:
|
|
66
|
+
UUID of the specific log entry, if approval is on a log
|
|
67
|
+
rather than a top-level decision.
|
|
68
|
+
metadata:
|
|
69
|
+
Extra context to attach to the approval request.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
dict
|
|
74
|
+
Backend response confirming the request was registered.
|
|
75
|
+
"""
|
|
76
|
+
payload: dict[str, Any] = {"reason": reason}
|
|
77
|
+
if decision_id is not None:
|
|
78
|
+
payload["decision_id"] = decision_id
|
|
79
|
+
if reviewer is not None:
|
|
80
|
+
payload["reviewer"] = reviewer
|
|
81
|
+
if log_id is not None:
|
|
82
|
+
payload["log_id"] = log_id
|
|
83
|
+
if metadata is not None:
|
|
84
|
+
payload["metadata"] = metadata
|
|
85
|
+
|
|
86
|
+
return self._client.retry.run(
|
|
87
|
+
lambda: self._client.transport.request("POST", "/hitl/approve", json=payload)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def reject(
|
|
91
|
+
self,
|
|
92
|
+
reviewer: str,
|
|
93
|
+
*,
|
|
94
|
+
reason: str,
|
|
95
|
+
decision_id: str | None = None,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""Record that a reviewer has rejected a decision.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
reviewer:
|
|
102
|
+
Identity of the person rejecting.
|
|
103
|
+
reason:
|
|
104
|
+
Explanation for the rejection.
|
|
105
|
+
decision_id:
|
|
106
|
+
Optional reference to the decision being rejected.
|
|
107
|
+
"""
|
|
108
|
+
payload: dict[str, Any] = {
|
|
109
|
+
"reviewer": reviewer,
|
|
110
|
+
"rejection_reason": reason,
|
|
111
|
+
}
|
|
112
|
+
if decision_id is not None:
|
|
113
|
+
payload["decision_id"] = decision_id
|
|
114
|
+
|
|
115
|
+
return self._client.retry.run(
|
|
116
|
+
lambda: self._client.transport.request("POST", "/hitl/reject", json=payload)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def escalate(
|
|
120
|
+
self,
|
|
121
|
+
from_reviewer: str,
|
|
122
|
+
to_reviewer: str,
|
|
123
|
+
*,
|
|
124
|
+
reason: str,
|
|
125
|
+
) -> dict[str, Any]:
|
|
126
|
+
"""Escalate an approval request to a different reviewer.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
from_reviewer:
|
|
131
|
+
Current reviewer escalating the decision.
|
|
132
|
+
to_reviewer:
|
|
133
|
+
Target reviewer who should take over.
|
|
134
|
+
reason:
|
|
135
|
+
Explanation for the escalation.
|
|
136
|
+
"""
|
|
137
|
+
return self._client.retry.run(
|
|
138
|
+
lambda: self._client.transport.request("POST", "/hitl/escalate", json={
|
|
139
|
+
"current_reviewer": from_reviewer,
|
|
140
|
+
"escalation_target": to_reviewer,
|
|
141
|
+
"escalation_reason": reason,
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def list_overrides(self) -> list[dict[str, Any]]:
|
|
146
|
+
"""Return all HITL override records for the company.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
list[dict]
|
|
151
|
+
List of override records (approvals that changed an outcome).
|
|
152
|
+
"""
|
|
153
|
+
return self._client.retry.run(
|
|
154
|
+
lambda: self._client.transport.request("GET", "/hitl/overrides")
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def get_override(self, override_id: int) -> dict[str, Any]:
|
|
158
|
+
"""Fetch a specific HITL override record.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
override_id:
|
|
163
|
+
Integer ID of the override.
|
|
164
|
+
"""
|
|
165
|
+
return self._client.retry.run(
|
|
166
|
+
lambda: self._client.transport.request("GET", f"/hitl/overrides/{override_id}")
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def audit_trail(self) -> list[dict[str, Any]]:
|
|
170
|
+
"""Return the full HITL audit trail for the company.
|
|
171
|
+
|
|
172
|
+
Includes all approve / reject / escalate events in reverse
|
|
173
|
+
chronological order.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
list[dict]
|
|
178
|
+
Ordered list of HITL audit log entries.
|
|
179
|
+
"""
|
|
180
|
+
return self._client.retry.run(
|
|
181
|
+
lambda: self._client.transport.request("GET", "/hitl/audit-trail")
|
|
182
|
+
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
blocklog.api.compliance
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Layer 2 client for compliance report generation.
|
|
5
|
+
|
|
6
|
+
Available via ``client.compliance.*``.
|
|
7
|
+
|
|
8
|
+
Backend endpoints
|
|
9
|
+
-----------------
|
|
10
|
+
- GET /api/v1/compliance/dashboard
|
|
11
|
+
- GET /api/v1/compliance/reports
|
|
12
|
+
- POST /api/v1/compliance/reports
|
|
13
|
+
- GET /api/v1/compliance/reports/{id}
|
|
14
|
+
- POST /api/v1/compliance/reports/{id}/share
|
|
15
|
+
- GET /api/v1/compliance/reports/{id}/export
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from blocklog.client import BlocklogClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ComplianceClient:
|
|
26
|
+
"""Generate and manage compliance reports.
|
|
27
|
+
|
|
28
|
+
Accessed as ``client.compliance``.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> report = client.compliance.generate(
|
|
33
|
+
... trace_id="trace-abc",
|
|
34
|
+
... framework="SOC2",
|
|
35
|
+
... )
|
|
36
|
+
>>> share_url = client.compliance.share(report["id"], expires_in=86400)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, client: "BlocklogClient") -> None:
|
|
40
|
+
self._client = client
|
|
41
|
+
|
|
42
|
+
def generate(
|
|
43
|
+
self,
|
|
44
|
+
trace_id: str | None = None,
|
|
45
|
+
*,
|
|
46
|
+
framework: str | None = None,
|
|
47
|
+
date_from: str | None = None,
|
|
48
|
+
date_to: str | None = None,
|
|
49
|
+
metadata: dict[str, Any] | None = None,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
"""Generate a compliance report.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
trace_id:
|
|
56
|
+
Scope the report to a specific trace. Omit to generate a
|
|
57
|
+
company-wide report.
|
|
58
|
+
framework:
|
|
59
|
+
Compliance framework (``"SOC2"``, ``"GDPR"``, ``"ISO27001"``…).
|
|
60
|
+
date_from:
|
|
61
|
+
ISO-8601 start of the reporting window.
|
|
62
|
+
date_to:
|
|
63
|
+
ISO-8601 end of the reporting window.
|
|
64
|
+
metadata:
|
|
65
|
+
Arbitrary extra data to embed in the report.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
dict
|
|
70
|
+
The generated report record.
|
|
71
|
+
"""
|
|
72
|
+
payload: dict[str, Any] = {}
|
|
73
|
+
if trace_id is not None:
|
|
74
|
+
payload["trace_id"] = trace_id
|
|
75
|
+
if framework is not None:
|
|
76
|
+
payload["framework"] = framework
|
|
77
|
+
if date_from is not None:
|
|
78
|
+
payload["date_from"] = date_from
|
|
79
|
+
if date_to is not None:
|
|
80
|
+
payload["date_to"] = date_to
|
|
81
|
+
if metadata is not None:
|
|
82
|
+
payload["metadata"] = metadata
|
|
83
|
+
|
|
84
|
+
return self._client.retry.run(
|
|
85
|
+
lambda: self._client.transport.request("POST", "/compliance/reports", json=payload)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get(self, report_id: str) -> dict[str, Any]:
|
|
89
|
+
"""Fetch a compliance report by ID."""
|
|
90
|
+
return self._client.retry.run(
|
|
91
|
+
lambda: self._client.transport.request("GET", f"/compliance/reports/{report_id}")
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def list(self) -> list[dict[str, Any]]:
|
|
95
|
+
"""List all compliance reports for the company."""
|
|
96
|
+
result = self._client.retry.run(
|
|
97
|
+
lambda: self._client.transport.request("GET", "/compliance/reports")
|
|
98
|
+
)
|
|
99
|
+
return result.get("items", result) if isinstance(result, dict) else result
|
|
100
|
+
|
|
101
|
+
def dashboard(self) -> dict[str, Any]:
|
|
102
|
+
"""Return the compliance dashboard summary."""
|
|
103
|
+
return self._client.retry.run(
|
|
104
|
+
lambda: self._client.transport.request("GET", "/compliance/dashboard")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def share(
|
|
108
|
+
self,
|
|
109
|
+
report_id: str,
|
|
110
|
+
*,
|
|
111
|
+
expires_in: int | None = None,
|
|
112
|
+
recipient_email: str | None = None,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Create a shareable link for a compliance report.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
report_id:
|
|
119
|
+
UUID of the report to share.
|
|
120
|
+
expires_in:
|
|
121
|
+
Seconds until the share link expires.
|
|
122
|
+
recipient_email:
|
|
123
|
+
Optional email of the recipient (for audit purposes).
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
dict
|
|
128
|
+
Response including ``share_url`` or ``token``.
|
|
129
|
+
"""
|
|
130
|
+
payload: dict[str, Any] = {}
|
|
131
|
+
if expires_in is not None:
|
|
132
|
+
payload["expires_in"] = expires_in
|
|
133
|
+
if recipient_email is not None:
|
|
134
|
+
payload["recipient_email"] = recipient_email
|
|
135
|
+
|
|
136
|
+
return self._client.retry.run(
|
|
137
|
+
lambda: self._client.transport.request("POST", f"/compliance/reports/{report_id}/share", json=payload)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def export(
|
|
141
|
+
self,
|
|
142
|
+
report_id: str,
|
|
143
|
+
*,
|
|
144
|
+
download: bool = False,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""Export a compliance report as JSON.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
report_id:
|
|
151
|
+
UUID of the report to export.
|
|
152
|
+
download:
|
|
153
|
+
When ``True``, request the binary download stream (returned
|
|
154
|
+
as raw content from the backend).
|
|
155
|
+
"""
|
|
156
|
+
return self._client.retry.run(
|
|
157
|
+
lambda: self._client.transport.request(
|
|
158
|
+
"GET",
|
|
159
|
+
f"/compliance/reports/{report_id}/export",
|
|
160
|
+
params={"download": str(download).lower()},
|
|
161
|
+
)
|
|
162
|
+
)
|