Skip to main content

Togul Python SDK

Official Python SDK for Togul feature flags.

Install

pip install togul

Framework integrations are extras:

pip install "togul[django]"
pip install "togul[fastapi]"

Quick start (sync)

"""Minimal synchronous usage."""

from __future__ import annotations

import os

from togul import Config, TogulClient

config = Config(
    api_key=os.environ["TOGUL_API_KEY"],
    environment=os.environ.get("TOGUL_ENVIRONMENT", "production"),
)

with TogulClient(config) as client:
    result = client.evaluate("new-dashboard", {"user_id": "u-123", "plan": "premium"})
    print(f"{result.flag_key}: enabled={result.enabled} value={result.value!r} ({result.reason})")

TogulClient is blocking. Use it from Django views, Celery tasks, scripts, or anywhere else that isn't running an event loop. The with block closes the underlying HTTP connection pool on exit; if you pass your own httpx.Client, the SDK leaves it open and you own its lifecycle.

See examples/basic.py.

Async

For FastAPI, Starlette, or any asyncio application, use AsyncTogulClient:

from togul import AsyncTogulClient, Config

config = Config(api_key="...", environment="production")

async with AsyncTogulClient(config) as client:
    result = await client.evaluate("new-dashboard", {"user_id": "u-123"})

The API mirrors TogulClient exactly, evaluate just needs an await. invalidate_cache() and invalidate_flag() stay synchronous on both clients: they only touch the in-memory cache under a lock, so there's nothing to await.

Real-time updates

Both clients cache evaluation results for cache_ttl seconds. A stream client keeps that cache fresh by invalidating entries as flags change, instead of waiting for the TTL to expire.

Sync, run on a background thread:

import threading

from togul import Config, TogulClient, TogulStreamClient

config = Config(api_key="...", environment="production")
client = TogulClient(config)
stream = TogulStreamClient(config, client.cache)

threading.Thread(target=stream.connect, daemon=True).start()
# ... use client.evaluate(...) as usual; stream.stop() to shut it down.

Async, run as a task:

import asyncio

from togul import AsyncTogulClient, AsyncTogulStreamClient, Config

config = Config(api_key="...", environment="production")
client = AsyncTogulClient(config)
stream = AsyncTogulStreamClient(config, client.cache)

task = asyncio.create_task(stream.connect())
# ... await client.evaluate(...) as usual; stream.stop() then cancel the task.

connect() never returns on its own. It reconnects with exponential backoff (starting at 1s, doubling up to a 30s cap, reset after a clean disconnect) until stop() is called. A 401 or 403 response is the one exception: it is re-raised out of connect() immediately, because reconnecting with a rejected key cannot fix anything.

The FastAPI integration below wires the async stream client into the application lifespan automatically; you don't need to manage the task by hand there.

Django

Add the middleware and a TOGUL settings block:

MIDDLEWARE = [
    # ... your other middleware ...
    "togul.contrib.django.TogulMiddleware",
]

TOGUL = {
    "API_KEY": os.environ["TOGUL_API_KEY"],
    "ENVIRONMENT": os.environ.get("TOGUL_ENVIRONMENT", "production"),
    "CACHE_TTL": 30.0,
    # "CONTEXT_BUILDER": "myapp.flags.build_context",
}
Key Default Notes
API_KEY required
ENVIRONMENT required
TIMEOUT 5.0 HTTP timeout in seconds.
CACHE_TTL 30.0 Seconds an evaluation stays cached.
RETRY_COUNT 2 Total request attempts (see below), not extra retries.
CONTEXT_BUILDER the built-in build_context Dotted path or callable; see below.

There is deliberately no BASE_URL key. This matches togul-laravel's config/togul.php, which exposes the same narrow set of framework-layer options. If you need a non-production origin, construct a Config directly (see examples/basic.py) rather than going through Django settings.

TogulMiddleware attaches request.togul (a TogulClient) and request.togul_context (built by CONTEXT_BUILDER, or the default build_context if unset) to every request. The default build_context maps only the authenticated user's primary key to user_id, and omits it entirely for an anonymous request rather than sending a blank value. If your flags need anything else — a plan, a country, an experiment cohort — point CONTEXT_BUILDER at your own function; don't edit the default.

def dashboard(request):
    result = request.togul.evaluate("new-dashboard", request.togul_context)
    if result.enabled:
        return render(request, "dashboard/new.html")
    return render(request, "dashboard/legacy.html")

For gating an entire view, use the @togul_flag("key") decorator instead. It raises Http404 when the flag is disabled, and works with or without the middleware installed (it builds context on demand if request.togul_context isn't there):

from togul.contrib.django import togul_flag


@togul_flag("new-dashboard")
def beta_dashboard(request):
    return render(request, "dashboard/new.html")

See examples/django_settings.py.

FastAPI

from fastapi import Depends, FastAPI

from togul import AsyncTogulClient, Config
from togul.contrib.fastapi import create_lifespan, get_togul

config = Config(api_key="...", environment="production")

app = FastAPI(lifespan=create_lifespan(config))


@app.get("/dashboard")
async def dashboard(
    user_id: str,
    togul: AsyncTogulClient = Depends(get_togul),
) -> dict:
    result = await togul.evaluate("new-dashboard", {"user_id": user_id})
    return {"variant": "new" if result.enabled else "legacy", "reason": result.reason}

create_lifespan(config, *, stream=True, client=None) builds an AsyncTogulClient, stores it on app.state.togul, and — since stream defaults to True — starts an AsyncTogulStreamClient as a background task so SSE events invalidate the cache in real time. Both are shut down cleanly on application exit. Pass client= to supply your own pre-built AsyncTogulClient (for tests, or a custom httpx.AsyncClient); the lifespan still closes it on shutdown. Pass stream=False to skip the background connection and rely on cache_ttl alone.

get_togul is a plain Depends() provider that reads app.state.togul.

See examples/fastapi_app.py.

Configuration reference

Every field of Config:

Field Type Default Meaning
api_key str required Environment-scoped API key.
environment str required Environment key, e.g. "production".
timeout float 5.0 HTTP request timeout, in seconds.
cache_ttl float 30.0 How long an evaluation result stays cached, in seconds.
retry_count int 2 Total request attempts, not extra retries — 2 means the SDK issues at most two requests, i.e. one retry.
base_url str "https://api.togul.io" API origin. Trailing slashes are stripped.

Config is a frozen dataclass: build a new one to change settings.

evaluate() is the only evaluation method — there are no typed evaluate_string / evaluate_boolean helpers. Read result.value and interpret it according to result.value_type.

Caching and invalidation

Both clients keep an in-memory, thread-safe TTL cache of EvaluateResults. An entry is dropped lazily, the first time it's read after cache_ttl seconds have passed, or if it carries a malformed (empty) value_type.

The cache key is flag_key:environment, followed by :key=value for every context pair, with context keys sorted so call order never changes the key. For example, evaluate("new-dashboard", {"plan": "premium", "user_id": "u-123"}) against environment production produces new-dashboard:production:plan=premium:user_id=u-123.

invalidate_cache() clears the whole cache; invalidate_flag(flag_key) clears only entries for that flag. A TogulStreamClient / AsyncTogulStreamClient calls these automatically as SSE events arrive. A line that is not a well-formed data: JSON object — a heartbeat, a blank line, or a malformed payload — is ignored entirely: no invalidation, no flush. A well-formed event naming a flag_key invalidates just that flag; a well-formed event that omits it flushes the whole cache. Register on_cache_invalidated(...) on the stream client to observe SSE-driven invalidations, e.g. for logging — the stream client invalidates the cache directly and only notifies its own listeners, so it never routes through the evaluation client. A listener registered on the evaluation client instead fires only for invalidations you trigger by calling that client's own invalidate_cache() / invalidate_flag() directly; it will not fire for SSE events.

Error handling

Every error the SDK raises is a TogulError. TogulAPIError is the subclass raised when the API itself returns a non-2xx response; it carries status_code and, when the response body has one, error_code.

Retries apply only to HTTP 429 and any 5xx response, plus network-level failures. Every other 4xx (400, 401, 403, 404, ...) raises immediately with no retry. Each retry waits attempt × 100ms before trying again, so with the default retry_count of 2 the single retry is delayed by 100ms. When retries are exhausted, the client raises a TogulError wrapping the last failure.

There is no fail-open fallback. Unlike SDKs that silently return a default value when evaluation fails, this SDK never guesses on your behalf: a failed evaluate() call raises, and it's up to your code to decide what happens next — return a default, log and re-raise, fall back to cached behavior, whatever fits your product. If you want fail-open behavior, wrap evaluate() in a try/except TogulError yourself.

Requirements

Python 3.9+. Django integration requires Django 4.2+. FastAPI integration requires FastAPI 0.100+.

License

MIT. See LICENSE.

Metadata

Release files for togul 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for togul 0.1.0
File Size Uploaded
togul-0.1.0.tar.gz 24.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for togul 0.1.0
File Interpreter ABI Platform
togul-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.5 kB

Release files / togul-0.1.0.tar.gz

Download URL togul-0.1.0.tar.gz
Size 24.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5e6ba5f2c536afa911de4cf0c18217c63115a4f507ba7d1ed01503b9a8018575
BLAKE2b-256 checksum
How to use checksums
d75990c9da864e5ce81edaab83a21b5b6a93e2ea26be4c7ac3ee41d5188068d1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / togul-0.1.0-py3-none-any.whl

Download URL togul-0.1.0-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d56673509d191778b69f7a19a6febe0da8dd82e09292c28217f18e5fac70097c
BLAKE2b-256 checksum
How to use checksums
83514d28fe111b2642fa89276b9a7e41a69611ed164ce14f670fb00446cbdf91
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page