Skip to main content

Feature Flagging for Flask

Project description

flask-pancake

GitHub Workflow Status (branch) Codecov branch PyPI

Feature Flagging for Flask

This library was heavily inspired by django-waffle.

Installation

flask-pancake depends on Redis and the flask-redis Python package.

$ python -m pip install flask-pancake
Successfully installed flask-pancake
from flask import Flask
from flask_pancake import FlaskPancake, Switch
from flask_redis import FlaskRedis

app = Flask(__name__)
app.secret_key = "s3cr!t"
pancake = FlaskPancake(app)
redis = FlaskRedis(app)

SWITCH_FEATURE = Switch("FEATURE", default=False)


@app.route("/")
def index():
    if SWITCH_FEATURE.is_active():
        return "Hello World!", 200
    else:
        return "Not found", 404

Alternatively, if you use a create_app() method to configure your Flask app, use pancake.init_app():

from flask import Flask
from flask_pancake import FlaskPancake

pancake = FlaskPancake()


def create_app() -> Flask:
    app = Flask(__name__)
    app.secret_key = "s3cr!t"
    pancake.init_app(app)
    return app

Usage

flask-pancake provides three types of flags:

  • Switches, which are either globally active or inactive. A common use case for these are system-wide enabling or disabling of a feature. E.g. in the context of a dependency on a third party service, disabling a feature with a global switch when that service is unavailable.

  • Flags are like Switches but can be overridden for individual groups. To make use of Flags, one needs to define at least one function that returns a group's unique ID or None. Groups can be anything that you want users to be grouped by: their user ID (which would allow per-user enabling/disabling of features), a user's attribute, such as "is_superuser" or "is_staff", or anything else that you can think of.

    The groups are tried in order. The first one to match will be used. Meaning, more specific functions should be defined first, less specific functions last.

    from flask import request
    from flask_pancake import FlaskPancake
    
    def get_group_user():
        # If the `request` object has a `user` attribute and the `user` object
        # has a `uid` attribute, return that.
        return getattr(getattr(request, "user", None), "uid", None)
    
    def get_group_superuser():
        # If the `request` object has a `user` attribute and the `user` object
        # has an `is_superuser` attribute, return "y" if that is boolean `True`
        # or "n" if it isn't.
        return getattr(getattr(request, "user", None), "is_superuser", None) and "y" or "n"
    
    # Alternatively, instead of using `get_group_superuser()` one can use a
    # slightly more verbose class-based approach which has the added benefit
    # of adding additional value to the flask-pancake overview API view (see
    # below).
    class IsSuperuser(GroupFunc):
        def __call__(self) -> str:
            return getattr(getattr(request, "user", None), "is_superuser", None) and "y" or "n"
    
        def get_candidate_ids(self) -> List[str]:
            return ["yes", "no"]
    
    pancake = FlaskPancake(
        group_funcs={"user": get_group_user, "superuser": get_group_superuser}
        # alternatively if using the class-based approach:
        # group_funcs={"user": get_group_user, "superuser": IsSuperuser}
    )
    # Or, if importing a function from somewhere isn't possible, a string based
    # approach can be used.
    # Separate the the fully qualified module path from the function with a `:`
    pancake = FlaskPancake(
        group_funcs={
            "user", "my.app.account.utils:get_group_user",
            "superuser", "my.app.account.utils:get_group_superuser",
            # alternatively if using the class-based approach:
            "superuser", "my.app.account.utils:IsSuperuser",
        }
    )
    

    In the example, whenever one checks for a Flag, FlaskPancake would check if a value has been set in the following order:

    1. Is the flag disable/enable for the current user?
    2. If not, is the flag disable/enabled for superusers/non-superusers?
    3. If not, is the flag disable/enabled by default?
  • Samples, have a global "ratio" of 0 - 100%. On the first check of a sample in a request, a random value is checked within these bounds. If it's lower or equal the set value, it's active, if it's larger, it's inactive.

    Due to the randomness, samples store their state in a request context (Flask's g context object). Additionally, in order to provide consistent behavior for a user between requests, the values of the used samples in a request are stored in a cookie in the user's browser. They are then loaded on the next request again and thus provide a stable behavior across requests.

    That means, despite the randomness involved, this behavior is actually safe:

    def foo():
        if MY_SAMPLE.is_active():
            # do something
            pass
        ...
        if MY_SAMPLE.is_active():
            # do more
            pass
    

The persisted state for all three types of feature flags can be cleared, using the clear() method.

Similarly, one can change the persisted state for Flags and Switches using their disable() and enable() methods. Samples can be updated using their set(value: float) method.

When using Flags, there are clear_group(group_id) and clear_all_group(group_id) methods, to clear the state for the current or all users within a group. Along the same line, there are disable_group(group_id) and enable_group(group_id) to set the group's state the current user is part of.

Web API

flask-pancake provides an API endpoint that shows all available Flags, Samples and Switches and their corresponding states under the /overview route within the blueprint. It also provides a JSON API to get the status for all feature flags for the current user under the /status route. The APIs can be enabled by registering a Flask blueprint:

from flask import Flask
from flask_pancake import FlaskPancake, blueprint

app = Flask(__name__)
app.secret_key = "s3cr!t"
pancake = FlaskPancake(app)
app.register_blueprint(blueprint, url_prefix="/pancakes")

WARNING: The API is not secured in any way! You should use Flask's Blueprint.before_request() feature to add some authentication for the /overview endpoint. Check the complex_app.py for an example.

NOTE: The /status API endpoint is meant to be used by front-end applications to load the status of all Flags, Samples and Switches and have them readily available in the front-end. If one does not want to expose some feature flags to the front-end via the /status endpoint, separate the feature flags into two FlaskPancake extension instances and only allow access to the /status endpoint serving the front-end feature flags.

As noted above, Samples store their state in cookies between requests. The cookie name defaults to the name of the extension, but can be set explicitly using the cookie_name argument when instantiating the FlaskPancake() extension. The same goes for the cookie options: by default, cookies will be set with the HttpOnly and SameSite=Lax attributes. The cookie options are passed through to Werkzeug's set_cookie() method:

from flask import Flask
from flask_pancake import FlaskPancake

app = Flask(__name__)
app.secret_key = "s3cr!t"
pancake = FlaskPancake(
    app,
    name="feature-flags",
    cookie_name="ff",
    cookie_options={"httponly": True, "samesite": "Lax", "secure": True},
)

Command Line Interface

flask-pancake comes with a CLI that hooks into Flask's own CLI. The same way you can call flask run to start your application in development mode you can call flask pancake. Here are some examples:

$ flask pancake
Usage: flask pancake [OPTIONS] COMMAND [ARGS]...

  Commands to manage flask-pancake flags, samples, and switches.

Options:
  --help  Show this message and exit.

Commands:
  flags
  samples
  switches

$ flask pancake flags list
DO_SOMETHING_ELSE: Yes (default: Yes)
FOO_CAN_DO: No (default: No)

$ flask pancake flags enable FOO_CAN_DO
Flag 'FOO_CAN_DO' enabled.

$ flask pancake flags list
DO_SOMETHING_ELSE: Yes (default: Yes)
FOO_CAN_DO: Yes (default: No)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flask-pancake-0.5.2.tar.gz (27.0 kB view hashes)

Uploaded Source

Built Distribution

flask_pancake-0.5.2-py3-none-any.whl (14.1 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page