API Reference¶
Manager¶
captchakit.manager.CaptchaManager
dataclass
¶
Coordinates a :class:ChallengeFactory, :class:Renderer and :class:Storage.
Typical usage::
manager = CaptchaManager(
factory=TextChallengeFactory(),
renderer=ImageRenderer(),
storage=MemoryStorage(),
ttl=120.0,
)
challenge_id, png = await manager.issue()
...
ok = await manager.verify(challenge_id, user_input)
discard
async
¶
Remove a challenge without checking it (e.g. on user cancel).
issue
async
¶
Create and persist a fresh challenge, return (id, image_bytes).
key identifies the caller for rate limiting — typically an IP
address, user id or session token. With the default
:class:NoOpRateLimiter the value is ignored.
Raises:
| Type | Description |
|---|---|
RateLimited
|
The configured :class: |
verify
async
¶
Check a user's answer.
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
|
Raises:
| Type | Description |
|---|---|
ChallengeNotFound
|
No such challenge (already consumed, never issued, or evicted by TTL). |
ChallengeExpired
|
The challenge passed its expiration time. |
TooManyAttempts
|
This was the final attempt and it was wrong. |
Challenges¶
captchakit.challenges.base.Challenge
dataclass
¶
A complete captcha challenge stored by the manager.
captchakit.challenges.base.ChallengeSpec
dataclass
¶
A challenge's raw content before an id/TTL is attached.
Produced by a :class:ChallengeFactory; the :class:~captchakit.CaptchaManager
wraps it into a :class:Challenge with identifier and timestamps.
captchakit.challenges.base.ChallengeFactory ¶
Bases: Protocol
Produces fresh :class:ChallengeSpec instances on demand.
captchakit.challenges.text.TextChallengeFactory
dataclass
¶
Produces random alphanumeric strings.
The default charset omits visually ambiguous characters
(I, O, 0, 1) so a user isn't penalised for font choice.
captchakit.challenges.math.MathChallengeFactory
dataclass
¶
Produces easy arithmetic puzzles such as "7 + 3 = ?".
Results of subtractions are always non-negative: operands are swapped when needed so the user never has to deal with negative numbers.
captchakit.challenges.grid.EmojiGridChallengeFactory
dataclass
¶
Produces a single-pick emoji-grid challenge.
One target emoji is placed in a randomly chosen cell; the remaining
cells are filled with distractors drawn from emoji_pool. The user's
expected answer is the 1-indexed cell number as a string (e.g. "3").
captchakit.challenges.word.WordChallengeFactory
dataclass
¶
Produces a single random word from a wordlist.
The default list avoids visually / phonetically ambiguous words and
ships ~50 common English nouns. Pass your own words tuple to
localise or narrow the pool.
Renderers¶
captchakit.renderers.base.Renderer ¶
Bases: Protocol
Turns a :class:Challenge into bytes (typically an image).
captchakit.renderers.image.ImageRenderer
dataclass
¶
Renders a challenge's prompt into a PNG.
The renderer is intentionally simple: it draws the prompt glyph-by-glyph with small random rotations / offsets and a few decorative lines. It is not meant to defeat OCR — it's a light human-check layer (see SECURITY notes in the README).
Pass a :class:Theme to switch visual style; built-in presets are
Theme.CLASSIC (default), Theme.DARK, Theme.PASTEL and
Theme.HIGH_CONTRAST.
captchakit.renderers.svg.SVGRenderer
dataclass
¶
Renders a challenge's prompt into an SVG byte-string.
Like :class:ImageRenderer, this is a lightweight human-check —
not an OCR-defeating captcha. Combine with rate-limiting.
captchakit.renderers.theme.Theme
dataclass
¶
Visual style for the image renderer.
Attributes:
| Name | Type | Description |
|---|---|---|
bg_color |
RGB
|
Canvas background colour (RGB). |
palette |
tuple[RGB, ...]
|
Glyph and noise-line colours. At least one entry. |
noise_lines |
int
|
Random decorative lines drawn behind the glyphs. |
font_path |
str | Path | None
|
Optional TrueType font path; |
captchakit.renderers.audio.AudioRenderer
dataclass
¶
Renders a challenge's solution into a WAV byte-stream.
Each character of the solution becomes tone_ms of a sine wave at
the frequency mapped in tone_map; tones are separated by
gap_ms of silence. Output is mono 16-bit PCM at sample_rate
samples per second.
The renderer is not meant to defeat bots — it is an accessibility alternative. Pair it with rate-limiting at the edge.
Storage¶
captchakit.storage.base.Storage ¶
Bases: Protocol
Persists challenges while they are live.
Implementations MUST be safe under concurrent async access from a single event loop. Multi-process safety is the backend's own responsibility (e.g. Redis is multi-process-safe; in-memory is not).
captchakit.storage.memory.MemoryStorage
dataclass
¶
Dict-based storage with a single :class:asyncio.Lock.
Suitable for single-process deployments and tests. For multi-process
setups (e.g. gunicorn with multiple workers) use a shared backend such as
:class:~captchakit.storage.redis.RedisStorage (optional extra).
cleanup_expired
async
¶
Remove every challenge whose expires_at is in the past.
Returns the number of entries evicted. Safe to call from a background task; intended to be scheduled periodically by the host application.
captchakit.storage.redis.RedisStorage
dataclass
¶
Stores challenges in Redis using native TTL.
Each challenge uses two keys:
<prefix>:ch:<id>— JSON-encoded challenge, expires atchallenge.expires_at.<prefix>:att:<id>— attempt counter, same expiry.
incr_attempts is a best-effort atomic check — it returns 0 if the
challenge key was already evicted (by TTL or explicit delete) at the
moment of the call.
captchakit.storage.postgres.PostgresStorage
dataclass
¶
Stores challenges in a single Postgres table.
pool must be an already-connected :class:asyncpg.Pool. Call
:meth:create_schema once at application startup to create the
table and index (idempotent — safe to run on every boot).
wall_clock_offset is a compatibility shim: the captcha manager's
:class:Clock is monotonic by default, but Postgres stores wall-clock
timestamps. The offset is captured on the first put and used to
translate monotonic timestamps into UTC datetimes consistently.
Rate limiting¶
captchakit.ratelimit.RateLimiter ¶
Bases: Protocol
Decides whether an issue call is allowed to proceed.
Implementations MUST be safe under concurrent async access from a single event loop. Multi-process safety is the backend's own responsibility.
captchakit.ratelimit.NoOpRateLimiter ¶
Default limiter — every call is permitted.
captchakit.ratelimit.TokenBucketRateLimiter
dataclass
¶
Classic token-bucket limiter, in-memory.
Each key gets capacity tokens that refill at refill_rate
tokens per second. acquire consumes one token; if none are
available, :class:RateLimited is raised with a retry_after
hint.
Not multi-process safe — use the Redis variant when running more than one worker.
i18n¶
captchakit.i18n.PromptTranslator ¶
Bases: Protocol
Maps (key, locale, **params) → a rendered string.
captchakit.i18n.DefaultTranslator
dataclass
¶
In-memory translator with English, Turkish, German and Spanish.
Pass catalog to override or extend the bundled strings::
translator = DefaultTranslator(
catalog={"fr": {"grid.pick": "Quelle case contient {emoji} ?"}},
)
Keys missing in the requested locale fall back to default_locale
("en"); keys missing everywhere raise :class:KeyError.
Metrics¶
captchakit.metrics.MetricsSink ¶
Bases: Protocol
Receives captchakit lifecycle events.
Every callback is synchronous and must not block: it runs on the event loop thread. If you need to do I/O (push to a remote aggregator, write to disk), buffer locally and flush from a background task.
captchakit.metrics.NoOpMetrics ¶
Default sink — all methods are no-ops.
Errors¶
captchakit.errors ¶
Exception hierarchy for captchakit.
CaptchaKitError ¶
Bases: Exception
Base class for all captchakit exceptions.
ChallengeError ¶
Bases: CaptchaKitError
Base for errors that reference a specific challenge id.
Catch this to handle :class:ChallengeNotFound, :class:ChallengeExpired
and :class:TooManyAttempts uniformly.
ChallengeExpired ¶
ChallengeNotFound ¶
RateLimited ¶
Bases: CaptchaKitError
The rate limiter rejected an issue call.
Thrown by :class:~captchakit.CaptchaManager.issue when the
configured :class:~captchakit.ratelimit.RateLimiter reports that
the caller (identified by key) has exceeded its quota.
StorageError ¶
Bases: CaptchaKitError
Underlying storage backend failed.