diff --git a/README.md b/README.md index 3c18fc2..7fafd7c 100644 --- a/README.md +++ b/README.md @@ -69,17 +69,6 @@ print(result) ```` -### Python SDK - -````bash -from redactable import apply - -data = "Customer email: test@example.com" -result = apply(data, policy="gdpr.yaml") -print(result) -# → "Customer email: ****@example.com" -```` - ### Pandas Integration (Coming in v0.2) Pandas DataFrame support is planned for the next release. For now, apply redaction row-by-row: diff --git a/pyproject.toml b/pyproject.toml index bec5edd..88a3187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "redactable" -description = "One-line value prop." +description = "Policy-driven framework for data redaction, masking, and privacy-preserving transformations." readme = "README.md" requires-python = ">=3.12" license = { text = "MIT" } diff --git a/src/redactable/detectors/base.py b/src/redactable/detectors/base.py index fb53247..e84ad70 100644 --- a/src/redactable/detectors/base.py +++ b/src/redactable/detectors/base.py @@ -3,10 +3,50 @@ Contents: - Finding: dataclass representing a detected entity. -- Detector: Protocol interface that all detectors must implement. +- Match: dataclass for the module-level detection system. +- Detector: Protocol interface that Finding-based detectors must implement. - Shared helper functions: digits_only, luhn_ok, guess_card_brand. """ +import re +from dataclasses import dataclass +from typing import Any, Iterable, Optional, Protocol + +# -------------------------------------------------------------------- +# Match-based detection types (used by individual detector modules) + + +@dataclass(slots=True) +class Match: + label: str # e.g. "EMAIL", "CREDIT_CARD" + start: int # byte/char index in the input text + end: int + value: str # matched text (pre-transform) + confidence: float = 1.0 # 0..1 + meta: dict[str, Any] | None = None + + +# Simple registry +_REGISTRY: dict[str, Any] = {} +_LABEL_TO_DETECTORS: dict[str, list[str]] = {} + + +def register(detector: Any) -> None: + _REGISTRY[detector.name] = detector + for label in detector.labels: + _LABEL_TO_DETECTORS.setdefault(label, []).append(detector.name) + + +def get(name: str) -> Any: + return _REGISTRY[name] + + +def detectors_for(label: str) -> list[Any]: + return [_REGISTRY[n] for n in _LABEL_TO_DETECTORS.get(label, [])] + + +def all_detectors() -> list[Any]: + return list(_REGISTRY.values()) import re from collections.abc import Iterable from dataclasses import dataclass @@ -17,6 +57,11 @@ Extras = dict[str, Any] + +# -------------------------------------------------------------------- +# Finding-based detection types (used by DetectorRegistry) + + @dataclass(slots=True) class Finding: """ @@ -33,10 +78,10 @@ class Finding: kind: str value: str - span: Span + span: tuple[int, int] confidence: float - normalized: str | None = None - extras: Extras | None = None + normalized: Optional[str] = None + extras: dict[str, Any] | None = None def __post_init__(self) -> None: if not (0.0 <= self.confidence <= 1.0): @@ -81,7 +126,8 @@ def all_detectors() -> list[Detector]: # -------------------------------------------------------------------- # Shared helpers -_DIGITS = re.compile(r"\\D+") +_DIGITS = re.compile(r"\D+") + def digits_only(s: str) -> str: diff --git a/src/redactable/detectors/entropy.py b/src/redactable/detectors/entropy.py index f86798b..871c34b 100644 --- a/src/redactable/detectors/entropy.py +++ b/src/redactable/detectors/entropy.py @@ -12,11 +12,12 @@ """ import re -from collections.abc import Iterable +from typing import Iterable -from .base import Finding, register +from .base import Finding, Match, register from .utils import looks_like_secret, shannon_entropy +# -------------------------------------------------------------------- # Regex pattern: matches candidate secrets BASELIKE_PATTERN = re.compile( r""" @@ -70,6 +71,7 @@ def detect(self, text: str) -> Iterable[Finding]: _TOKEN = re.compile(r"([A-Za-z0-9_\-=+/]{20,})") + class EntropyDetector: name = "entropy" diff --git a/src/redactable/hide.py b/src/redactable/hide.py index 4e3cd4e..cf5111d 100644 --- a/src/redactable/hide.py +++ b/src/redactable/hide.py @@ -1,7 +1,3 @@ -""" -Use this as a main entrypoint? -""" - +from .decors import redactable_io from .decors import * # noqa: F401, F403 - -__all__: list[str] = [] +__all__ = ["redactable_io"] diff --git a/src/redactable/transforms/tokenise.py b/src/redactable/transforms/tokenise.py index 9ee88ad..07cc0db 100644 --- a/src/redactable/transforms/tokenise.py +++ b/src/redactable/transforms/tokenise.py @@ -1,8 +1,17 @@ -from collections.abc import Iterable +import hashlib +from typing import Iterable from redactable.detectors import Finding +def _sha256(value: str, salt: str = "") -> str: + return hashlib.sha256((salt + value).encode("utf-8")).hexdigest() + + +def tokenise_in_place(text: str, findings: Iterable[Finding], salt: str = "") -> str: + + + def _mask(value: str, keep_head: int = 0, keep_tail: int = 4, glyph: str = "•") -> str: if len(value) <= keep_head + keep_tail: return glyph * len(value) @@ -19,5 +28,6 @@ def mask_in_place( out = text for f in sorted(findings, key=lambda x: x.span[0], reverse=True): s, e = f.span - out = out[:s] + _mask(out[s:e], keep_head, keep_tail, glyph) + out[e:] + token = _sha256(f.normalized or f.value, salt) + out = out[:s] + token + out[e:] return out