Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
56 changes: 51 additions & 5 deletions src/redactable/detectors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +57,11 @@
Extras = dict[str, Any]



# --------------------------------------------------------------------
# Finding-based detection types (used by DetectorRegistry)


@dataclass(slots=True)
class Finding:
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions src/redactable/detectors/entropy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -70,6 +71,7 @@ def detect(self, text: str) -> Iterable[Finding]:
_TOKEN = re.compile(r"([A-Za-z0-9_\-=+/]{20,})")



class EntropyDetector:
name = "entropy"

Expand Down
8 changes: 2 additions & 6 deletions src/redactable/hide.py
Original file line number Diff line number Diff line change
@@ -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"]
14 changes: 12 additions & 2 deletions src/redactable/transforms/tokenise.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Loading