spanforge 1.0.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 (174) hide show
  1. spanforge/__init__.py +815 -0
  2. spanforge/_ansi.py +93 -0
  3. spanforge/_batch_exporter.py +409 -0
  4. spanforge/_cli.py +2094 -0
  5. spanforge/_cli_audit.py +639 -0
  6. spanforge/_cli_compliance.py +711 -0
  7. spanforge/_cli_cost.py +243 -0
  8. spanforge/_cli_ops.py +791 -0
  9. spanforge/_cli_phase11.py +356 -0
  10. spanforge/_hooks.py +337 -0
  11. spanforge/_server.py +1708 -0
  12. spanforge/_span.py +1036 -0
  13. spanforge/_store.py +288 -0
  14. spanforge/_stream.py +664 -0
  15. spanforge/_trace.py +335 -0
  16. spanforge/_tracer.py +254 -0
  17. spanforge/actor.py +141 -0
  18. spanforge/alerts.py +469 -0
  19. spanforge/auto.py +464 -0
  20. spanforge/baseline.py +335 -0
  21. spanforge/cache.py +635 -0
  22. spanforge/compliance.py +325 -0
  23. spanforge/config.py +532 -0
  24. spanforge/consent.py +228 -0
  25. spanforge/consumer.py +377 -0
  26. spanforge/core/__init__.py +5 -0
  27. spanforge/core/compliance_mapping.py +1254 -0
  28. spanforge/cost.py +600 -0
  29. spanforge/debug.py +548 -0
  30. spanforge/deprecations.py +205 -0
  31. spanforge/drift.py +482 -0
  32. spanforge/egress.py +58 -0
  33. spanforge/eval.py +648 -0
  34. spanforge/event.py +1064 -0
  35. spanforge/exceptions.py +240 -0
  36. spanforge/explain.py +178 -0
  37. spanforge/export/__init__.py +69 -0
  38. spanforge/export/append_only.py +337 -0
  39. spanforge/export/cloud.py +357 -0
  40. spanforge/export/datadog.py +497 -0
  41. spanforge/export/grafana.py +320 -0
  42. spanforge/export/jsonl.py +195 -0
  43. spanforge/export/openinference.py +158 -0
  44. spanforge/export/otel_bridge.py +294 -0
  45. spanforge/export/otlp.py +811 -0
  46. spanforge/export/otlp_bridge.py +233 -0
  47. spanforge/export/redis_backend.py +282 -0
  48. spanforge/export/siem_schema.py +98 -0
  49. spanforge/export/siem_splunk.py +264 -0
  50. spanforge/export/siem_syslog.py +212 -0
  51. spanforge/export/webhook.py +299 -0
  52. spanforge/exporters/__init__.py +30 -0
  53. spanforge/exporters/console.py +271 -0
  54. spanforge/exporters/jsonl.py +144 -0
  55. spanforge/exporters/sqlite.py +142 -0
  56. spanforge/gate.py +1150 -0
  57. spanforge/governance.py +181 -0
  58. spanforge/hitl.py +295 -0
  59. spanforge/http.py +187 -0
  60. spanforge/inspect.py +427 -0
  61. spanforge/integrations/__init__.py +45 -0
  62. spanforge/integrations/_pricing.py +280 -0
  63. spanforge/integrations/anthropic.py +388 -0
  64. spanforge/integrations/azure_openai.py +133 -0
  65. spanforge/integrations/bedrock.py +292 -0
  66. spanforge/integrations/crewai.py +251 -0
  67. spanforge/integrations/gemini.py +351 -0
  68. spanforge/integrations/groq.py +442 -0
  69. spanforge/integrations/langchain.py +349 -0
  70. spanforge/integrations/langgraph.py +306 -0
  71. spanforge/integrations/llamaindex.py +373 -0
  72. spanforge/integrations/ollama.py +287 -0
  73. spanforge/integrations/openai.py +368 -0
  74. spanforge/integrations/together.py +483 -0
  75. spanforge/io.py +214 -0
  76. spanforge/lint.py +322 -0
  77. spanforge/metrics.py +417 -0
  78. spanforge/metrics_export.py +343 -0
  79. spanforge/migrate.py +402 -0
  80. spanforge/model_registry.py +278 -0
  81. spanforge/models.py +389 -0
  82. spanforge/namespaces/__init__.py +254 -0
  83. spanforge/namespaces/audit.py +256 -0
  84. spanforge/namespaces/cache.py +237 -0
  85. spanforge/namespaces/chain.py +77 -0
  86. spanforge/namespaces/confidence.py +72 -0
  87. spanforge/namespaces/consent.py +92 -0
  88. spanforge/namespaces/cost.py +179 -0
  89. spanforge/namespaces/decision.py +143 -0
  90. spanforge/namespaces/diff.py +157 -0
  91. spanforge/namespaces/drift.py +80 -0
  92. spanforge/namespaces/eval_.py +251 -0
  93. spanforge/namespaces/feedback.py +241 -0
  94. spanforge/namespaces/fence.py +193 -0
  95. spanforge/namespaces/guard.py +105 -0
  96. spanforge/namespaces/hitl.py +91 -0
  97. spanforge/namespaces/latency.py +72 -0
  98. spanforge/namespaces/prompt.py +190 -0
  99. spanforge/namespaces/redact.py +173 -0
  100. spanforge/namespaces/retrieval.py +379 -0
  101. spanforge/namespaces/runtime_governance.py +494 -0
  102. spanforge/namespaces/template.py +208 -0
  103. spanforge/namespaces/tool_call.py +77 -0
  104. spanforge/namespaces/trace.py +1029 -0
  105. spanforge/normalizer.py +171 -0
  106. spanforge/plugins.py +82 -0
  107. spanforge/presidio_backend.py +349 -0
  108. spanforge/processor.py +258 -0
  109. spanforge/prompt_registry.py +418 -0
  110. spanforge/py.typed +0 -0
  111. spanforge/redact.py +914 -0
  112. spanforge/regression.py +192 -0
  113. spanforge/runtime_policy.py +159 -0
  114. spanforge/sampling.py +511 -0
  115. spanforge/schema.py +183 -0
  116. spanforge/schemas/v1.0/schema.json +170 -0
  117. spanforge/schemas/v2.0/schema.json +536 -0
  118. spanforge/sdk/__init__.py +625 -0
  119. spanforge/sdk/_base.py +584 -0
  120. spanforge/sdk/_base.pyi +71 -0
  121. spanforge/sdk/_exceptions.py +1096 -0
  122. spanforge/sdk/_types.py +2184 -0
  123. spanforge/sdk/alert.py +1514 -0
  124. spanforge/sdk/alert.pyi +56 -0
  125. spanforge/sdk/audit.py +1196 -0
  126. spanforge/sdk/audit.pyi +67 -0
  127. spanforge/sdk/cec.py +1215 -0
  128. spanforge/sdk/cec.pyi +37 -0
  129. spanforge/sdk/config.py +641 -0
  130. spanforge/sdk/config.pyi +55 -0
  131. spanforge/sdk/enterprise.py +714 -0
  132. spanforge/sdk/enterprise.pyi +79 -0
  133. spanforge/sdk/explain.py +170 -0
  134. spanforge/sdk/fallback.py +432 -0
  135. spanforge/sdk/feedback.py +351 -0
  136. spanforge/sdk/gate.py +874 -0
  137. spanforge/sdk/gate.pyi +51 -0
  138. spanforge/sdk/identity.py +2114 -0
  139. spanforge/sdk/identity.pyi +47 -0
  140. spanforge/sdk/lineage.py +175 -0
  141. spanforge/sdk/observe.py +1065 -0
  142. spanforge/sdk/observe.pyi +50 -0
  143. spanforge/sdk/operator.py +338 -0
  144. spanforge/sdk/pii.py +1473 -0
  145. spanforge/sdk/pii.pyi +119 -0
  146. spanforge/sdk/pipelines.py +458 -0
  147. spanforge/sdk/pipelines.pyi +39 -0
  148. spanforge/sdk/policy.py +930 -0
  149. spanforge/sdk/rag.py +594 -0
  150. spanforge/sdk/rbac.py +280 -0
  151. spanforge/sdk/registry.py +430 -0
  152. spanforge/sdk/registry.pyi +46 -0
  153. spanforge/sdk/scope.py +279 -0
  154. spanforge/sdk/secrets.py +293 -0
  155. spanforge/sdk/secrets.pyi +25 -0
  156. spanforge/sdk/security.py +560 -0
  157. spanforge/sdk/security.pyi +57 -0
  158. spanforge/sdk/trust.py +472 -0
  159. spanforge/sdk/trust.pyi +41 -0
  160. spanforge/secrets.py +799 -0
  161. spanforge/signing.py +1179 -0
  162. spanforge/stats.py +100 -0
  163. spanforge/stream.py +560 -0
  164. spanforge/testing.py +378 -0
  165. spanforge/testing_mocks.py +1052 -0
  166. spanforge/trace.py +199 -0
  167. spanforge/types.py +696 -0
  168. spanforge/ulid.py +300 -0
  169. spanforge/validate.py +379 -0
  170. spanforge-1.0.0.dist-info/METADATA +1509 -0
  171. spanforge-1.0.0.dist-info/RECORD +174 -0
  172. spanforge-1.0.0.dist-info/WHEEL +4 -0
  173. spanforge-1.0.0.dist-info/entry_points.txt +5 -0
  174. spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
@@ -0,0 +1,1052 @@
1
+ """spanforge.testing_mocks — Mock service clients for testing (DX-003).
2
+
3
+ Provides drop-in mock replacements for every SpanForge SDK service client.
4
+ All mocks operate purely in-memory with no network calls, making them ideal
5
+ for unit tests.
6
+
7
+ Quick start::
8
+
9
+ from spanforge.testing_mocks import mock_all_services
10
+
11
+ with mock_all_services():
12
+ from spanforge.sdk import sf_pii, sf_audit
13
+ result = sf_pii.scan({"msg": "Call 555-867-5309"})
14
+ assert result.clean # mock returns clean by default
15
+
16
+ Each mock class records all calls for assertion. Use ``.calls`` to inspect
17
+ what was called, or ``.configure_response()`` to override default returns.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import contextlib
23
+ import threading
24
+ from dataclasses import dataclass
25
+ from datetime import datetime, timezone
26
+ from typing import TYPE_CHECKING, Any, TypeVar
27
+
28
+ from spanforge.sdk._types import (
29
+ AirGapConfig,
30
+ AlertRecord,
31
+ AlertStatusInfo,
32
+ Annotation,
33
+ APIKeyBundle,
34
+ AuditAppendResult,
35
+ AuditStatusInfo,
36
+ BundleResult,
37
+ BundleVerificationResult,
38
+ CECStatusInfo,
39
+ DPADocument,
40
+ DSARExport,
41
+ EncryptionConfig,
42
+ EnterpriseStatusInfo,
43
+ ErasureReceipt,
44
+ ExportResult,
45
+ GateArtifact,
46
+ GateEvaluationResult,
47
+ GateStatusInfo,
48
+ HealthEndpointResult,
49
+ IsolationScope,
50
+ JWTClaims,
51
+ MagicLinkResult,
52
+ ObserveStatusInfo,
53
+ PIIAnonymisedResult,
54
+ PIIHeatMapEntry,
55
+ PIIPipelineResult,
56
+ PIIStatusInfo,
57
+ PIITextScanResult,
58
+ PRRIResult,
59
+ PublishResult,
60
+ RateLimitInfo,
61
+ SafeHarborResult,
62
+ SecretStr,
63
+ SecurityAuditResult,
64
+ SecurityScanResult,
65
+ SFPIIAnonymizeResult,
66
+ SFPIIRedactResult,
67
+ SFPIIScanResult,
68
+ SignedRecord,
69
+ TenantConfig,
70
+ ThreatModelEntry,
71
+ TokenIntrospectionResult,
72
+ TOTPEnrollResult,
73
+ TrustBadgeResult,
74
+ TrustDimension,
75
+ TrustDimensionWeights,
76
+ TrustGateResult,
77
+ TrustHistoryEntry,
78
+ TrustScorecardResponse,
79
+ TrustStatusInfo,
80
+ )
81
+
82
+ if TYPE_CHECKING:
83
+ from collections.abc import Generator
84
+
85
+ __all__ = [
86
+ "MockSFAlert",
87
+ "MockSFAudit",
88
+ "MockSFCEC",
89
+ "MockSFEnterprise",
90
+ "MockSFGate",
91
+ "MockSFIdentity",
92
+ "MockSFObserve",
93
+ "MockSFPII",
94
+ "MockSFSecrets",
95
+ "MockSFSecurity",
96
+ "MockSFTrust",
97
+ "mock_all_services",
98
+ ]
99
+
100
+ _T = TypeVar("_T")
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Call recorder mixin
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ @dataclass
109
+ class _MockCall:
110
+ """Record of a single method invocation."""
111
+
112
+ method: str
113
+ args: tuple[Any, ...]
114
+ kwargs: dict[str, Any]
115
+
116
+
117
+ class _MockBase:
118
+ """Base class for all mock service clients.
119
+
120
+ Provides call recording and response overriding.
121
+ """
122
+
123
+ def __init__(self) -> None:
124
+ self.calls: list[_MockCall] = []
125
+ self._responses: dict[str, Any] = {}
126
+ self._lock = threading.Lock()
127
+
128
+ def _record(self, method: str, *args: Any, **kwargs: Any) -> None:
129
+ with self._lock:
130
+ self.calls.append(_MockCall(method=method, args=args, kwargs=kwargs))
131
+
132
+ def configure_response(self, method: str, response: Any) -> None:
133
+ """Set a custom return value for *method*."""
134
+ self._responses[method] = response
135
+
136
+ def _get_response(self, method: str, default: _T) -> _T:
137
+ response = self._responses.get(method, default)
138
+ return response if isinstance(response, type(default)) else default
139
+
140
+ def reset(self) -> None:
141
+ """Clear all recorded calls and configured responses."""
142
+ with self._lock:
143
+ self.calls.clear()
144
+ self._responses.clear()
145
+
146
+ def assert_called(self, method: str) -> None:
147
+ """Assert that *method* was called at least once."""
148
+ if not any(c.method == method for c in self.calls):
149
+ raise AssertionError(f"{type(self).__name__}.{method}() was never called")
150
+
151
+ def assert_not_called(self, method: str) -> None:
152
+ """Assert that *method* was never called."""
153
+ if any(c.method == method for c in self.calls):
154
+ raise AssertionError(
155
+ f"{type(self).__name__}.{method}() was called "
156
+ f"{sum(1 for c in self.calls if c.method == method)} time(s)"
157
+ )
158
+
159
+ def call_count(self, method: str) -> int:
160
+ """Return the number of times *method* was called."""
161
+ return sum(1 for c in self.calls if c.method == method)
162
+
163
+
164
+ def _now_iso() -> str:
165
+ return datetime.now(timezone.utc).isoformat()
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # MockSFIdentity
170
+ # ---------------------------------------------------------------------------
171
+
172
+
173
+ class MockSFIdentity(_MockBase):
174
+ """Mock replacement for :class:`~spanforge.sdk.identity.SFIdentityClient`."""
175
+
176
+ def issue_api_key(self, **kwargs: Any) -> APIKeyBundle:
177
+ self._record("issue_api_key", **kwargs)
178
+ return self._get_response(
179
+ "issue_api_key",
180
+ APIKeyBundle(
181
+ api_key=SecretStr("sf_test_mock_key_000000000000"),
182
+ key_id="mock-key-id",
183
+ jwt="mock.jwt.token",
184
+ expires_at=datetime(2099, 1, 1, tzinfo=timezone.utc),
185
+ scopes=kwargs.get("scopes", []),
186
+ ),
187
+ )
188
+
189
+ def refresh_token(self) -> str:
190
+ self._record("refresh_token")
191
+ return self._get_response("refresh_token", "mock.refreshed.jwt")
192
+
193
+ def create_session(self, api_key: str) -> str:
194
+ self._record("create_session", api_key)
195
+ return self._get_response("create_session", "mock.session.jwt")
196
+
197
+ def verify_token(self, jwt: str) -> JWTClaims:
198
+ self._record("verify_token", jwt)
199
+ return self._get_response(
200
+ "verify_token",
201
+ JWTClaims(
202
+ subject="mock-subject",
203
+ scopes=["*"],
204
+ project_id="mock-project",
205
+ expires_at=datetime(2099, 1, 1, tzinfo=timezone.utc),
206
+ issued_at=datetime.now(timezone.utc),
207
+ jti="mock-jti",
208
+ ),
209
+ )
210
+
211
+ def introspect(self, token: str) -> TokenIntrospectionResult:
212
+ self._record("introspect", token)
213
+ return self._get_response(
214
+ "introspect", TokenIntrospectionResult(active=True, scope="*", sub="mock-subject")
215
+ )
216
+
217
+ def rotate_key(self, key_id: str) -> APIKeyBundle:
218
+ self._record("rotate_key", key_id)
219
+ return self.issue_api_key()
220
+
221
+ def revoke_key(self, key_id: str) -> None:
222
+ self._record("revoke_key", key_id)
223
+
224
+ def issue_magic_link(self, email: str) -> MagicLinkResult:
225
+ self._record("issue_magic_link", email)
226
+ return self._get_response(
227
+ "issue_magic_link",
228
+ MagicLinkResult(
229
+ link_id="mock-magic-link", expires_at=datetime(2099, 1, 1, tzinfo=timezone.utc)
230
+ ),
231
+ )
232
+
233
+ def enroll_totp(self, key_id: str) -> TOTPEnrollResult:
234
+ self._record("enroll_totp", key_id)
235
+ return self._get_response(
236
+ "enroll_totp",
237
+ TOTPEnrollResult(
238
+ secret_base32=SecretStr("MOCKSECRET"),
239
+ qr_uri="otpauth://totp/mock",
240
+ backup_codes=["000000"],
241
+ ),
242
+ )
243
+
244
+ def verify_backup_code(self, key_id: str, code: str) -> bool:
245
+ self._record("verify_backup_code", key_id, code)
246
+ return self._get_response("verify_backup_code", True)
247
+
248
+ def check_rate_limit(self, key_id: str) -> RateLimitInfo:
249
+ self._record("check_rate_limit", key_id)
250
+ return self._get_response(
251
+ "check_rate_limit",
252
+ RateLimitInfo(limit=600, remaining=599, reset_at=datetime.now(timezone.utc)),
253
+ )
254
+
255
+ def record_request(self, key_id: str) -> bool:
256
+ self._record("record_request", key_id)
257
+ return self._get_response("record_request", True)
258
+
259
+ def require_scope(self, claims: Any, scope: str) -> None:
260
+ self._record("require_scope", claims, scope)
261
+
262
+ def get_jwks(self) -> dict[str, Any]:
263
+ self._record("get_jwks")
264
+ return self._get_response("get_jwks", {"keys": []})
265
+
266
+ def check_ip_allowlist(self, key_id: str, ip: str) -> None:
267
+ self._record("check_ip_allowlist", key_id, ip)
268
+
269
+ def saml_metadata(self) -> str:
270
+ self._record("saml_metadata")
271
+ return self._get_response("saml_metadata", "<mock-saml/>")
272
+
273
+ def saml_acs(self, saml_response: str) -> dict[str, Any]: # F-03
274
+ self._record("saml_acs", saml_response)
275
+ return self._get_response(
276
+ "saml_acs", {"subject": "mock-subject", "session": "mock-session"}
277
+ )
278
+
279
+ def validate_api_key(self, jwt: str) -> Any: # F-02 alias
280
+ return self.verify_token(jwt)
281
+
282
+ def get_status(self) -> dict[str, Any]: # F-02
283
+ self._record("get_status")
284
+ return self._get_response(
285
+ "get_status", {"status": "ok", "mode": "local", "keys_issued": 0, "active_sessions": 0}
286
+ )
287
+
288
+ def set_mfa_policy(self, project_id: str, mfa_required: bool) -> None:
289
+ self._record("set_mfa_policy", project_id, mfa_required)
290
+
291
+ def get_mfa_policy(self, project_id: str) -> bool:
292
+ self._record("get_mfa_policy", project_id)
293
+ return self._get_response("get_mfa_policy", False)
294
+
295
+ def set_key_tier(self, key_id: str, tier: str) -> None:
296
+ self._record("set_key_tier", key_id, tier)
297
+
298
+ def consume_quota(self, key_id: str) -> bool:
299
+ self._record("consume_quota", key_id)
300
+ return self._get_response("consume_quota", True)
301
+
302
+ def get_quota_usage(self, key_id: str) -> dict[str, Any]:
303
+ self._record("get_quota_usage", key_id)
304
+ return self._get_response("get_quota_usage", {"used": 0, "limit": 1000})
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # MockSFPII
309
+ # ---------------------------------------------------------------------------
310
+
311
+
312
+ class MockSFPII(_MockBase):
313
+ """Mock replacement for :class:`~spanforge.sdk.pii.SFPIIClient`."""
314
+
315
+ def scan(self, payload: dict[str, Any], **kwargs: Any) -> SFPIIScanResult:
316
+ self._record("scan", payload, **kwargs)
317
+ return self._get_response("scan", SFPIIScanResult(hits=[], scanned=1))
318
+
319
+ def redact(self, event: Any, **kwargs: Any) -> SFPIIRedactResult:
320
+ self._record("redact", event, **kwargs)
321
+ return self._get_response(
322
+ "redact",
323
+ SFPIIRedactResult(
324
+ event=event, redaction_count=0, redacted_at=_now_iso(), redacted_by="mock"
325
+ ),
326
+ )
327
+
328
+ def contains_pii(self, event: Any, **kwargs: Any) -> bool:
329
+ self._record("contains_pii", event, **kwargs)
330
+ return self._get_response("contains_pii", False)
331
+
332
+ def assert_redacted(self, event: Any, **kwargs: Any) -> None:
333
+ self._record("assert_redacted", event, **kwargs)
334
+
335
+ def anonymize(self, text: str, **kwargs: Any) -> SFPIIAnonymizeResult:
336
+ self._record("anonymize", text, **kwargs)
337
+ return self._get_response(
338
+ "anonymize", SFPIIAnonymizeResult(text=text, replacements=0, pii_types_found=[])
339
+ )
340
+
341
+ def scan_text(self, text: str, **kwargs: Any) -> PIITextScanResult:
342
+ self._record("scan_text", text, **kwargs)
343
+ return self._get_response(
344
+ "scan_text", PIITextScanResult(entities=[], redacted_text=text, detected=False)
345
+ )
346
+
347
+ def anonymise(self, payload: dict[str, Any], **kwargs: Any) -> PIIAnonymisedResult:
348
+ self._record("anonymise", payload, **kwargs)
349
+ return self._get_response(
350
+ "anonymise", PIIAnonymisedResult(clean_payload=payload, redaction_manifest=[])
351
+ )
352
+
353
+ def scan_batch(self, texts: list[str], **kwargs: Any) -> list[PIITextScanResult]:
354
+ self._record("scan_batch", texts, **kwargs)
355
+ return self._get_response(
356
+ "scan_batch",
357
+ [PIITextScanResult(entities=[], redacted_text=t, detected=False) for t in texts],
358
+ )
359
+
360
+ def apply_pipeline_action(self, text: str, **kwargs: Any) -> PIIPipelineResult:
361
+ self._record("apply_pipeline_action", text, **kwargs)
362
+ return self._get_response(
363
+ "apply_pipeline_action",
364
+ PIIPipelineResult(
365
+ text=text,
366
+ action="flag",
367
+ detected=False,
368
+ entity_types=[],
369
+ low_confidence_hits=[],
370
+ redacted_text=text,
371
+ blocked=False,
372
+ ),
373
+ )
374
+
375
+ def get_status(self) -> PIIStatusInfo:
376
+ self._record("get_status")
377
+ return self._get_response(
378
+ "get_status",
379
+ PIIStatusInfo(
380
+ status="ok", presidio_available=False, entity_types_loaded=[], last_scan_at=None
381
+ ),
382
+ )
383
+
384
+ def erase_subject(self, subject_id: str, project_id: str) -> ErasureReceipt:
385
+ self._record("erase_subject", subject_id, project_id)
386
+ return self._get_response(
387
+ "erase_subject",
388
+ ErasureReceipt(
389
+ subject_id=subject_id,
390
+ project_id=project_id,
391
+ records_erased=0,
392
+ erasure_id="mock-erasure",
393
+ erased_at=_now_iso(),
394
+ exceptions=[],
395
+ ),
396
+ )
397
+
398
+ def export_subject_data(self, subject_id: str, project_id: str) -> DSARExport:
399
+ self._record("export_subject_data", subject_id, project_id)
400
+ return self._get_response(
401
+ "export_subject_data",
402
+ DSARExport(
403
+ subject_id=subject_id,
404
+ project_id=project_id,
405
+ event_count=0,
406
+ export_id="mock-export",
407
+ exported_at=_now_iso(),
408
+ events=[],
409
+ ),
410
+ )
411
+
412
+ def safe_harbor_deidentify(self, text: str) -> SafeHarborResult:
413
+ self._record("safe_harbor_deidentify", text)
414
+ return self._get_response(
415
+ "safe_harbor_deidentify",
416
+ SafeHarborResult(text=text, replacements=0, phi_types_found=[]),
417
+ )
418
+
419
+ def get_pii_stats(self, project_id: str, **kwargs: Any) -> list[PIIHeatMapEntry]:
420
+ self._record("get_pii_stats", project_id, **kwargs)
421
+ return self._get_response("get_pii_stats", [])
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # MockSFSecrets
426
+ # ---------------------------------------------------------------------------
427
+
428
+
429
+ class MockSFSecrets(_MockBase):
430
+ """Mock replacement for :class:`~spanforge.sdk.secrets.SFSecretsClient`."""
431
+
432
+ def scan(self, text: str, **kwargs: Any) -> Any:
433
+ self._record("scan", text, **kwargs)
434
+ return self._get_response("scan", None)
435
+
436
+ def scan_batch(self, texts: list[str], **kwargs: Any) -> list[Any]:
437
+ self._record("scan_batch", texts, **kwargs)
438
+ return self._get_response("scan_batch", [])
439
+
440
+ def get_status(self) -> dict[str, Any]:
441
+ self._record("get_status")
442
+ return self._get_response("get_status", {"status": "ok", "patterns_loaded": 0})
443
+
444
+
445
+ # ---------------------------------------------------------------------------
446
+ # MockSFAudit
447
+ # ---------------------------------------------------------------------------
448
+
449
+
450
+ class MockSFAudit(_MockBase):
451
+ """Mock replacement for :class:`~spanforge.sdk.audit.SFAuditClient`."""
452
+
453
+ def append(self, record: dict[str, Any], schema_key: str, **kwargs: Any) -> AuditAppendResult:
454
+ self._record("append", record, schema_key, **kwargs)
455
+ return self._get_response(
456
+ "append",
457
+ AuditAppendResult(
458
+ record_id="mock-record-id",
459
+ chain_position=0,
460
+ timestamp=_now_iso(),
461
+ hmac="mock-hmac",
462
+ schema_key=schema_key,
463
+ backend="mock",
464
+ ),
465
+ )
466
+
467
+ def sign(self, record: dict[str, Any]) -> SignedRecord:
468
+ self._record("sign", record)
469
+ return self._get_response(
470
+ "sign",
471
+ SignedRecord(
472
+ record=record,
473
+ record_id="mock-id",
474
+ checksum="mock-cksum",
475
+ signature="mock-sig",
476
+ timestamp=_now_iso(),
477
+ ),
478
+ )
479
+
480
+ def verify_chain(self, records: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]:
481
+ self._record("verify_chain", records, **kwargs)
482
+ return self._get_response("verify_chain", {"valid": True, "gaps": []})
483
+
484
+ def export(self, schema_key: str | None = None, **kwargs: Any) -> list[dict[str, Any]]:
485
+ self._record("export", schema_key, **kwargs)
486
+ return self._get_response("export", [])
487
+
488
+ def get_trust_scorecard(self, project_id: str | None = None, **kwargs: Any) -> Any:
489
+ self._record("get_trust_scorecard", project_id, **kwargs)
490
+ return self._get_response("get_trust_scorecard", None)
491
+
492
+ def get_status(self) -> AuditStatusInfo:
493
+ self._record("get_status")
494
+ return self._get_response(
495
+ "get_status",
496
+ AuditStatusInfo(
497
+ status="ok",
498
+ backend="mock",
499
+ byos_enabled=False,
500
+ record_count=0,
501
+ last_append_at=None,
502
+ schema_count=0,
503
+ index_healthy=True,
504
+ retention_years=7,
505
+ ),
506
+ )
507
+
508
+ def close(self) -> None:
509
+ self._record("close")
510
+
511
+
512
+ # ---------------------------------------------------------------------------
513
+ # MockSFObserve
514
+ # ---------------------------------------------------------------------------
515
+
516
+
517
+ class MockSFObserve(_MockBase):
518
+ """Mock replacement for :class:`~spanforge.sdk.observe.SFObserveClient`."""
519
+
520
+ def export_spans(self, spans: list[dict[str, Any]], **kwargs: Any) -> ExportResult:
521
+ self._record("export_spans", spans, **kwargs)
522
+ return self._get_response(
523
+ "export_spans",
524
+ ExportResult(
525
+ exported_count=len(spans), failed_count=0, backend="mock", exported_at=_now_iso()
526
+ ),
527
+ )
528
+
529
+ def emit_span(self, name: str, attributes: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
530
+ self._record("emit_span", name, attributes, **kwargs)
531
+ return self._get_response("emit_span", {"span_id": "mock-span-id"})
532
+
533
+ def add_annotation(self, event_type: str, payload: dict[str, Any], **kwargs: Any) -> str:
534
+ self._record("add_annotation", event_type, payload, **kwargs)
535
+ return self._get_response("add_annotation", "mock-annotation-id")
536
+
537
+ def get_annotations(
538
+ self, event_type: str, from_dt: str, to_dt: str, **kwargs: Any
539
+ ) -> list[Annotation]:
540
+ self._record("get_annotations", event_type, from_dt, to_dt, **kwargs)
541
+ return self._get_response("get_annotations", [])
542
+
543
+ @property
544
+ def healthy(self) -> bool:
545
+ return True
546
+
547
+ @property
548
+ def last_export_at(self) -> str | None:
549
+ return None
550
+
551
+ def get_status(self) -> ObserveStatusInfo:
552
+ self._record("get_status")
553
+ return self._get_response(
554
+ "get_status",
555
+ ObserveStatusInfo(
556
+ status="ok",
557
+ backend="mock",
558
+ sampler_strategy="always_on",
559
+ span_count=0,
560
+ annotation_count=0,
561
+ export_count=0,
562
+ last_export_at=None,
563
+ healthy=True,
564
+ ),
565
+ )
566
+
567
+
568
+ # ---------------------------------------------------------------------------
569
+ # MockSFGate
570
+ # ---------------------------------------------------------------------------
571
+
572
+
573
+ class MockSFGate(_MockBase):
574
+ """Mock replacement for :class:`~spanforge.sdk.gate.SFGateClient`."""
575
+
576
+ def evaluate(
577
+ self, gate_id: str, payload: dict[str, Any], **kwargs: Any
578
+ ) -> GateEvaluationResult:
579
+ self._record("evaluate", gate_id, payload, **kwargs)
580
+ return self._get_response(
581
+ "evaluate",
582
+ GateEvaluationResult(
583
+ gate_id=gate_id, verdict="PASS", metrics={}, artifact_url="", duration_ms=1
584
+ ),
585
+ )
586
+
587
+ def run_trust_gate(self, project_id: str, **kwargs: Any) -> TrustGateResult:
588
+ self._record("run_trust_gate", project_id, **kwargs)
589
+ return self._get_response(
590
+ "run_trust_gate",
591
+ TrustGateResult(
592
+ gate_id="mock-trust-gate",
593
+ verdict="PASS",
594
+ hri_critical_rate=0.0,
595
+ hri_critical_threshold=0.05,
596
+ pii_detected=False,
597
+ pii_detections_24h=0,
598
+ secrets_detected=False,
599
+ secrets_detections_24h=0,
600
+ failures=[],
601
+ timestamp=_now_iso(),
602
+ pipeline_id="",
603
+ project_id=project_id,
604
+ pass_=True,
605
+ ),
606
+ )
607
+
608
+ def evaluate_prri(self, project_id: str, **kwargs: Any) -> PRRIResult:
609
+ self._record("evaluate_prri", project_id, **kwargs)
610
+ return self._get_response(
611
+ "evaluate_prri",
612
+ PRRIResult(
613
+ gate_id="gate5_governance",
614
+ prri_score=95,
615
+ verdict="GREEN",
616
+ dimension_breakdown={},
617
+ framework="soc2",
618
+ policy_file="",
619
+ timestamp=_now_iso(),
620
+ allow=True,
621
+ project_id=project_id,
622
+ ),
623
+ )
624
+
625
+ def list_artifacts(self, gate_id: str | None = None, **kwargs: Any) -> list[GateArtifact]:
626
+ self._record("list_artifacts", gate_id, **kwargs)
627
+ return self._get_response("list_artifacts", [])
628
+
629
+ def get_status(self) -> GateStatusInfo:
630
+ self._record("get_status")
631
+ return self._get_response(
632
+ "get_status",
633
+ GateStatusInfo(
634
+ status="ok",
635
+ evaluate_count=0,
636
+ trust_gate_count=0,
637
+ last_evaluate_at=None,
638
+ artifact_count=0,
639
+ artifact_dir="",
640
+ open_circuit_breakers=[],
641
+ ),
642
+ )
643
+
644
+
645
+ # ---------------------------------------------------------------------------
646
+ # MockSFCEC
647
+ # ---------------------------------------------------------------------------
648
+
649
+
650
+ class MockSFCEC(_MockBase):
651
+ """Mock replacement for :class:`~spanforge.sdk.cec.SFCECClient`."""
652
+
653
+ def build_bundle(
654
+ self, project_id: str, date_range: tuple[str, str], **kwargs: Any
655
+ ) -> BundleResult:
656
+ self._record("build_bundle", project_id, date_range, **kwargs)
657
+ return self._get_response(
658
+ "build_bundle",
659
+ BundleResult(
660
+ bundle_id="mock-bundle",
661
+ download_url="",
662
+ expires_at=_now_iso(),
663
+ hmac_manifest="mock-hmac",
664
+ record_counts={},
665
+ zip_path="mock.zip",
666
+ frameworks=[],
667
+ project_id=project_id,
668
+ generated_at=_now_iso(),
669
+ ),
670
+ )
671
+
672
+ def verify_bundle(self, zip_path: str) -> BundleVerificationResult:
673
+ self._record("verify_bundle", zip_path)
674
+ return self._get_response(
675
+ "verify_bundle",
676
+ BundleVerificationResult(
677
+ bundle_id="mock-bundle",
678
+ manifest_valid=True,
679
+ chain_valid=True,
680
+ timestamp_valid=True,
681
+ overall_valid=True,
682
+ errors=[],
683
+ ),
684
+ )
685
+
686
+ def generate_dpa(
687
+ self,
688
+ project_id: str,
689
+ controller_details: dict[str, str],
690
+ processor_details: dict[str, str],
691
+ **kwargs: Any,
692
+ ) -> DPADocument:
693
+ self._record("generate_dpa", project_id, controller_details, processor_details, **kwargs)
694
+ return self._get_response(
695
+ "generate_dpa",
696
+ DPADocument(
697
+ project_id=project_id,
698
+ controller_name="Mock Controller",
699
+ controller_address="",
700
+ processor_name="SpanForge",
701
+ processor_address="",
702
+ processing_purposes=[],
703
+ data_categories=[],
704
+ data_subjects=[],
705
+ sub_processors=[],
706
+ transfer_mechanism="SCCs",
707
+ retention_period="7 years",
708
+ security_measures=[],
709
+ scc_clauses="Module 2",
710
+ document_id="mock-dpa",
711
+ generated_at=_now_iso(),
712
+ text="<mock-dpa/>",
713
+ ),
714
+ )
715
+
716
+ def get_status(self) -> CECStatusInfo:
717
+ self._record("get_status")
718
+ return self._get_response(
719
+ "get_status",
720
+ CECStatusInfo(
721
+ status="ok",
722
+ byos_enabled=False,
723
+ bundle_count=0,
724
+ last_bundle_at=None,
725
+ frameworks_supported=[],
726
+ ),
727
+ )
728
+
729
+
730
+ # ---------------------------------------------------------------------------
731
+ # MockSFAlert
732
+ # ---------------------------------------------------------------------------
733
+
734
+
735
+ class MockSFAlert(_MockBase):
736
+ """Mock replacement for :class:`~spanforge.sdk.alert.SFAlertClient`."""
737
+
738
+ def register_topic(self, topic: str, description: str = "", **kwargs: Any) -> None:
739
+ self._record("register_topic", topic, description, **kwargs)
740
+
741
+ def publish(
742
+ self, topic: str, payload: dict[str, Any] | None = None, **kwargs: Any
743
+ ) -> PublishResult:
744
+ self._record("publish", topic, payload, **kwargs)
745
+ return self._get_response(
746
+ "publish", PublishResult(alert_id="mock-alert-id", routed_to=[], suppressed=False)
747
+ )
748
+
749
+ def acknowledge(self, alert_id: str) -> bool:
750
+ self._record("acknowledge", alert_id)
751
+ return self._get_response("acknowledge", True)
752
+
753
+ def get_alert_history(self, **kwargs: Any) -> list[AlertRecord]:
754
+ self._record("get_alert_history", **kwargs)
755
+ return self._get_response("get_alert_history", [])
756
+
757
+ def get_status(self) -> AlertStatusInfo:
758
+ self._record("get_status")
759
+ return self._get_response(
760
+ "get_status",
761
+ AlertStatusInfo(
762
+ status="ok",
763
+ publish_count=0,
764
+ suppress_count=0,
765
+ queue_depth=0,
766
+ registered_topics=0,
767
+ active_maintenance_windows=0,
768
+ healthy=True,
769
+ ),
770
+ )
771
+
772
+ def add_sink(self, alerter: Any, name: str | None = None) -> None:
773
+ self._record("add_sink", alerter, name)
774
+
775
+ def shutdown(self, timeout: float = 5.0) -> None:
776
+ self._record("shutdown", timeout)
777
+
778
+ @property
779
+ def healthy(self) -> bool:
780
+ return True
781
+
782
+
783
+ # ---------------------------------------------------------------------------
784
+ # MockSFTrust
785
+ # ---------------------------------------------------------------------------
786
+
787
+
788
+ class MockSFTrust(_MockBase):
789
+ """Mock replacement for :class:`~spanforge.sdk.trust.SFTrustClient`."""
790
+
791
+ def get_scorecard(self, project_id: str | None = None, **kwargs: Any) -> TrustScorecardResponse:
792
+ self._record("get_scorecard", project_id, **kwargs)
793
+ dim = TrustDimension(score=1.0, trend="stable", last_updated=_now_iso())
794
+ return self._get_response(
795
+ "get_scorecard",
796
+ TrustScorecardResponse(
797
+ project_id=project_id or "mock",
798
+ overall_score=1.0,
799
+ colour_band="green",
800
+ transparency=dim,
801
+ reliability=dim,
802
+ user_trust=dim,
803
+ security=dim,
804
+ traceability=dim,
805
+ from_dt=_now_iso(),
806
+ to_dt=_now_iso(),
807
+ record_count=0,
808
+ weights=TrustDimensionWeights(),
809
+ ),
810
+ )
811
+
812
+ def get_history(self, project_id: str | None = None, **kwargs: Any) -> list[TrustHistoryEntry]:
813
+ self._record("get_history", project_id, **kwargs)
814
+ return self._get_response("get_history", [])
815
+
816
+ def get_badge(self, project_id: str | None = None) -> TrustBadgeResult:
817
+ self._record("get_badge", project_id)
818
+ return self._get_response(
819
+ "get_badge",
820
+ TrustBadgeResult(svg="<svg/>", overall=1.0, colour_band="green", etag="mock-etag"),
821
+ )
822
+
823
+ def get_status(self) -> TrustStatusInfo:
824
+ self._record("get_status")
825
+ return self._get_response(
826
+ "get_status",
827
+ TrustStatusInfo(
828
+ status="ok",
829
+ dimension_count=5,
830
+ total_trust_records=0,
831
+ pipelines_registered=0,
832
+ last_scorecard_computed=None,
833
+ ),
834
+ )
835
+
836
+
837
+ # ---------------------------------------------------------------------------
838
+ # MockSFEnterprise
839
+ # ---------------------------------------------------------------------------
840
+
841
+
842
+ class MockSFEnterprise(_MockBase):
843
+ """Mock replacement for :class:`~spanforge.sdk.enterprise.SFEnterpriseClient`."""
844
+
845
+ def register_tenant(self, project_id: str, org_id: str, **kwargs: Any) -> TenantConfig:
846
+ self._record("register_tenant", project_id, org_id, **kwargs)
847
+ return self._get_response(
848
+ "register_tenant",
849
+ TenantConfig(
850
+ project_id=project_id,
851
+ org_id=org_id,
852
+ data_residency="global",
853
+ org_secret="mock-secret", # nosec B106
854
+ ),
855
+ )
856
+
857
+ def get_tenant(self, project_id: str) -> TenantConfig | None:
858
+ self._record("get_tenant", project_id)
859
+ return self._get_response("get_tenant", None)
860
+
861
+ def list_tenants(self) -> list[TenantConfig]:
862
+ self._record("list_tenants")
863
+ return self._get_response("list_tenants", [])
864
+
865
+ def get_isolation_scope(self, project_id: str) -> IsolationScope:
866
+ self._record("get_isolation_scope", project_id)
867
+ return self._get_response(
868
+ "get_isolation_scope", IsolationScope(org_id="mock-org", project_id=project_id)
869
+ )
870
+
871
+ def check_cross_project_access(self, source: str, targets: list[str]) -> None:
872
+ self._record("check_cross_project_access", source, targets)
873
+
874
+ def get_endpoint_for_project(self, project_id: str) -> str:
875
+ self._record("get_endpoint_for_project", project_id)
876
+ return self._get_response("get_endpoint_for_project", "http://localhost:8080")
877
+
878
+ def enforce_data_residency(self, project_id: str, target_region: str) -> None:
879
+ self._record("enforce_data_residency", project_id, target_region)
880
+
881
+ def configure_encryption(self, **kwargs: Any) -> EncryptionConfig:
882
+ self._record("configure_encryption", **kwargs)
883
+ return self._get_response("configure_encryption", EncryptionConfig())
884
+
885
+ def get_encryption_config(self) -> EncryptionConfig:
886
+ self._record("get_encryption_config")
887
+ return self._get_response("get_encryption_config", EncryptionConfig())
888
+
889
+ def encrypt_payload(self, plaintext: bytes, key: bytes) -> dict[str, Any]:
890
+ self._record("encrypt_payload", plaintext, key)
891
+ return self._get_response("encrypt_payload", {"ciphertext": "", "nonce": "", "tag": ""})
892
+
893
+ def decrypt_payload(
894
+ self, ciphertext_hex: str, nonce_hex: str, tag_hex: str, key: bytes
895
+ ) -> bytes:
896
+ self._record("decrypt_payload", ciphertext_hex, nonce_hex, tag_hex, key)
897
+ return self._get_response("decrypt_payload", b"")
898
+
899
+ def configure_airgap(self, **kwargs: Any) -> AirGapConfig:
900
+ self._record("configure_airgap", **kwargs)
901
+ return self._get_response("configure_airgap", AirGapConfig())
902
+
903
+ def get_airgap_config(self) -> AirGapConfig:
904
+ self._record("get_airgap_config")
905
+ return self._get_response("get_airgap_config", AirGapConfig())
906
+
907
+ def assert_network_allowed(self) -> None:
908
+ self._record("assert_network_allowed")
909
+
910
+ def check_health_endpoint(
911
+ self, service: str, endpoint: str = "/healthz"
912
+ ) -> HealthEndpointResult:
913
+ self._record("check_health_endpoint", service, endpoint)
914
+ return self._get_response(
915
+ "check_health_endpoint",
916
+ HealthEndpointResult(
917
+ service=service,
918
+ endpoint=endpoint,
919
+ status=200,
920
+ ok=True,
921
+ latency_ms=1.0,
922
+ checked_at=_now_iso(),
923
+ ),
924
+ )
925
+
926
+ def check_all_services_health(self) -> list[HealthEndpointResult]:
927
+ self._record("check_all_services_health")
928
+ return self._get_response("check_all_services_health", [])
929
+
930
+ def get_status(self) -> EnterpriseStatusInfo:
931
+ self._record("get_status")
932
+ return self._get_response("get_status", EnterpriseStatusInfo(status="ok"))
933
+
934
+
935
+ # ---------------------------------------------------------------------------
936
+ # MockSFSecurity
937
+ # ---------------------------------------------------------------------------
938
+
939
+
940
+ class MockSFSecurity(_MockBase):
941
+ """Mock replacement for :class:`~spanforge.sdk.security.SFSecurityClient`."""
942
+
943
+ def run_owasp_audit(self, **kwargs: Any) -> SecurityAuditResult:
944
+ self._record("run_owasp_audit", **kwargs)
945
+ return self._get_response(
946
+ "run_owasp_audit",
947
+ SecurityAuditResult(categories={}, pass_=True, audited_at=_now_iso(), threat_model=[]),
948
+ )
949
+
950
+ def add_threat(
951
+ self, service: str, category: str, threat: str, mitigation: str, risk_level: str = "medium"
952
+ ) -> ThreatModelEntry:
953
+ self._record("add_threat", service, category, threat, mitigation, risk_level)
954
+ return self._get_response(
955
+ "add_threat",
956
+ ThreatModelEntry(
957
+ service=service,
958
+ category=category,
959
+ threat=threat,
960
+ mitigation=mitigation,
961
+ risk_level=risk_level,
962
+ reviewed_at=_now_iso(),
963
+ ),
964
+ )
965
+
966
+ def get_threat_model(self, service: str | None = None) -> list[ThreatModelEntry]:
967
+ self._record("get_threat_model", service)
968
+ return self._get_response("get_threat_model", [])
969
+
970
+ def generate_default_threat_model(self) -> list[ThreatModelEntry]:
971
+ self._record("generate_default_threat_model")
972
+ return self._get_response("generate_default_threat_model", [])
973
+
974
+ def scan_dependencies(self, **kwargs: Any) -> list[Any]:
975
+ self._record("scan_dependencies", **kwargs)
976
+ return self._get_response("scan_dependencies", [])
977
+
978
+ def run_static_analysis(self, **kwargs: Any) -> list[Any]:
979
+ self._record("run_static_analysis", **kwargs)
980
+ return self._get_response("run_static_analysis", [])
981
+
982
+ def audit_logs_for_secrets(self, log_lines: list[str]) -> int:
983
+ self._record("audit_logs_for_secrets", log_lines)
984
+ return self._get_response("audit_logs_for_secrets", 0)
985
+
986
+ def audit_logs_for_secrets_safe(self, log_lines: list[str]) -> int:
987
+ self._record("audit_logs_for_secrets_safe", log_lines)
988
+ return self._get_response("audit_logs_for_secrets_safe", 0)
989
+
990
+ def run_full_scan(self, **kwargs: Any) -> SecurityScanResult:
991
+ self._record("run_full_scan", **kwargs)
992
+ return self._get_response(
993
+ "run_full_scan",
994
+ SecurityScanResult(
995
+ vulnerabilities=[],
996
+ static_findings=[],
997
+ secrets_in_logs=0,
998
+ pass_=True,
999
+ scanned_at=_now_iso(),
1000
+ ),
1001
+ )
1002
+
1003
+ def get_last_scan(self) -> SecurityScanResult | None:
1004
+ self._record("get_last_scan")
1005
+ return self._get_response("get_last_scan", None)
1006
+
1007
+ def get_status(self) -> dict[str, Any]:
1008
+ self._record("get_status")
1009
+ return self._get_response("get_status", {"status": "ok"})
1010
+
1011
+
1012
+ # ---------------------------------------------------------------------------
1013
+ # mock_all_services() context manager
1014
+ # ---------------------------------------------------------------------------
1015
+
1016
+
1017
+ @contextlib.contextmanager
1018
+ def mock_all_services() -> Generator[dict[str, _MockBase], None, None]:
1019
+ """Replace all SDK singletons with mock clients for the duration of the block.
1020
+
1021
+ Yields a dict mapping service names to their mock instances::
1022
+
1023
+ with mock_all_services() as mocks:
1024
+ mocks["sf_pii"].configure_response("scan", custom_result)
1025
+ # ... code under test ...
1026
+ mocks["sf_audit"].assert_called("append")
1027
+ """
1028
+ from spanforge import sdk
1029
+
1030
+ mocks = {
1031
+ "sf_identity": MockSFIdentity(),
1032
+ "sf_pii": MockSFPII(),
1033
+ "sf_secrets": MockSFSecrets(),
1034
+ "sf_audit": MockSFAudit(),
1035
+ "sf_observe": MockSFObserve(),
1036
+ "sf_gate": MockSFGate(),
1037
+ "sf_cec": MockSFCEC(),
1038
+ "sf_alert": MockSFAlert(),
1039
+ "sf_trust": MockSFTrust(),
1040
+ "sf_enterprise": MockSFEnterprise(),
1041
+ "sf_security": MockSFSecurity(),
1042
+ }
1043
+
1044
+ originals = {name: getattr(sdk, name) for name in mocks}
1045
+
1046
+ try:
1047
+ for name, mock in mocks.items():
1048
+ setattr(sdk, name, mock)
1049
+ yield mocks
1050
+ finally:
1051
+ for name, original in originals.items():
1052
+ setattr(sdk, name, original)