Results & constants¶
Results¶
wardcat.ScanResult
dataclass
¶
ScanResult(
original_text,
sanitized_text,
violations=list(),
scan_error=None,
warnings=list(),
context_id="",
_salt="",
)
Result of a single guard.scan() call.
.. warning::
The original_text and violations[].original fields contain raw PII,
as do :attr:token_map and anything :meth:restore returns. When writing
this object to logs, databases, or API responses, use only
sanitized_text. Use the :meth:redacted method to obtain a dict
that contains no PII.
original_text
instance-attribute
¶
Unmodified original input. Contains raw PII — do not expose externally.
violations
class-attribute
instance-attribute
¶
List of all detected violations. The original fields contain raw PII.
scan_error
class-attribute
instance-attribute
¶
Set when this item failed during :meth:~wardcat.Wardcat.scan_batch.
The original text is returned unchanged. Non-None means the scan result
is incomplete — callers should not treat the text as clean.
warnings
class-attribute
instance-attribute
¶
Non-fatal issues during this scan — e.g. a detector layer that could not run (LLM backend unreachable). The scan still returns results from the other layers, but a non-empty list means detection was degraded: some PII may be missed. Empty when every configured layer ran.
context_id
class-attribute
instance-attribute
¶
Identifies this scan's anonymization pass. It is stamped into every
reversible (:attr:Action.TOKENIZE) placeholder — [EMAIL_1_9f3a2c8b71d4]
— so no two scans ever produce the same token and :meth:restore can tell
another scan's placeholder from its own. Empty when nothing stamped one.
token_map
property
¶
Placeholder → original value for every violation that replaced something.
The vault behind :meth:restore, exposed for callers that need to hand the
mapping to another process (a queue worker restoring a reply later, say)::
vault = result.token_map # {"[EMAIL_1]": "ali@example.com"}
Meaningful for reversible placeholders — see :attr:Action.TOKENIZE; with
redact or mask several values can share one placeholder and the map
keeps only the last. warn violations are absent (nothing was replaced).
.. warning:: Raw PII. Never log or persist this without protecting it like the original text.
redacted
¶
Return a safe :class:RedactedResult dict with no PII.
Excludes the original_text and violations[].original fields.
Use this method for logs, API responses, or database records::
result = guard.scan(text)
log.info("scan result: %s", result.redacted())
Returns:
A :class:RedactedResult (TypedDict) containing sanitized_text,
is_clean, scan_error, and violation metadata (entity_type,
start, end, action, replacement, confidence). Raw PII is not included.
Source code in src/wardcat/core/models.py
restore
¶
Put the real values back — the reverse of the anonymization stage.
The round trip an LLM call needs: scan the prompt, send the anonymized text out, then restore the answer that comes back::
result = guard.scan(prompt)
answer = call_llm(result.sanitized_text) # never sees the real values
print(result.restore(answer)) # answer + its source list
Printing the returned :class:~wardcat.core.restore.RestoredText appends an
ordered source list naming, per placeholder, which filter fired and what it
stood for; use .text for the bare restored text and .substitutions
for the same information as data.
Restoring is reliable when the values were replaced with
:attr:Action.TOKENIZE (or hash): each distinct value has its own
placeholder. With redact/mask two values can collapse onto the same
placeholder — those are reported in .unrestored as ambiguous and
left in the text rather than guessed. Use :meth:reapply to derive a
reversible view of a scan that used another action::
reversible = result.reapply(Action.TOKENIZE)
Placeholders carry this scan's :attr:context_id, so a token produced by a
different scan cannot be matched by accident — restoring one request's
answer against another request's result would otherwise substitute the
wrong person's values. Such tokens are reported as foreign, left in the
text, and make is_complete False; strict=True raises
:class:~wardcat.exceptions.ContextMismatch instead. In a multi-turn
exchange an answer may legitimately carry an earlier scan's placeholders —
pass those results in also and they are restored too::
turn2.restore(answer, also=[turn1], strict=True)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str | None
|
the text to restore; defaults to :attr: |
None
|
strict
|
bool
|
raise :class: |
False
|
also
|
Iterable[ScanResult]
|
other results whose placeholders are legitimate here. |
()
|
Returns:
| Type | Description |
|---|---|
RestoredText
|
a :class: |
Source code in src/wardcat/core/models.py
reapply
¶
Re-anonymize this already-detected result under a different action.
scan() does the expensive detection (regex + NER + LLM) once; this
reuses those spans and re-runs only the cheap anonymization step, so you
can derive several outputs from a single scan without re-scanning or
reloading any model::
result = guard.scan(text)
masked = result.reapply(Action.MASK).sanitized_text
hashed = result.reapply(Action.HASH).sanitized_text
# re-anonymize only a subset of the detected types:
emails = result.reapply(Action.HASH, entities=["EMAIL"])
The salt from the originating :class:~wardcat.Wardcat is reused, so
hash output matches what the guard would have produced. Types passed
in entities that were not detected are simply absent (nothing to
anonymize). Concurrency-safe: nothing shared is mutated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
Action | str
|
the action to apply (an :class: |
required |
entities
|
Iterable[str] | None
|
optional subset of entity types to re-anonymize;
|
None
|
Returns:
| Type | Description |
|---|---|
ScanResult
|
a new :class: |
Raises:
| Type | Description |
|---|---|
ConfigError
|
if |
Source code in src/wardcat/core/models.py
wardcat.Violation
dataclass
¶
A single PII violation detected in the text.
action
instance-attribute
¶
Name of the action that was applied ("warn" / "hash" / "redact" /
"mask", or a custom registered action). Compares equal to the matching
:class:Action constant (v.action == Action.HASH).
replacement
class-attribute
instance-attribute
¶
What the original was replaced with — the hash/redact/mask output, or the
reversible tokenize placeholder. None for warn, which replaces nothing.
Together with :attr:original this is what :meth:ScanResult.restore reverses.
confidence
class-attribute
instance-attribute
¶
Detection confidence in [0.0, 1.0], tiered by how certain the match is:
- Checksum-validated regex (
TC_ID,IBAN,CREDIT_CARD):1.0— mathematically verified. - High-precision structural regex (email, JWT, IP, secrets, …):
0.97. - Fuzzy regex (
ADDRESS,VEHICLE_PLATE):0.90— a distinctive but ambiguous, heuristic match. - NER (SpaCy) / LLM:
0.85— model-based.
The engine resolves overlaps highest-confidence-first, so a regex span always beats a model guess, and in adjudication every regex span is protected while the model layers may be overridden. Use this field to threshold::
certain = [v for v in result.violations if v.confidence >= 1.0] # checksum only
wardcat.RedactedResult
¶
Bases: TypedDict
The PII-free dict returned by :meth:ScanResult.redacted (safe to log).
wardcat.RestoredText
dataclass
¶
Result of :meth:~wardcat.ScanResult.restore — the text plus its sources.
str(restored) is the text with the source list appended, which is usually
what you want to show::
answer = call_llm(result.sanitized_text)
print(result.restore(answer))
.. warning::
Contains raw PII in text and in every substitutions[].original.
text
instance-attribute
¶
The text with every unambiguous placeholder replaced by its original value.
substitutions
class-attribute
instance-attribute
¶
What was put back, ordered by first appearance in :attr:text.
unrestored
class-attribute
instance-attribute
¶
Detected values that were not put back, with the reason for each.
is_complete
property
¶
True when no placeholder was left sitting in the text.
False when something was skipped for being ambiguous or was
foreign — in both cases a placeholder is still there for a reader to
trip over. A not-present entry does not make a restore incomplete: an
answer that never mentions a placeholder has nothing to put back.
sources_block
¶
Render the ordered source list — which filter fired, and what it hid.
::
[1] [PERSON_1] → Ali Veli (PERSON · tokenize · x2 · confidence 0.85)
[2] [EMAIL_1] → ali@example.com (EMAIL · tokenize · confidence 0.97)
Returns "" when this text carries nothing to cite — nothing was put
back and no placeholder was left in it — so appending the block to a
clean answer adds nothing. A value that was masked but never mentioned in
the text is not, by itself, something to cite.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
title
|
str
|
heading for the block. |
_SOURCES_TITLE
|
notes
|
bool
|
also summarize the values that were not put back. |
True
|
Source code in src/wardcat/core/restore.py
with_sources
¶
:attr:text with :meth:sources_block appended below it.
Source code in src/wardcat/core/restore.py
wardcat.Substitution
dataclass
¶
One placeholder that was swapped back for its original value.
.. warning::
original is raw PII — the whole point of restoring — so a
:class:RestoredText is as sensitive as the input it came from.
wardcat.UnrestoredValue
dataclass
¶
A detected value that was not put back, and why.
Carries no raw value: not-present is the common, boring case (the answer
simply never mentioned that placeholder), and the caller can look the value
up in violations when it matters.
placeholder
instance-attribute
¶
None for report-only actions such as warn, which replace nothing.
reason
instance-attribute
¶
"not-present" — the placeholder does not occur in the text (an empty
replacement, which a custom action may return, occurs nowhere locatable);
"ambiguous" — it stood for more than one distinct value, so restoring it
would be a guess; "not-replaced" — the action left the original text in
place (warn), so there is nothing to reverse; "foreign" — the text
carries a placeholder this result never produced (another scan's, or one the
model invented), so it was left alone.
Exceptions¶
Everything wardcat raises derives from WardcatError, so one except catches
the lot. ConfigError and ModelDownloadError also subclass the built-in they
replaced (ValueError / RuntimeError), so existing handlers keep working.
wardcat.WardcatError
¶
Bases: Exception
Base class for every error raised by wardcat.
wardcat.ContextMismatch
¶
Bases: WardcatError
A text carries a reversible placeholder that belongs to a different scan.
Raised by ScanResult.restore(..., strict=True). Every scan stamps its own
context id into the placeholders it produces, so a token from another scan can
be recognized rather than silently matched — restoring one request's answer
against another request's result would otherwise substitute the wrong
person's values. Nothing is substituted for the foreign tokens either way;
strict only decides whether that is an error or a report.
placeholders lists the foreign tokens found.
Source code in src/wardcat/exceptions.py
wardcat.ConfigError
¶
Bases: WardcatError, ValueError
Invalid configuration (bad action, backend, pattern, entity spec, …).
Subclasses :class:ValueError for backward compatibility.
wardcat.ModelDownloadError
¶
Bases: WardcatError, RuntimeError
A SpaCy/LLM model could not be downloaded or is incompatible.
Subclasses :class:RuntimeError for backward compatibility.
wardcat.UnsupportedLanguageError
¶
Bases: ConfigError
The requested NER language (or size tier) has no compatible model.
Constants¶
wardcat.Entity
¶
Bases: str, Enum
Known entity types, as constants — for autocomplete and typo-proofing.
Use these instead of bare strings when configuring the guard::
from wardcat import Entity, Action
guard.add_entity(Entity.CREDIT_CARD, action=Action.HASH)
Each member is its string value (Entity.EMAIL == "EMAIL"), so it can be
used anywhere a plain entity-type string is accepted. Note: because this is a
(str, Enum), use .value to get the canonical string — str(Entity.EMAIL)
returns "Entity.EMAIL", not "EMAIL".
The special member :attr:Entity.ALL is a sentinel, not a real entity
type: passing it to :meth:~wardcat.Wardcat.add_entity /
:meth:~wardcat.Wardcat.remove_entity enables/disables every known entity
in one call. It is excluded from :data:KNOWN_ENTITY_TYPES. Entity.All
is a deprecated alias of Entity.ALL kept for backward compatibility.
wardcat.Action
¶
Bases: str, Enum
Action to apply to detected PII.
WARN
class-attribute
instance-attribute
¶
Leave the text as-is, report only as a violation.
HASH
class-attribute
instance-attribute
¶
Mask with SHA-256 + salt: [ENTITY_TYPE:abcd1234].
REDACT
class-attribute
instance-attribute
¶
Replace with a plain label: [ENTITY_TYPE] — no hash, no original value.
MASK
class-attribute
instance-attribute
¶
Partially obscure the value, entity-aware. Most types reveal only the last
few characters — e.g. CREDIT_CARD → ************1111,
EMAIL → u***@example.com, SSN → ***-**-6789. Types without a
specific rule fall back to first 2 + * + last 2 (abcdef → ab**ef);
values shorter than 4 characters are fully replaced with *.
See wardcat.core.actions._mask_value for the per-type rules.
TOKENIZE
class-attribute
instance-attribute
¶
Reversible masking: replace with a numbered placeholder — [PERSON_1],
[EMAIL_1], [PERSON_2] — numbered per entity type in order of appearance,
with one token per distinct value (a name repeated stays one referent). The
mapping lives on the :class:ScanResult, so :meth:ScanResult.restore can put
the real values back into whatever comes out the other side — an LLM's answer,
typically. Unlike hash/redact/mask this keeps the originals in
memory: the result object is as sensitive as the input.
wardcat.Backend
¶
Bases: str, Enum
LLM backend types, as constants — for typo-proof selection.
Pass these to :meth:Wardcat.with_llm instead of bare strings::
from wardcat import Wardcat, Backend
Wardcat(salt="s").with_llm(backend=Backend.OPENAI_COMPATIBLE, model="...")
Each member is its string value (Backend.OLLAMA == "ollama"), so the
plain string form is still accepted.
OLLAMA
class-attribute
instance-attribute
¶
Local Ollama service (supports model download).
OPENAI_COMPATIBLE
class-attribute
instance-attribute
¶
OpenAI-compatible HTTP API — LM Studio, LocalAI, LiteLLM, …
VLLM
class-attribute
instance-attribute
¶
vLLM server (OpenAI-compatible API) — native chat, vLLM defaults.
TRANSFORMERS
class-attribute
instance-attribute
¶
In-process HuggingFace Transformers (no HTTP; loads the model locally).
wardcat.Language
¶
Bases: str, Enum
Supported NER languages, as constants — for documented, typo-proof selection.
Pass these to Wardcat().with_ner(language=...) instead of bare ISO codes::
from wardcat import Wardcat, Language
Wardcat().with_ner(language=Language.EN) # one language
Wardcat().with_ner(language=[Language.EN, Language.DE]) # multilingual NER
Each member is its ISO 639-1 code (Language.EN == "en"), so the plain
string form is still accepted.