Skip to content

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

discard(challenge_id: str) -> None

Remove a challenge without checking it (e.g. on user cancel).

issue async

issue(key: str = 'global') -> tuple[str, bytes]

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:RateLimiter rejected this key.

verify async

verify(challenge_id: str, user_input: str) -> bool

Check a user's answer.

Returns:

Type Description
bool

True if the answer matches (the challenge is then deleted),

bool

False if the answer is wrong but the user still has attempts left.

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.

check

check(user_input: str) -> bool

Constant-time comparison against the expected solution.

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; None uses Pillow's default.

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

cleanup_expired() -> int

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 at challenge.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.

cleanup_expired async

cleanup_expired() -> int

Delete rows whose expires_at is in the past. Returns count.

create_schema async

create_schema() -> None

Create the challenges table and index if they don't exist.

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.

acquire async

acquire(key: str) -> None

Permit one request from key, or raise :class:RateLimited.

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

Bases: ChallengeError

The challenge has passed its expiration time.

ChallengeNotFound

Bases: ChallengeError

The referenced challenge id does not exist in storage.

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.

TooManyAttempts

Bases: ChallengeError

The allowed number of verification attempts has been exceeded.