Skip to main content

furagu Python SDK

furagu feature flags for Python servers. This is an OpenFeature provider that downloads your environment's ruleset once and evaluates every flag in-process. Publishes reach it over a WebSocket in about a second, with polling as the fallback. It comes with integrations for Django, FastAPI, Flask and any other WSGI or ASGI app.

The provider runs the same evaluation engine as the furagu API, so a server evaluating locally and a browser asking the API get the same answer for the same context.

pip install furagu          # or: uv add furagu

Quickstart

import os

from openfeature import api
from openfeature.evaluation_context import EvaluationContext

from furagu import FuraguProvider

api.set_provider_and_wait(FuraguProvider(os.environ["FURAGU_SERVER_KEY"]))

client = api.get_client()
on = client.get_boolean_value("checkout-v2", False, EvaluationContext("user-8231", {"plan": "pro"}))
details = client.get_boolean_details("checkout-v2", False, EvaluationContext("user-8231"))
details.value, details.reason, details.variant, details.error_code

api.shutdown()  # on exit: closes the stream, stops polling, flushes usage counts (at most 2 s)

You need a server key (furagu_srv_...), and it is a secret. Every get_*_value or get_*_details call is an in-memory lookup that takes microseconds and never touches the network. The _async variants exist for async code and resolve the same way.

OpenFeature has five value types and furagu has four flag types. They map like this:

Flag type Client method Note
boolean get_boolean_*
string get_string_*
number get_integer_* JSON integers only; 42.0 or 0.5 is a TYPE_MISMATCH
number get_float_* Any finite JSON number, returned as a float
json get_object_* A JSON object or array

Framework integrations

Each integration installs the provider when the app starts and shuts it down when the app exits. It also sets the OpenFeature transaction context for the current request, which is what lets a view call get_client().get_boolean_value("flag", False) without passing a context. If you do pass one, its attributes win.

Django

# settings.py
INSTALLED_APPS = [..., "furagu.contrib.django"]
MIDDLEWARE = [
    ...,
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "furagu.contrib.django.FuraguMiddleware",
]
FURAGU = {"SDK_KEY": os.environ["FURAGU_SERVER_KEY"]}
from furagu.contrib.django import get_client


def checkout(request):
    return JsonResponse({"v2": get_client().get_boolean_value("checkout-v2", False)})

By default the targeting key is the authenticated user's primary key, or anonymous, and the attributes are authenticated, path and method. To build your own, set FURAGU_CONTEXT = "myapp.flags.context", a dotted path or callable that takes the request and returns an EvaluationContext, a dict, or None.

The FURAGU dict takes every provider option in upper case (BASE_URL, POLL_INTERVAL, EVENTS, STREAMING and so on). Three extra keys are Django-specific: WAIT blocks startup until the ruleset is downloaded (default True), DOMAIN names an OpenFeature client domain, and ENABLED turns the whole thing off, which is handy in tests and one-off management commands.

Django REST Framework

DRF authenticates inside the view, after Django's middleware has run. Session-authenticated requests are already covered by the middleware above, but for token, JWT or Basic authentication the middleware sees an anonymous user. Add the mixin in front of your DRF base class and the context is set again once DRF has authenticated:

from rest_framework.views import APIView
from furagu.contrib.drf import FuraguContextMixin


class CheckoutView(FuraguContextMixin, APIView):
    def get(self, request):
        return Response({"v2": get_client().get_boolean_value("checkout-v2", False)})

It works with APIView, generic views and viewsets, uses the same default context and FURAGU_CONTEXT builder as plain Django views, and runs before permission classes so those can evaluate flags too. If you prefer explicit contexts, furagu.contrib.drf.context_for(request) builds one from an authenticated DRF request.

FastAPI / Starlette

from fastapi import Depends, FastAPI
from openfeature.client import OpenFeatureClient

from furagu import FuraguProvider
from furagu.contrib.fastapi import get_client, install

app = FastAPI()
install(app, FuraguProvider(os.environ["FURAGU_SERVER_KEY"]))


@app.get("/checkout")
async def checkout(flags: OpenFeatureClient = Depends(get_client)):
    return {"v2": flags.get_boolean_value("checkout-v2", False)}

install() wraps the app's lifespan, composing with one you already have, and adds the ASGI middleware. Pass context_from_request= to derive the targeting key from your own auth. Without it the context is anonymous with path and method, or the identity of Starlette's request.user if you run AuthenticationMiddleware. If you would rather wire things yourself, furagu.contrib.fastapi.lifespan(provider) works directly with FastAPI(lifespan=...).

Flask

from flask import Flask
from furagu.contrib.flask import Furagu

app = Flask(__name__)
app.config["FURAGU_SDK_KEY"] = os.environ["FURAGU_SERVER_KEY"]
furagu = Furagu(app)


@app.get("/checkout")
def checkout():
    return {"v2": furagu.client.get_boolean_value("checkout-v2", False)}

Config keys are the provider options with a FURAGU_ prefix (FURAGU_BASE_URL, FURAGU_POLL_INTERVAL and so on), plus FURAGU_WAIT and FURAGU_ENABLED. The app-factory pattern works too: Furagu(provider=..., context_from_request=...) now, init_app(app) later.

Any ASGI or WSGI app

from furagu.contrib.asgi import FuraguMiddleware  # Litestar, Quart, Django ASGI, raw Starlette, ...

app = FuraguMiddleware(app, provider=FuraguProvider(key), context_from_scope=lambda scope: {...})

from furagu.contrib.wsgi import FuraguMiddleware  # any PEP 3333 app

application = FuraguMiddleware(application, provider=FuraguProvider(key))

The ASGI middleware installs the provider on lifespan.startup and shuts it down on lifespan.shutdown. WSGI has no startup hook, so that middleware installs the provider when it is constructed. Both take a context builder that returns an EvaluationContext, a dict with targeting_key plus attributes, or None.

Options

FuraguProvider(
    sdk_key,
    base_url="https://api.furagu.dev",
    poll_interval=30.0,  # seconds between GET /v1/ruleset polls (jittered ±10%)
    events=True,  # report evaluation counts so the dashboard shows flag usage
    events_flush_interval=30.0,  # seconds between batched count uploads (jittered ±10%)
    streaming=True,  # hold a WebSocket and fetch a new ruleset the moment one is published
    streaming_poll_interval=900.0,  # fallback poll interval while the socket is up
    initial_ruleset=None,  # a Ruleset or its JSON shape to serve before the first download
    transport=None,  # a custom HTTP transport (proxies with special needs, tests)
    logger=None,  # defaults to logging.getLogger("furagu")
)

Requests to the API time out after 5 seconds. Polls never overlap, and they are free: the only metered call is POST /v1/evaluations, which this SDK does not make.

Streaming

With streaming=True (the default) the provider keeps a WebSocket open to GET /v1/stream. The API announces new versions on it and the provider fetches them right away with GET /v1/ruleset?v=N, so a publish reaches your servers in about a second. Polling continues underneath at streaming_poll_interval in case a message is missed. If the socket drops, polling goes back to poll_interval and runs once immediately, while the stream reconnects with exponential backoff from 1 second up to 5 minutes.

Streaming is worth turning off for processes that live a few seconds (functions, cron jobs, CLIs), for networks that block WebSockets, and for very large fleets where a thousand open sockets per environment is a real number.

Bootstrap

provider.current_ruleset is the ruleset being served. Save current_ruleset.to_json() somewhere and pass it back as initial_ruleset= on the next start. With a bootstrap in hand, a failed first download no longer fails initialization. The provider comes up STALE, serves the bootstrap, and recovers on the first successful poll.

Status and events

Status Meaning
READY Serving a ruleset downloaded from the API
STALE The last refresh failed (network, 5xx, timeout). The previous ruleset is still served and polling keeps trying
FATAL The key was rejected with a 401 or 403, for example because it was revoked or it is a client key. Polling, streaming and reporting all stop
ERROR The first download failed and there was no bootstrap, so set_provider_and_wait raised

The provider emits PROVIDER_READY, PROVIDER_STALE, PROVIDER_ERROR and PROVIDER_CONFIGURATION_CHANGED. The last one carries the keys of the flags whose version changed. Subscribe with api.add_handler(...), and keep in mind that the OpenFeature SDK runs handlers on its own thread pool.

Reasons and errors

furagu OpenFeature reason error_code
flag disabled DISABLED
individual target matched TARGETING_MATCH
rule matched (flag_metadata["ruleId"] names it) TARGETING_MATCH
fallthrough, fixed variation DEFAULT
fallthrough, percentage rollout SPLIT
unknown flag key ERROR FLAG_NOT_FOUND
malformed flag ERROR GENERAL (the message names the problem)
value of another type ERROR TYPE_MISMATCH
no ruleset loaded yet ERROR PROVIDER_NOT_READY

Evaluation never raises. Every error case hands back the default you passed in.

Context

EvaluationContext(targeting_key, attributes) maps onto the furagu context: targeting_key becomes key, which is what individual targets and percentage rollouts bucket on, and attributes pass through for rules to match. A missing targeting key becomes "anonymous". Attribute types are strict, as in every furagu SDK: "30" does not match 30, and True does not match 1.

Deployment

Threads and forking

The provider runs four small daemon threads: poll, stream, refresh and event reporting. Threads do not survive fork(), so create and install the provider after the fork.

  • gunicorn: install in a post_fork hook, or just skip --preload so each worker imports the app itself.
  • uvicorn with --workers N: each worker imports the app, so lifespan-based installation already happens after the fork.
  • Celery: install from the worker_process_init signal.

If you forget, the provider has a safety net: an os.register_at_fork hook restarts its threads in the child. Pending usage counts are dropped in that case, the ruleset snapshot is kept.

Shutdown

api.shutdown() closes the stream, stops polling and flushes pending counts, all within 2 seconds. The provider also registers an atexit hook so a plain script flushes on exit without being asked.

Proxies

The default transport honours HTTPS_PROXY, HTTP_PROXY and NO_PROXY. The WebSocket goes through the websockets library's own proxy support.

Performance

An evaluation reads one reference to an immutable ruleset snapshot and walks it in memory. It never waits for the network or for a background refresh.

The one cost worth knowing about is parsing a newly downloaded ruleset. CPython's JSON decoder holds the GIL while it builds objects, so request threads pause for that parse once per publish per process. That is about 2 ms for a typical ruleset and up to about 25 ms at the 4 MiB limit. pip install furagu[orjson] roughly halves it, and free-threaded Python builds have no pause at all.

Logging

Everything logs under the furagu logger. The SDK key is never logged.

Development

uv sync --group dev
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run pytest --cov

See CONTRIBUTING.md. Bugs and feature requests go to GitHub issues; for security reports see SECURITY.md.

License

MIT © 2026 furagu

Metadata

Release files for furagu 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 furagu 0.1.0
File Size Uploaded Provenance Actions
furagu-0.1.0.tar.gz 77.7 kB With provenance Details for furagu-0.1.0.tar.gz

Built distribution (wheel)

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

Total release size: 131.4 kB

Release files / furagu-0.1.0.tar.gz

Download URL furagu-0.1.0.tar.gz
Size 77.7 kB
Tags Source
SHA-256 checksum
How to use checksums
97ab38e3040918902034f8403159851c8f773a87a5e5e760f6b48aac43aa90d5
BLAKE2b-256 checksum
How to use checksums
0aca6ffd12b32c133f912a4ca241d4c2ac75725e8c9eeabbdbabb55d5b701b48
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 22, 2026.

Transparency log

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

Download URL furagu-0.1.0-py3-none-any.whl
Size 53.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
90c177505b7713062eb41d4b28db6fcd1e91b55f1584b4abbeffcce8e2b91efc
BLAKE2b-256 checksum
How to use checksums
0534f05b0c79440f7fb24b6a2dae2ebd2d67639cb6a6ddb10b42a05030164799
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 22, 2026.

Transparency log

Security for furagu 0.1.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for furagu-0.1.0.tar.gz

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 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

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