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_forkhook, or just skip--preloadso 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_initsignal.
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.