Wardcat¶
The main interface. Everything below is generated from the source docstrings.
wardcat.Wardcat
¶
Bases: EntityPolicyMixin
The main interface exposed to users.
Programmatic API (method chaining)::
import os
from wardcat import Wardcat, Entity, Action
guard = (
# Read secrets from the environment in YOUR app — the library itself
# never reads env vars; pass everything explicitly.
Wardcat(salt=os.environ["WARDCAT_SALT"])
.add_entity(Entity.EMAIL, action=Action.HASH)
.add_entity(Entity.CREDIT_CARD, action=Action.HASH)
.remove_entity(Entity.ORG)
)
result = guard.scan(text)
Enable everything, then prune::
guard = Wardcat(salt="...").add_entity(Entity.ALL, action="hash")
guard.remove_entity(Entity.ORG)
guard.entity_policy() # inspect: {"CREDIT_CARD": "hash", ...}
Declarative API (YAML)::
guard = Wardcat(config_path="config/my_policy.yaml")
result = guard.scan(text)
Configuration is explicit. The constructor takes only salt and an optional
YAML config_path; every detection layer is configured with a fluent
builder — :meth:with_ner and :meth:with_llm — or in the YAML file. The
library does not read environment variables: read any secrets in your own
application and hand them to the constructor. Builders are chainable and their
order does not matter — the final configuration is what counts::
from wardcat import Wardcat, Language
# LLM layer
guard = Wardcat(salt="s").with_llm(model="llama3.1:8b")
# NER layer — needs an explicit model (wardcat ships no default). Choose
# one via language= (recommended) or spacy_model=:
guard = Wardcat(salt="s").with_ner(language=Language.DE, spacy_size="md")
guard = Wardcat(salt="s").with_ner(language=[Language.DE, Language.FR])
guard = Wardcat(salt="s").with_ner(spacy_model=["en_core_web_sm", "de_core_news_sm"])
# Supported languages: en, de, fr, es, it, nl, pt, tr; sizes sm/md/lg/trf.
# A named-but-missing model is auto-downloaded (auto_download=False to disable).
# For mixed-language text without extra models, use the LLM layer, whose
# prompt is multilingual.
Source code in src/wardcat/guard.py
scan
¶
scan_async
async
¶
Async scan — uses native async I/O for the LLM backend when available.
CPU-bound detectors (regex, SpaCy NER) run in a thread pool;
the LLM detector (if enabled) uses httpx.AsyncClient natively,
so multiple concurrent calls do not block each other.
Source code in src/wardcat/guard.py
is_sensitive
¶
Return whether text contains sensitive information, judged semantically.
A general, holistic LLM decision — not the per-entity detection of
:meth:scan. It asks the configured LLM whether the text as a whole
contains sensitive information (PII, credentials, financial, health, or
confidential business data) and returns a single True/False flag.
Useful as a lightweight guardrail before sending text to an external
service.
Requires the LLM layer (:meth:with_llm); no entities need to be enabled
and no regex/NER runs. Empty text is False. Fail-closed: if the LLM
backend is unreachable the underlying error propagates, so a guardrail
never silently treats sensitive text as safe.
Raises:
| Type | Description |
|---|---|
ConfigError
|
if the LLM layer is not configured. |
Source code in src/wardcat/guard.py
is_sensitive_async
async
¶
Async variant of :meth:is_sensitive (native async LLM I/O when available).
Source code in src/wardcat/guard.py
scan_batch
¶
Scan multiple texts in parallel using a thread pool.
Each text is scanned independently; an error in a single item does not affect the others — the original text is returned untouched for any item that fails.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
List of texts to scan |
required |
max_workers
|
int | None
|
Number of parallel threads. Defaults to the
|
None
|
Returns:
| Type | Description |
|---|---|
list[ScanResult]
|
List of |
Source code in src/wardcat/guard.py
scan_batch_async
async
¶
Scan multiple texts concurrently using native async.
Each text is scanned independently via :meth:scan_async; all are
run concurrently with asyncio.gather. Errors in individual items
are caught — the original text is returned with scan_error set.
Source code in src/wardcat/guard.py
supported_entities
staticmethod
¶
Return the entity types wardcat can detect (discoverability helper).
::
Wardcat.supported_entities() # every known entity type
Wardcat.supported_entities("regex") # only what the regex layer detects
Wardcat.supported_entities("ner") # PERSON, ORG, ADDRESS
Wardcat.supported_entities("llm") # contextual/semantic types
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer
|
str | None
|
|
None
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
if |
Source code in src/wardcat/guard.py
with_ner
¶
Enable the SpaCy NER layer with an explicit model. Supports chaining.
Mirrors :meth:with_llm. Pass language= (recommended; a list enables
multilingual NER) or spacy_model= (explicit package name(s)).
::
guard = Wardcat(salt="s").with_ner(language=Language.EN)
guard = Wardcat(salt="s").with_ner(spacy_model=["en_core_web_sm", "de_core_news_sm"])
Raises:
| Type | Description |
|---|---|
ConfigError
|
if neither |
Source code in src/wardcat/guard.py
with_llm
¶
with_llm(
*,
backend=OLLAMA,
model="llama3.2",
base_url=None,
api_key="",
timeout=60,
allow_http=False,
adjudicate=False,
auto_pull=False,
device_map="auto",
load_in_8bit=False,
load_in_4bit=False,
dtype=None,
language=None,
)
Enable the on-prem LLM detector. Supports chaining, like :meth:with_ner.
.. warning::
Unlike with_ner(), this does not leave detection fully opt-in.
The LLM layer carries its own default entity policy, so with_llm()
switches on around fifteen entity types with the actions that policy
names (PERSON → hash, EMAIL → warn, …) — not the action
you pass to :meth:add_entity for something else. They are listed in a
one-time warning at the first scan. Override one with
add_entity(name, action, layers=["llm"]) or drop it with
remove_entity(name); pass a YAML config_path to replace the
policy wholesale.
The fluent way to configure the LLM layer (the constructor takes only
config_path and salt) — keeps the LLM configuration in one place::
guard = (
Wardcat(salt="s")
.with_ner(language=Language.TR)
.with_llm(backend=Backend.OLLAMA, model="llama3.2", adjudicate=True)
)
backend is the backend type (:class:~wardcat.Backend); the
address goes to base_url.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str | None
|
the backend's address. Leave it unset to use the
backend's own default — |
None
|
dtype
|
str | None
|
weight dtype for the |
None
|
language
|
str | Language | None
|
selects a localized system prompt for :meth: |
None
|
Source code in src/wardcat/guard.py
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 | |
with_phone_regions
¶
Detect national phone formats for regions via libphonenumber.
The built-in PHONE pattern is precision-first and covers Turkish,
French and German national formats plus E.164 — a number written the way
it is written in Manchester or Madrid falls through it. Naming the regions
you actually serve swaps in libphonenumber for those formats::
guard = Wardcat(salt=s).add_entity(Entity.PHONE).with_phone_regions("GB", "ES")
Regions are CLDR two-letter codes. Needs the extra: pip install
'wardcat[phone]' — without it the built-in pattern is used and a warning
is logged. Call with no arguments to go back to the pattern.
Matches are reported at 0.90 confidence, not the 0.97 of the built-in pattern: libphonenumber validates against each region's numbering plan, which is far stronger than a bare digit run but weaker than a checksum, and every extra region widens what counts as a number. Add the regions you serve, not every region there is.
Source code in src/wardcat/guard.py
with_min_confidence
¶
Set the confidence floor: spans scoring below minimum are dropped.
Every detection carries a confidence, tiered by how strong the evidence is — a checksum-verified card is 1.0, a distinctive format such as an email is 0.97, a model layer is 0.85, a keyword-heuristic address is 0.90, and a checksum whose own odds are weak (the ABA mod-10, the NHS mod-11, the IMEI Luhn) with no supporting keyword nearby is 0.70.
The default floor is 0.8, which sits between that last tier and
everything else: those uncued matches are found but not acted on. Lower
it to trade precision for recall::
guard.with_min_confidence(0.6) # act on uncued checksum matches too
Set it to 0 to act on everything a layer reports.
Raises:
| Type | Description |
|---|---|
ConfigError
|
if minimum is not a number between 0 and 1. |
Source code in src/wardcat/guard.py
set_salt
¶
add_allowlist
¶
Add exact values that should never be flagged as PII.
Supports method chaining::
guard.add_allowlist(["no-reply@company.com", "192.168.1.1"])
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
list[str]
|
List of exact string values to exempt from detection. |
required |
Source code in src/wardcat/guard.py
add_denylist
¶
Add values that should always be flagged as PII.
Each entry must have a value key and an entity_type key.
The action applied is taken from the entity's config (same as
regular detections). Supports method chaining::
guard.add_denylist([
{"value": "John Smith", "entity_type": "PERSON"},
{"value": "ProjectSecret", "entity_type": "CUSTOM_SECRET"},
])
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
list[dict[str, str]]
|
List of dicts with |
required |
Source code in src/wardcat/guard.py
with_propagation
¶
Redact every occurrence of a value once any layer detects it.
Model-based layers (NER/LLM) often report a repeated value only once, which would leave the other occurrences unredacted. With propagation on, a value detected anywhere is anonymized at every whole-token occurrence in the text — using that value's entity type and action. Deterministic regex spans still win overlaps, so a propagated match never displaces a checksum-validated one. Chainable::
guard = Wardcat(salt="s").with_ner().add_entity("PERSON").with_propagation()
It can over-redact (e.g. a short common name), so it is off by default
and only exact, token-bounded matches at least min_length chars long
are propagated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
enabled
|
bool
|
turn propagation on (default) or off. |
True
|
min_length
|
int
|
skip values shorter than this many characters. |
3
|