Gutter
Fully featured Python feature switch toolkit
Install / Use
/learn @disqus/GutterREADME
.. image:: https://api.travis-ci.org/disqus/gutter.png?branch=master :target: http://travis-ci.org/disqus/gutter
Gutter
NOTE: This repo is the client for Gargoyle 2, known as "Gutter". It does not work with the existing Gargoyle 1 codebase <https://github.com/disqus/gargoyle/>_.
Gutter is feature switch management library. It allows users to create feature switches and setup conditions those switches will be enabled for. Once configured, switches can then be checked against inputs (requests, user objects, etc) to see if the switches are active.
For a UI to configure Gutter with see the gutter-django project <https://github.com/disqus/gutter-django>_
Table of Contents
- Configuration_
- Setup_
- Arguments_
Switches_Conditions_Checking Switches as Active_- Signals_
- Namespaces_
- Decorators_
Testing Utilities_
Configuration
Gutter requires a small bit of configuration before usage.
Choosing Storage
Switches are persisted in a ``storage`` object, which is a `dict` or any object which provides the ``types.MappingType`` interface (``__setitem__`` and ``__getitem__`` methods). By default, ``gutter`` uses an instance of `MemoryDict` from the `durabledict library <https://github.com/disqus/durabledict>`_. This engine **does not persist data once the process ends** so a more persistent data store should be used.
Autocreate
~~~~~~~~~~
``gutter`` can also "autocreate" switches. If ``autocreate`` is enabled, and ``gutter`` is asked if the switch is active but the switch has not been created yet, ``gutter`` will create the switch automatically. When autocreated, a switch's state is set to "disabled."
This behavior is off by default, but can be enabled through a setting. More on "settings" below.
Configuring Settings
To change the storage and/or autocreate settings, simply import the settings module and set the appropriate variables:
.. code:: python
from gutter.client.settings import manager as manager_settings
from durabledict.dict import RedisDict
from redis import RedisClient
manager_settings.storage_engine = RedisDict('gutter', RedisClient()))
manager_settings.autocreate = True
In this case, we are changing the engine to durabledict's RedisDict and turning on autocreate. These settings will then apply to all newly constructed Manager instances. More on what a Manager is and how you use it later in this document.
Setup
Once the Manager's storage engine has been configured, you can import gutter's default Manager object, which is your main interface with gutter:
.. code:: python
from gutter.client.default import gutter
At this point the gutter object is an instance of the Manager class, which holds all methods to register switches and check if they are active. In most installations and usage scenarios, the gutter.client.gutter manager will be your main interface.
Using a different default Manager
If you would like to construct and use a different default manager, but still have it accessible via ``gutter.client.gutter``, you can construct and then assign a ``Manager`` instance to ``settings.manager.default`` value:
.. code:: python
from gutter.client.settings import manager as manager_settings
from gutter.client.models import Manager
manager_settings.default = Manager({}) # Must be done before importing the default manager
from gutter.client.default import gutter
assert manager_settings.default is gutter
.. WARNING::
:warning::warning:
Note that the ``settings.manager.default`` value must be set **before** importing the default ``gutter`` instance.
:warning::warning:
Arguments
=========
The first step in your usage of ``gutter`` should be to define your arguments that you will be checking switches against. An "argument" is an object which understands the business logic and object in your system (users, requests, etc) and knows how to validate, transform and extract variables from those business objects for ``Switch`` conditions. For instance, your system may have a ``User`` object that has properties like ``is_admin``, ``date_joined``, etc. To switch against it, you would then create arguments for each of those values.
To do that, you construct a class which inherits from ``gutter.client.arguments.Container``. Inside the body of the class, you create as many class variable "arguments" that you need by using the ``gutter.client.arguments`` function.
.. code:: python
from gutter.client import arguments
from myapp import User
class UserArguments(arguments.Container):
COMPATIBLE_TYPE = User
name = arguments.String(lambda self: self.input.name)
is_admin = arguments.Boolean(lambda self: self.input.is_admin)
age = arguments.Value(lambda self: self.input.age)
There are a few things going on here, so let's break down what they all mean.
1. The ``UserArgument`` class is subclassed from ``Container``. The subclassing is required since ``Container`` implements some of the required API.
2. The class has a bunch of class variables that are calls to ``arguments.TYPE``, where ``TYPE`` is the type of variable this argument is. At present there are 3 types: ``Value`` for general values, ``Boolean`` for boolean values and ``String`` for string values.
3. ``arguments.TYPE()`` is called with a callable that returns the value. In the above example, we'll want to make some switches active based on a user's ``name``, ``is_admin`` status and ``age``.
4. Those callables return the actual value, which is derefenced from ``self.input``, which is the input object (in this case a ``User`` instance).
5. ``Variable`` objects understand ``Switch`` conditions and operators, and implement the correct API to allow themselves to be appropriately compared.
6. ``COMPATIBLE_TYPE`` declares that this argument only works with ``User`` instances. This works with the default implementation of ``applies`` in the base argument that checks if the ``type`` of the input is the same as ``COMPATIBLE_TYPE``.
Since constructing arguments that simply reference an attribute on ``self.input`` is so common, if you pass a string as the first argument of ``argument()``, when the argument is accessed, it will simply return that property from ``self.input``. You must also pass a ``Variable`` to the ``variable=`` kwarg so gutter know what Variable to wrap your value in.
.. code:: python
from gutter.client import arguments
from myapp import User
class UserArguments(Container):
COMPATIBLE_TYPE = User
name = arguments.String('name')
is_admin = arguments.Boolean('is_admin')
age = arguments.Value('age')
Rationale for Arguments
~~~~~~~~~~~~~~~~~~~~~~~
You might be asking, why have these ``Argument`` objects at all? They seem to just wrap an object in my system and provide the same API. Why can't I just use my business object **itself** and compare it against my switch conditions?
The short answer is that ``Argument`` objects provide a translation layer to translate your business objects into objects that ``gutter`` understands. This is important for a couple reasons.
First, it means you don't clutter your business logic/objects with code to support ``gutter``. You declare all the arguments you wish to provide to switches in one location (an Argument) whose single responsibility it to interface with ``gutter``. You can also construct more savvy Argument objects that may be the combination of multiple business objects, consult 3rd party services, etc. All still not cluttering your main application code or business objects.
Secondly, and most importantly, Arguments return ``Variable`` objects, which ensure ``gutter`` conditions work correctly. This is mostly relevant to the percentage-based operators, and is best illustrated with an example.
Imagine you have a ``User`` class with an ``is_vip`` boolean field. Let's say you wanted to turn on a feature for only 10% of your VIP customers. To do that, you would write a condition that says, "10% of the time when I'm called with the variable, I should be true." That line of code would probably do something like this:
.. code:: python
return 0 <= (hash(variable) % 100) < 10
The issue is that if ``variable = True``, then ``hash(variable) % 100`` will always be the same value for **every** ``User`` with ``is_vip`` of ``True``:
.. code:: python
>>> hash(True)
1
>>> hash(True) % 100
1
This is because in Python `True` objects always have the same hash value, and thus the percentage check doesn't work. This is not the behavior you want.
For the 10% percentage range, you want it to be active for 10% of the inputs. Therefore, each input must have a unique hash value, exactly the feature the ``Boolean`` variable provides. Every ``Variable`` has known characteristics against conditions, while your objects may not.
That said, you don't absolutely **have** to use ``Variable`` objects. For obvious cases, like ``use.age > some_value`` your ``User`` instance will work just fine, but to play it safe you should use ``Variable`` objects. Using ``Variable`` objects also ensure that if you update ``gutter`` any new ``Operator`` types that are added will work correctly with your ``Variable``s.
Switches
============================================
Switches encapsulate the concept of an item that is either 'on' or 'off' depending on the input. The swich determines its on/off status by checking each of its ``conditions`` and seeing if it applies to a certain input.
Switches are constructed with only one required argument, a ``name``:
.. code:: python
from gutter.client.models import Switch
switch = Switch('my cool feature')
Switches can be in 3 core states: ``GLOBAL``, ``DISABLED`` and ``SELECTIVE``. In t
