@privacyscrubber/sdk 2.3.7 → 2.4.0
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.
- package/README.md +86 -28
- package/benchmark.js +347 -0
- package/browser.js +2 -2
- package/cli.js +54 -2
- package/examples/python-bridge/MANIFEST.in +5 -0
- package/examples/python-bridge/README.md +265 -0
- package/examples/python-bridge/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/dist/privacyscrubber-2.3.9-py3-none-any.whl +0 -0
- package/examples/python-bridge/dist/privacyscrubber-2.3.9.tar.gz +0 -0
- package/examples/python-bridge/fastapi_example.py +206 -0
- package/examples/python-bridge/langchain_adapter.py +20 -0
- package/examples/python-bridge/langchain_example.py +126 -0
- package/examples/python-bridge/llamaindex_example.py +103 -0
- package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/privacyscrubber/cli.py +138 -0
- package/examples/python-bridge/privacyscrubber/client.py +542 -0
- package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
- package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
- package/examples/python-bridge/privacyscrubber/types.py +52 -0
- package/examples/python-bridge/privacyscrubber.egg-info/PKG-INFO +311 -0
- package/examples/python-bridge/privacyscrubber.egg-info/SOURCES.txt +18 -0
- package/examples/python-bridge/privacyscrubber.egg-info/dependency_links.txt +1 -0
- package/examples/python-bridge/privacyscrubber.egg-info/entry_points.txt +3 -0
- package/examples/python-bridge/privacyscrubber.egg-info/requires.txt +18 -0
- package/examples/python-bridge/privacyscrubber.egg-info/top_level.txt +1 -0
- package/examples/python-bridge/privacyscrubber.py +36 -0
- package/examples/python-bridge/pyproject.toml +62 -0
- package/examples/python-bridge/setup.py +66 -0
- package/examples/python-bridge/test_python_bridge.py +320 -0
- package/examples/vercel-ai-stream.js +195 -0
- package/fastmcp.cjs +291 -2
- package/fastmcp.d.ts +18 -0
- package/fastmcp.js +4 -0
- package/index.cjs +211 -134
- package/index.js +211 -134
- package/package.json +15 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +400 -0
- package/vercel-ai.d.ts +156 -0
- package/vercel-ai.js +36 -0
- package/vision.cjs +13 -5
- package/vision.js +13 -5
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PrivacyScrubber ZTDS Client Implementation
|
|
3
|
+
Synchronous and Asynchronous sidecar IPC over stdio NDJSON.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Any, Dict, List, Optional, Union
|
|
14
|
+
|
|
15
|
+
from privacyscrubber.exceptions import PrivacyScrubberError, PrivacyScrubberLicenseError
|
|
16
|
+
from privacyscrubber.types import SanitizeResult, RestoreResult, ObjectSanitizeResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _find_daemon_path(explicit_path: Optional[str] = None) -> str:
|
|
20
|
+
"""Resolves the absolute path to bridge_daemon.cjs across package and filesystem."""
|
|
21
|
+
if explicit_path and os.path.isfile(explicit_path):
|
|
22
|
+
return os.path.abspath(explicit_path)
|
|
23
|
+
|
|
24
|
+
env_path = os.environ.get("PRIVACYSCRUBBER_BRIDGE_DAEMON")
|
|
25
|
+
if env_path and os.path.isfile(env_path):
|
|
26
|
+
return os.path.abspath(env_path)
|
|
27
|
+
|
|
28
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
29
|
+
candidates = [
|
|
30
|
+
# 1. Directly inside packaged directory
|
|
31
|
+
os.path.join(here, "bridge_daemon.cjs"),
|
|
32
|
+
# 2. Parent directory (for dev repository layouts)
|
|
33
|
+
os.path.join(here, "..", "bridge_daemon.cjs"),
|
|
34
|
+
# 3. Monorepo / relative to npm-sdk
|
|
35
|
+
os.path.join(here, "..", "..", "python-bridge", "bridge_daemon.cjs"),
|
|
36
|
+
os.path.join(here, "..", "..", "examples", "python-bridge", "bridge_daemon.cjs"),
|
|
37
|
+
# 4. Standard node_modules locations
|
|
38
|
+
os.path.join(here, "node_modules", "@privacyscrubber", "sdk", "examples", "python-bridge", "bridge_daemon.cjs"),
|
|
39
|
+
os.path.join(os.getcwd(), "node_modules", "@privacyscrubber", "sdk", "examples", "python-bridge", "bridge_daemon.cjs"),
|
|
40
|
+
os.path.join(os.getcwd(), "npm-sdk", "examples", "python-bridge", "bridge_daemon.cjs"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
for cand in candidates:
|
|
44
|
+
if os.path.isfile(cand):
|
|
45
|
+
return os.path.abspath(cand)
|
|
46
|
+
|
|
47
|
+
raise FileNotFoundError(
|
|
48
|
+
"Could not locate bridge_daemon.cjs. "
|
|
49
|
+
"Please provide daemon_path or set PRIVACYSCRUBBER_BRIDGE_DAEMON environment variable."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PrivacyScrubberClient:
|
|
54
|
+
"""
|
|
55
|
+
Synchronous PrivacyScrubber ZTDS client.
|
|
56
|
+
Manages a persistent, low-overhead Node.js sidecar daemon over stdio NDJSON.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
node_path: str = "node",
|
|
62
|
+
daemon_path: Optional[str] = None,
|
|
63
|
+
license_key: Optional[str] = None,
|
|
64
|
+
default_profile: str = "General",
|
|
65
|
+
detect_secrets: bool = False,
|
|
66
|
+
auto_start: bool = True
|
|
67
|
+
) -> None:
|
|
68
|
+
self.node_path = os.environ.get("PRIVACYSCRUBBER_NODE_PATH", node_path)
|
|
69
|
+
self.daemon_path = _find_daemon_path(daemon_path)
|
|
70
|
+
self.license_key = license_key or os.environ.get("PRIVACYSCRUBBER_KEY") or os.environ.get("PRIVACYSCRUBBER_LICENSE_KEY") or ""
|
|
71
|
+
self.default_profile = default_profile
|
|
72
|
+
self.detect_secrets = detect_secrets
|
|
73
|
+
|
|
74
|
+
self._process: Optional[subprocess.Popen] = None
|
|
75
|
+
self._lock = threading.Lock()
|
|
76
|
+
self._req_id = 0
|
|
77
|
+
|
|
78
|
+
if auto_start:
|
|
79
|
+
self.start()
|
|
80
|
+
|
|
81
|
+
def start(self) -> None:
|
|
82
|
+
"""Starts the persistent sidecar process."""
|
|
83
|
+
with self._lock:
|
|
84
|
+
if self._process and self._process.poll() is None:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
cmd = [
|
|
88
|
+
self.node_path,
|
|
89
|
+
self.daemon_path,
|
|
90
|
+
"--profile", self.default_profile
|
|
91
|
+
]
|
|
92
|
+
if self.license_key:
|
|
93
|
+
cmd.extend(["--license-key", self.license_key])
|
|
94
|
+
|
|
95
|
+
self._process = subprocess.Popen(
|
|
96
|
+
cmd,
|
|
97
|
+
stdin=subprocess.PIPE,
|
|
98
|
+
stdout=subprocess.PIPE,
|
|
99
|
+
stderr=subprocess.PIPE,
|
|
100
|
+
text=True,
|
|
101
|
+
bufsize=1
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def close(self) -> None:
|
|
105
|
+
"""Gracefully shuts down the sidecar process."""
|
|
106
|
+
with self._lock:
|
|
107
|
+
if not self._process:
|
|
108
|
+
return
|
|
109
|
+
try:
|
|
110
|
+
if self._process.poll() is None:
|
|
111
|
+
if self._process.stdin:
|
|
112
|
+
self._process.stdin.close()
|
|
113
|
+
self._process.terminate()
|
|
114
|
+
self._process.wait(timeout=2.0)
|
|
115
|
+
except Exception:
|
|
116
|
+
try:
|
|
117
|
+
self._process.kill()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
finally:
|
|
121
|
+
try:
|
|
122
|
+
if self._process.stdout:
|
|
123
|
+
self._process.stdout.close()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
try:
|
|
127
|
+
if self._process.stderr:
|
|
128
|
+
self._process.stderr.close()
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
self._process = None
|
|
132
|
+
|
|
133
|
+
def __enter__(self) -> PrivacyScrubberClient:
|
|
134
|
+
self.start()
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
138
|
+
self.close()
|
|
139
|
+
|
|
140
|
+
def _send_request(self, action: str, **kwargs: Any) -> Dict[str, Any]:
|
|
141
|
+
"""Sends a JSON-RPC request to the sidecar and awaits single-line response."""
|
|
142
|
+
with self._lock:
|
|
143
|
+
if not self._process or self._process.poll() is not None:
|
|
144
|
+
self.start()
|
|
145
|
+
|
|
146
|
+
self._req_id += 1
|
|
147
|
+
req_id = self._req_id
|
|
148
|
+
|
|
149
|
+
payload = {
|
|
150
|
+
"id": req_id,
|
|
151
|
+
"action": action,
|
|
152
|
+
**kwargs
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
req_line = json.dumps(payload) + "\n"
|
|
156
|
+
try:
|
|
157
|
+
assert self._process.stdin is not None
|
|
158
|
+
assert self._process.stdout is not None
|
|
159
|
+
self._process.stdin.write(req_line)
|
|
160
|
+
self._process.stdin.flush()
|
|
161
|
+
|
|
162
|
+
resp_line = self._process.stdout.readline()
|
|
163
|
+
if not resp_line:
|
|
164
|
+
stderr_output = self._process.stderr.read() if self._process.stderr else ""
|
|
165
|
+
raise PrivacyScrubberError(f"Sidecar terminated unexpectedly: {stderr_output.strip()}")
|
|
166
|
+
|
|
167
|
+
res = json.loads(resp_line.strip())
|
|
168
|
+
except Exception as e:
|
|
169
|
+
if not isinstance(e, PrivacyScrubberError):
|
|
170
|
+
raise PrivacyScrubberError(f"RPC communication failure: {str(e)}") from e
|
|
171
|
+
raise
|
|
172
|
+
|
|
173
|
+
if "error" in res and res["error"]:
|
|
174
|
+
err = res["error"]
|
|
175
|
+
msg = err.get("message", "Unknown error in PrivacyScrubber engine")
|
|
176
|
+
if err.get("name") == "PrivacyScrubberLicenseError":
|
|
177
|
+
raise PrivacyScrubberLicenseError(
|
|
178
|
+
msg,
|
|
179
|
+
tier=err.get("tier", "FREE"),
|
|
180
|
+
limit=err.get("limit", 15000),
|
|
181
|
+
profile=err.get("profile", "General")
|
|
182
|
+
)
|
|
183
|
+
raise PrivacyScrubberError(msg)
|
|
184
|
+
|
|
185
|
+
return res.get("result", {})
|
|
186
|
+
|
|
187
|
+
def ping(self) -> Dict[str, Any]:
|
|
188
|
+
"""Verifies sidecar health, engine version, and process uptime."""
|
|
189
|
+
return self._send_request("ping")
|
|
190
|
+
|
|
191
|
+
def status(self) -> Dict[str, Any]:
|
|
192
|
+
"""Queries engine status, memory footprint, and session size."""
|
|
193
|
+
return self._send_request("status")
|
|
194
|
+
|
|
195
|
+
def sanitize(
|
|
196
|
+
self,
|
|
197
|
+
text: str,
|
|
198
|
+
profile: Optional[str] = None,
|
|
199
|
+
detect_secrets: Optional[bool] = None,
|
|
200
|
+
license_key: Optional[str] = None,
|
|
201
|
+
persist_session: bool = True,
|
|
202
|
+
generate_receipt: bool = True,
|
|
203
|
+
ignore_list: Optional[List[str]] = None,
|
|
204
|
+
custom_rules: Optional[List[Dict[str, Any]]] = None,
|
|
205
|
+
throw_on_limit: bool = True
|
|
206
|
+
) -> SanitizeResult:
|
|
207
|
+
"""
|
|
208
|
+
Sanitizes text, replacing sensitive PII with cryptographic surrogate tokens.
|
|
209
|
+
Operates 100% in local process RAM with sub-millisecond latency.
|
|
210
|
+
"""
|
|
211
|
+
res = self._send_request(
|
|
212
|
+
"sanitize",
|
|
213
|
+
text=text,
|
|
214
|
+
profile=profile or self.default_profile,
|
|
215
|
+
detect_secrets=self.detect_secrets if detect_secrets is None else detect_secrets,
|
|
216
|
+
license_key=license_key or self.license_key,
|
|
217
|
+
persist_session=persist_session,
|
|
218
|
+
generate_receipt=generate_receipt,
|
|
219
|
+
ignore_list=ignore_list,
|
|
220
|
+
custom_rules=custom_rules,
|
|
221
|
+
throw_on_limit=throw_on_limit
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return SanitizeResult(
|
|
225
|
+
sanitized=res.get("sanitized", ""),
|
|
226
|
+
token_map=res.get("token_map", {}),
|
|
227
|
+
count=res.get("count", 0),
|
|
228
|
+
execution_ms=res.get("execution_ms", 0.0),
|
|
229
|
+
telemetry=res.get("telemetry"),
|
|
230
|
+
audit_receipt=res.get("audit_receipt"),
|
|
231
|
+
license=res.get("license"),
|
|
232
|
+
matches=res.get("matches")
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def restore(
|
|
236
|
+
self,
|
|
237
|
+
text: str,
|
|
238
|
+
token_map: Optional[Dict[str, str]] = None
|
|
239
|
+
) -> RestoreResult:
|
|
240
|
+
"""
|
|
241
|
+
Replaces surrogate tokens in AI responses with their original values.
|
|
242
|
+
Uses in-memory engine session or the explicitly provided token_map.
|
|
243
|
+
"""
|
|
244
|
+
res = self._send_request(
|
|
245
|
+
"restore",
|
|
246
|
+
text=text,
|
|
247
|
+
token_map=token_map
|
|
248
|
+
)
|
|
249
|
+
return RestoreResult(
|
|
250
|
+
restored_text=res.get("restored_text", ""),
|
|
251
|
+
restored_count=res.get("restored_count", 0)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def sanitize_object(
|
|
255
|
+
self,
|
|
256
|
+
data: Any,
|
|
257
|
+
profile: Optional[str] = None,
|
|
258
|
+
detect_secrets: Optional[bool] = None,
|
|
259
|
+
license_key: Optional[str] = None,
|
|
260
|
+
persist_session: bool = True,
|
|
261
|
+
throw_on_limit: bool = True
|
|
262
|
+
) -> ObjectSanitizeResult:
|
|
263
|
+
"""
|
|
264
|
+
Recursively sanitizes dictionary or list data structures.
|
|
265
|
+
Preserves original keys and non-string values intact.
|
|
266
|
+
"""
|
|
267
|
+
res = self._send_request(
|
|
268
|
+
"sanitize_object",
|
|
269
|
+
data=data,
|
|
270
|
+
profile=profile or self.default_profile,
|
|
271
|
+
detect_secrets=self.detect_secrets if detect_secrets is None else detect_secrets,
|
|
272
|
+
license_key=license_key or self.license_key,
|
|
273
|
+
persist_session=persist_session,
|
|
274
|
+
throw_on_limit=throw_on_limit
|
|
275
|
+
)
|
|
276
|
+
return ObjectSanitizeResult(
|
|
277
|
+
sanitized=res.get("sanitized"),
|
|
278
|
+
token_map=res.get("token_map", {}),
|
|
279
|
+
count=res.get("count", 0),
|
|
280
|
+
execution_ms=res.get("execution_ms", 0.0),
|
|
281
|
+
telemetry=res.get("telemetry"),
|
|
282
|
+
audit_receipt=res.get("audit_receipt")
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def restore_object(
|
|
286
|
+
self,
|
|
287
|
+
data: Any,
|
|
288
|
+
token_map: Optional[Dict[str, str]] = None
|
|
289
|
+
) -> Any:
|
|
290
|
+
"""Recursively restores tokenized dictionary or list structures."""
|
|
291
|
+
res = self._send_request(
|
|
292
|
+
"restore_object",
|
|
293
|
+
data=data,
|
|
294
|
+
token_map=token_map
|
|
295
|
+
)
|
|
296
|
+
return res.get("restored")
|
|
297
|
+
|
|
298
|
+
def reset_session(self) -> bool:
|
|
299
|
+
"""Wipes the persistent in-memory session token map."""
|
|
300
|
+
res = self._send_request("reset_session")
|
|
301
|
+
return bool(res.get("success", False))
|
|
302
|
+
|
|
303
|
+
def mark_false_positive(self, token: str) -> Dict[str, Any]:
|
|
304
|
+
"""
|
|
305
|
+
Excludes the plaintext value associated with a token from future sanitization.
|
|
306
|
+
"""
|
|
307
|
+
return self._send_request("mark_false_positive", token=token)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class AsyncPrivacyScrubberClient:
|
|
311
|
+
"""
|
|
312
|
+
Asynchronous PrivacyScrubber ZTDS client for FastAPI and asyncio runtimes.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def __init__(
|
|
316
|
+
self,
|
|
317
|
+
node_path: str = "node",
|
|
318
|
+
daemon_path: Optional[str] = None,
|
|
319
|
+
license_key: Optional[str] = None,
|
|
320
|
+
default_profile: str = "General",
|
|
321
|
+
detect_secrets: bool = False
|
|
322
|
+
) -> None:
|
|
323
|
+
self.node_path = os.environ.get("PRIVACYSCRUBBER_NODE_PATH", node_path)
|
|
324
|
+
self.daemon_path = _find_daemon_path(daemon_path)
|
|
325
|
+
self.license_key = license_key or os.environ.get("PRIVACYSCRUBBER_KEY") or os.environ.get("PRIVACYSCRUBBER_LICENSE_KEY") or ""
|
|
326
|
+
self.default_profile = default_profile
|
|
327
|
+
self.detect_secrets = detect_secrets
|
|
328
|
+
|
|
329
|
+
self._process: Optional[asyncio.subprocess.Process] = None
|
|
330
|
+
self._lock = asyncio.Lock()
|
|
331
|
+
self._req_id = 0
|
|
332
|
+
|
|
333
|
+
async def start(self) -> None:
|
|
334
|
+
"""Starts the persistent asynchronous sidecar process."""
|
|
335
|
+
async with self._lock:
|
|
336
|
+
if self._process and self._process.returncode is None:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
cmd = [
|
|
340
|
+
self.node_path,
|
|
341
|
+
self.daemon_path,
|
|
342
|
+
"--profile", self.default_profile
|
|
343
|
+
]
|
|
344
|
+
if self.license_key:
|
|
345
|
+
cmd.extend(["--license-key", self.license_key])
|
|
346
|
+
|
|
347
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
348
|
+
*cmd,
|
|
349
|
+
stdin=asyncio.subprocess.PIPE,
|
|
350
|
+
stdout=asyncio.subprocess.PIPE,
|
|
351
|
+
stderr=asyncio.subprocess.PIPE
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
async def close(self) -> None:
|
|
355
|
+
"""Gracefully closes the async sidecar process."""
|
|
356
|
+
async with self._lock:
|
|
357
|
+
if not self._process:
|
|
358
|
+
return
|
|
359
|
+
try:
|
|
360
|
+
if self._process.returncode is None:
|
|
361
|
+
if self._process.stdin:
|
|
362
|
+
self._process.stdin.close()
|
|
363
|
+
await self._process.stdin.wait_closed()
|
|
364
|
+
self._process.terminate()
|
|
365
|
+
await asyncio.wait_for(self._process.wait(), timeout=2.0)
|
|
366
|
+
except Exception:
|
|
367
|
+
try:
|
|
368
|
+
self._process.kill()
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
finally:
|
|
372
|
+
self._process = None
|
|
373
|
+
|
|
374
|
+
async def __aenter__(self) -> AsyncPrivacyScrubberClient:
|
|
375
|
+
await self.start()
|
|
376
|
+
return self
|
|
377
|
+
|
|
378
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
379
|
+
await self.close()
|
|
380
|
+
|
|
381
|
+
async def _send_request(self, action: str, **kwargs: Any) -> Dict[str, Any]:
|
|
382
|
+
"""Sends an async JSON-RPC request to the sidecar."""
|
|
383
|
+
async with self._lock:
|
|
384
|
+
if not self._process or self._process.returncode is not None:
|
|
385
|
+
cmd = [
|
|
386
|
+
self.node_path,
|
|
387
|
+
self.daemon_path,
|
|
388
|
+
"--profile", self.default_profile
|
|
389
|
+
]
|
|
390
|
+
if self.license_key:
|
|
391
|
+
cmd.extend(["--license-key", self.license_key])
|
|
392
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
393
|
+
*cmd,
|
|
394
|
+
stdin=asyncio.subprocess.PIPE,
|
|
395
|
+
stdout=asyncio.subprocess.PIPE,
|
|
396
|
+
stderr=asyncio.subprocess.PIPE
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
self._req_id += 1
|
|
400
|
+
req_id = self._req_id
|
|
401
|
+
|
|
402
|
+
payload = {
|
|
403
|
+
"id": req_id,
|
|
404
|
+
"action": action,
|
|
405
|
+
**kwargs
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
req_line = (json.dumps(payload) + "\n").encode("utf-8")
|
|
409
|
+
assert self._process.stdin is not None
|
|
410
|
+
assert self._process.stdout is not None
|
|
411
|
+
|
|
412
|
+
self._process.stdin.write(req_line)
|
|
413
|
+
await self._process.stdin.drain()
|
|
414
|
+
|
|
415
|
+
resp_bytes = await self._process.stdout.readline()
|
|
416
|
+
if not resp_bytes:
|
|
417
|
+
stderr_bytes = await self._process.stderr.read() if self._process.stderr else b""
|
|
418
|
+
raise PrivacyScrubberError(f"Async sidecar terminated: {stderr_bytes.decode('utf-8').strip()}")
|
|
419
|
+
|
|
420
|
+
res = json.loads(resp_bytes.decode("utf-8").strip())
|
|
421
|
+
|
|
422
|
+
if "error" in res and res["error"]:
|
|
423
|
+
err = res["error"]
|
|
424
|
+
msg = err.get("message", "Unknown error in PrivacyScrubber engine")
|
|
425
|
+
if err.get("name") == "PrivacyScrubberLicenseError":
|
|
426
|
+
raise PrivacyScrubberLicenseError(
|
|
427
|
+
msg,
|
|
428
|
+
tier=err.get("tier", "FREE"),
|
|
429
|
+
limit=err.get("limit", 15000),
|
|
430
|
+
profile=err.get("profile", "General")
|
|
431
|
+
)
|
|
432
|
+
raise PrivacyScrubberError(msg)
|
|
433
|
+
|
|
434
|
+
return res.get("result", {})
|
|
435
|
+
|
|
436
|
+
async def ping(self) -> Dict[str, Any]:
|
|
437
|
+
return await self._send_request("ping")
|
|
438
|
+
|
|
439
|
+
async def status(self) -> Dict[str, Any]:
|
|
440
|
+
return await self._send_request("status")
|
|
441
|
+
|
|
442
|
+
async def sanitize(
|
|
443
|
+
self,
|
|
444
|
+
text: str,
|
|
445
|
+
profile: Optional[str] = None,
|
|
446
|
+
detect_secrets: Optional[bool] = None,
|
|
447
|
+
license_key: Optional[str] = None,
|
|
448
|
+
persist_session: bool = True,
|
|
449
|
+
generate_receipt: bool = True,
|
|
450
|
+
ignore_list: Optional[List[str]] = None,
|
|
451
|
+
custom_rules: Optional[List[Dict[str, Any]]] = None,
|
|
452
|
+
throw_on_limit: bool = True
|
|
453
|
+
) -> SanitizeResult:
|
|
454
|
+
res = await self._send_request(
|
|
455
|
+
"sanitize",
|
|
456
|
+
text=text,
|
|
457
|
+
profile=profile or self.default_profile,
|
|
458
|
+
detect_secrets=self.detect_secrets if detect_secrets is None else detect_secrets,
|
|
459
|
+
license_key=license_key or self.license_key,
|
|
460
|
+
persist_session=persist_session,
|
|
461
|
+
generate_receipt=generate_receipt,
|
|
462
|
+
ignore_list=ignore_list,
|
|
463
|
+
custom_rules=custom_rules,
|
|
464
|
+
throw_on_limit=throw_on_limit
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return SanitizeResult(
|
|
468
|
+
sanitized=res.get("sanitized", ""),
|
|
469
|
+
token_map=res.get("token_map", {}),
|
|
470
|
+
count=res.get("count", 0),
|
|
471
|
+
execution_ms=res.get("execution_ms", 0.0),
|
|
472
|
+
telemetry=res.get("telemetry"),
|
|
473
|
+
audit_receipt=res.get("audit_receipt"),
|
|
474
|
+
license=res.get("license"),
|
|
475
|
+
matches=res.get("matches")
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
async def restore(
|
|
479
|
+
self,
|
|
480
|
+
text: str,
|
|
481
|
+
token_map: Optional[Dict[str, str]] = None
|
|
482
|
+
) -> RestoreResult:
|
|
483
|
+
res = await self._send_request(
|
|
484
|
+
"restore",
|
|
485
|
+
text=text,
|
|
486
|
+
token_map=token_map
|
|
487
|
+
)
|
|
488
|
+
return RestoreResult(
|
|
489
|
+
restored_text=res.get("restored_text", ""),
|
|
490
|
+
restored_count=res.get("restored_count", 0)
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
async def sanitize_object(
|
|
494
|
+
self,
|
|
495
|
+
data: Any,
|
|
496
|
+
profile: Optional[str] = None,
|
|
497
|
+
detect_secrets: Optional[bool] = None,
|
|
498
|
+
license_key: Optional[str] = None,
|
|
499
|
+
persist_session: bool = True,
|
|
500
|
+
throw_on_limit: bool = True
|
|
501
|
+
) -> ObjectSanitizeResult:
|
|
502
|
+
res = await self._send_request(
|
|
503
|
+
"sanitize_object",
|
|
504
|
+
data=data,
|
|
505
|
+
profile=profile or self.default_profile,
|
|
506
|
+
detect_secrets=self.detect_secrets if detect_secrets is None else detect_secrets,
|
|
507
|
+
license_key=license_key or self.license_key,
|
|
508
|
+
persist_session=persist_session,
|
|
509
|
+
throw_on_limit=throw_on_limit
|
|
510
|
+
)
|
|
511
|
+
return ObjectSanitizeResult(
|
|
512
|
+
sanitized=res.get("sanitized"),
|
|
513
|
+
token_map=res.get("token_map", {}),
|
|
514
|
+
count=res.get("count", 0),
|
|
515
|
+
execution_ms=res.get("execution_ms", 0.0),
|
|
516
|
+
telemetry=res.get("telemetry"),
|
|
517
|
+
audit_receipt=res.get("audit_receipt")
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
async def restore_object(
|
|
521
|
+
self,
|
|
522
|
+
data: Any,
|
|
523
|
+
token_map: Optional[Dict[str, str]] = None
|
|
524
|
+
) -> Any:
|
|
525
|
+
res = await self._send_request(
|
|
526
|
+
"restore_object",
|
|
527
|
+
data=data,
|
|
528
|
+
token_map=token_map
|
|
529
|
+
)
|
|
530
|
+
return res.get("restored")
|
|
531
|
+
|
|
532
|
+
async def reset_session(self) -> bool:
|
|
533
|
+
res = await self._send_request("reset_session")
|
|
534
|
+
return bool(res.get("success", False))
|
|
535
|
+
|
|
536
|
+
async def mark_false_positive(self, token: str) -> Dict[str, Any]:
|
|
537
|
+
return await self._send_request("mark_false_positive", token=token)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# Aliases
|
|
541
|
+
PrivacyScrubber = PrivacyScrubberClient
|
|
542
|
+
AsyncPrivacyScrubber = AsyncPrivacyScrubberClient
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PrivacyScrubber Exception Hierarchy
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PrivacyScrubberError(Exception):
|
|
9
|
+
"""Base exception for all PrivacyScrubber errors."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PrivacyScrubberLicenseError(PrivacyScrubberError):
|
|
14
|
+
"""Raised when Free tier quotas or license gates are exceeded."""
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
tier: str = "FREE",
|
|
19
|
+
limit: int = 15000,
|
|
20
|
+
profile: str = "General"
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.tier = tier
|
|
24
|
+
self.limit = limit
|
|
25
|
+
self.profile = profile
|
|
26
|
+
self.upgrade_url = "https://privacyscrubber.com/pricing?tier=sdk&src=python_sdk"
|