{ "info": { "author": "DISQUS", "author_email": "opensource@disqus.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Operating System :: OS Independent", "Topic :: Software Development" ], "description": ".. image:: https://api.travis-ci.org/disqus/gutter.png?branch=master\n :target: http://travis-ci.org/disqus/gutter\n\nGutter\n------\n\n**NOTE:** This repo is the client for Gargoyle 2, known as \"Gutter\". It does not work with the existing `Gargoyle 1 codebase `_.\n\nGutter 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.\n\nFor a UI to configure Gutter with see the `gutter-django project `_\n\nTable of Contents\n=================\n\n* Configuration_\n* Setup_\n* Arguments_\n* `Switches`_\n* `Conditions`_\n* `Checking Switches as Active`_\n* Signals_\n* Namespaces_\n* Templates_\n* Decorators_\n* `Testing Utilities`_\n\nConfiguration\n=============\n\nGutter requires a small bit of configuration before usage.\n\nChoosing Storage\n~~~~~~~~~~~~~~~~\n\nSwitches 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 `_. This engine **does not persist data once the process ends** so a more persistent data store should be used.\n\nAutocreate\n~~~~~~~~~~\n\n``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.\"\n\nThis behavior is off by default, but can be enabled through a setting. More on \"settings\" below.\n\nConfiguring Settings\n~~~~~~~~~~~~~~~~~~~~\n\nTo change the ``storage`` and/or ``autocreate`` settings, simply import the settings module and set the appropriate variables:\n\n.. code:: python\n\n from gutter.client.settings import manager as manager_settings\n from durabledict.dict import RedisDict\n from redis import RedisClient\n\n manager_settings.storage_engine = RedisDict('gutter', RedisClient()))\n manager_settings.autocreate = True\n\nIn 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.\n\nSetup\n=====\n\nOnce the ``Manager``'s storage engine has been configured, you can import gutter's default ``Manager`` object, which is your main interface with ``gutter``:\n\n.. code:: python\n\n from gutter.client.default import gutter\n\nAt 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.\n\nUsing a different default Manager\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf 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:\n\n.. code:: python\n\n from gutter.client.settings import manager as manager_settings\n from gutter.client.models import Manager\n\n manager_settings.default = Manager({}) # Must be done before importing the default manager\n\n from gutter.client.default import gutter\n\n assert manager_settings.default is gutter\n\n.. WARNING::\n\n :warning::warning:\n Note that the ``settings.manager.default`` value must be set **before** importing the default ``gutter`` instance.\n :warning::warning:\n\nArguments\n=========\n\nThe 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.\n\nTo 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.\n\n.. code:: python\n\n from gutter.client import arguments\n\n from myapp import User\n\n class UserArguments(arguments.Container):\n\n COMPATIBLE_TYPE = User\n\n name = arguments.String(lambda self: self.input.name)\n is_admin = arguments.Boolean(lambda self: self.input.is_admin)\n age = arguments.Value(lambda self: self.input.age)\n\nThere are a few things going on here, so let's break down what they all mean.\n\n1. The ``UserArgument`` class is subclassed from ``Container``. The subclassing is required since ``Container`` implements some of the required API.\n2. 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.\n3. ``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``.\n4. Those ``argument``s return the actual value, which is derefenced from ``self.input``, which is the input object (in this case a ``User`` instance).\n5. ``Variable`` objects understand ``Switch`` conditions and operators, and implement the correct API to allow themselves to be appropriately compared.\n6. ``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``.\n\nSince 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.\n\n.. code:: python\n\n from gutter.client import arguments\n\n from myapp import User\n\n class UserArguments(Container):\n\n COMPATIBLE_TYPE = User\n\n name = arguments.String('name')\n is_admin = arguments.Boolean('name')\n age = arguments.Value('name')\n\n\nRationale for Arguments\n~~~~~~~~~~~~~~~~~~~~~~~\n\nYou 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?\n\nThe 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.\n\nFirst, 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.\n\nSecondly, 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.\n\nImagine 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:\n\n.. code:: python\n\n return 0 <= (hash(variable) % 100) < 10\n\nThe issue is that if ``variable = True``, then ``hash(variable) % 100`` will always be the same value for **every** ``User`` with ``is_vip`` of ``True``:\n\n.. code:: python\n\n >>> hash(True)\n 1\n >>> hash(True) % 100\n 1\n\nThis 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.\n\nFor 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.\n\nThat 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.\n\nSwitches\n============================================\n\nSwitches 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.\n\nSwitches are constructed with only one required argument, a ``name``:\n\n.. code:: python\n\n from gutter.client.models import Switch\n\n switch = Switch('my cool feature')\n\nSwitches can be in 3 core states: ``GLOBAL``, ``DISABLED`` and ``SELECTIVE``. In the ``GLOBAL`` state, the Switch is enabled for every input no matter what. ``DISABLED`` Switches are not **disabled** for any input, no matter what. ``SELECTIVE`` Switches enabled based on their conditions.\n\nSwitches can be constructed in a certain state or the property can be changed later:\n\n.. code:: python\n\n switch = Switch('new feature', state=Switch.states.DISABLED)\n another_switch = Switch('new feature')\n another_switch.state = Switch.states.DISABLED\n\nCompounded\n~~~~~~~~~~\n\nWhen in the ``SELECTIVE`` state, normally only one condition needs be true for the Switch to be enabled for a particular input. If ``switch.compounded`` is set to ``True``, then **all** of the switches conditions need to be true in order to be enabled::\n\n switch = Switch('require alll conditions', compounded=True)\n\nHeriarchical Switches\n~~~~~~~~~~~~~~~~~~~~~\n\nYou can create switches using a specific hierarchical naming scheme. Switch namespaces are divided by the colon character (\":\"), and hierarchies of switches can be constructed in this fashion:\n\n.. code:: python\n\n parent = Switch('movies')\n child1 = Switch('movies:star_wars')\n child2 = Switch('movies:die_hard')\n grandchild = Switch('movies:star_wars:a_new_hope')\n\nIn the above example, the ``child1`` switch is a child of the ``\"movies\"`` switch because it has ``movies:`` as a prefix to the switch name. Both ``child1`` and ``child2`` are \"children of the parent ``parent`` switch. And ``grandchild`` is a child of the ``child1`` switch, but *not* the ``child2`` switch.\n\nConcent\n~~~~~~~\n\nBy default, each switch makes its \"am I active?\" decision independent of other switches in the Manager (including its parent), and only consults its own conditions to check if it is enabled for the input. However, this is not always the case. Perhaps you have a cool new feature that is only available to a certain class of user. And of *those* users, you want 10% to be be exposed to a different user interface to see how they behave vs the other 90%.\n\n``gutter`` allows you to set a ``concent`` flag on a switch that instructs it to check its parental switch first, before checking itself. If it checks its parent and it is not enabled for the same input, the switch immediately returns ``False``. If its parent *is* enabled for the input, then the switch will continue and check its own conditions, returning as it would normally.\n\nFor example:\n\n.. code:: python\n\n parent = Switch('cool_new_feature')\n child = Switch('cool_new_feature:new_ui', concent=True)\n\nFor example, because ``child`` was constructed with ``concent=True``, even if ``child`` is enabled for an input, it will only return ``True`` if ``parent`` is **also** enabled for that same input.\n\n**Note:** Even switches in a ``GLOBAL`` or ``DISABLED`` state (see \"Switch\" section above) still consent their parent before checking themselves. That means that even if a particular switch is ``GLOBAL``, if it has ``concent`` set to ``True`` and its parent is **not** enabled for the input, the switch itself will return ``False``.\n\nRegistering a Switch\n~~~~~~~~~~~~~~~~~~~~\n\nOnce your ``Switch`` is constructed with the right conditions, you need to register it with a ``Manager`` instance to preserve it for future use. Otherwise it will only exist in memory for the current process. Register a switch via the ``register`` method on a ``Manager`` instance:\n\n.. code:: python\n\n gutter.register(switch)\n\nThe Switch is now stored in the Manager's storage and can be checked if active through ``gutter.active(switch)``.\n\nUpdating a Switch\n~~~~~~~~~~~~~~~~~\n\nIf you need to update your Switch, simply make the changes to the ``Switch`` object, then call the ``Manager``'s ``update()`` method with the switch to tell it to update the switch with the new object:\n\n.. code:: python\n\n switch = Switch('cool switch')\n manager.register(switch)\n\n switch.name = 'even cooler switch' # Switch has not been updated in manager yet\n\n manager.update(switch) # Switch is now updated in the manager\n\nSince this is a common pattern (retrieve switch from the manager, then update it), gutter provides a shorthand API in which you ask the manager for a switch by name, and then call ``save()`` on the **switch** to update it in the ``Manager`` it was retreived from:\n\n.. code:: python\n\n switch = manager.switch('existing switch')\n switch.name = 'a new name' # Switch is not updated in manager yet\n switch.save() # Same as calling manager.update(switch)\n\nUnregistering a Switch\n~~~~~~~~~~~~~~~~~~~~~~\n\nExisting switches may be removed from the Manager by calling ``unregister()`` with the switch name or switch instance:\n\n.. code:: python\n\n gutter.unregister('deprecated switch')\n gutter.unregister(a_switch_instance)\n\n**Note:** If the switch is part of a hierarchy and has children switches (see the \"Hierarchical Switches\" section above), all descendent switches (children, grandchildren, etc) will also be unregistered and deleted.\n\n\nConditions\n==========\n\nEach Switch can have 0+ conditions, which describe the conditions under which that switch is active. ``Condition`` objects are constructed with three values: a ``argument``, ``attribute`` and ``operator``.\n\nAn ``argument`` is any ``Argument`` class, like the one you defined earlier. From the previous example, ``UserArgument`` is an argument object. ``attribute`` is the attribute on a argument instance that you want this condition to check. ``operator`` is some sort of check applied against that attribute. For instance, is the ``UserArgument.age`` greater than some value? Equal to some value? Within a range of values? Etc.\n\nLet's say you wanted a ``Condition`` that checks if the user's age is > 65 years old? You would construct a Condition that way:\n\n.. code:: python\n\n from gutter.client.operators.comparable import MoreThan\n\n condition = Condition(argument=UserArgument, attribute='age', operator=MoreThan(65))\n\nThis Condition will be true if any input instance has an ``age`` that is more than ``65``.\n\nPlease see the ``gutter.operators`` for a list of available operators.\n\nConditions can also be constructed with a ``negative`` argument, which negates the condition. For example:\n\n.. code:: python\n\n from gutter.client.operators.comparable import MoreThan\n\n condition = Condition(argument=UserArgument, attribute='age', operator=MoreThan(65), negative=True)\n\nThis Condition is now ``True`` if the condition evaluates to ``False``. In this case if the user's ``age`` is **not** more than ``65``.\n\nConditions then need to be appended to a switch instance like so:\n\n.. code:: python\n\n switch.conditions.append(condition)\n\nYou can append as many conditions as you would like to a switch, there is no limit.\n\nChecking Switches as Active\n===========================\n\nAs stated before, switches are checked against input objects. To do this, you would call the switch's ``enabled_for()`` method with a ``User`` instance, for instance. You may call ``enabled_for()`` with any input object, it will ignore inputs for which it knows nothing about. If the ``Switch`` is active for your input, ``enabled_for`` will return ``True``. Otherwise, it will return ``False``.\n\n``gutter.active()`` API\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nA common use case of gutter is to use it during the processing of a web request. During execution of code, different code paths are taken depending on if certain switches are active or not. Often times there are multiple switches in existence at any one time and they all need to be checked against multiple arguments. To handle this use case, Gutter provides a higher-level API.\n\nTo check if a ``Switch`` is active, simply call ``gutter.active()`` with the Switch name:\n\n.. code:: python\n\n gutter.active('my cool feature')\n >>> True\n\nThe switch is checked against some number of input objects. Inputs can be added to the ``active()`` check one of two ways: locally, passed in to the ``active()`` call or globally, configured ahead of time.\n\nTo check against local inputs, ``active()`` takes any number of input objects after the switch name to check the switch against. In this example, the switch named ``'my cool feature'`` is checked against input objects ``input1`` and ``input2``:\n\n.. code:: python\n\n gutter.active('my cool feature', input1, input2)\n >>> True\n\nIf you have global input objects you would like to use for every check, you can set them up by calling the Manager's ``input()`` method:\n\n.. code:: python\n\n gutter.input(input1, input2)\n\nNow, ``input1`` and ``input2`` are checked against for every ``active`` call. For example, assuming ``input1`` and ``input2`` are configured as above, this ``active()`` call would check if the Switch was enabled for inputs ``input1``, ``input2`` and ``input3`` in that order::\n\n gutter.active('my cool feature', input3)\n\nOnce you're doing using global inputs, perhaps at the end of a request, you should call the Manager's ``flush()`` method to remove all the inputs:\n\n.. code:: python\n\n gutter.flush()\n\nThe Manager is now setup and ready for its next set of inputs.\n\nWhen calling ``active()`` with a local inputs, you can skip checking the ``Switch`` against the global inputs and **only** check against your locally passed in inputs by passing ``exclusive=True`` as a keyword argument to ``active()``:\n\n.. code:: python\n\n gutter.input(input1, input2)\n gutter.active('my cool feature', input3, exclusive=True)\n\nIn the above example, since ``exclusive=True`` is passed, the switch named ``'my cool feature'`` is **only** checked against ``input3``, and not ``input1`` or ``input2``. The ``exclusive=True`` argument is not persistent, so the next call to ``active()`` without ``exclusive=True`` will again use the globally defined inputs.\n\nSignals\n=======\n\nGutter provides 4 total signals to connect to: 3 about changes to Switches, and 1 about errors applying Conditions. They are all available from the ``gutter.signals`` module\n\nSwitch Signals\n~~~~~~~~~~~~~~\nThere are 3 signals related to Switch changes:\n\n1. ``switch_registered`` - Called when a new switch is registered with the Manager.\n2. ``switch_unregistered`` - Called when a switch is unregistered with the Manager.\n3. ``switch_updated`` - Called with a switch was updated.\n\nTo use a signal, simply call the signal's ``connect()`` method and pass in a callable object. When the signal is fired, it will call your callable with the switch that is being register/unregistered/updated. I.e.:\n\n.. code:: python\n\n from gutter.client.signals import switch_updated\n\n def log_switch_update(switch):\n Syslog.log(\"Switch %s updated\" % switch.name)\n\n switch_updated.connect(log_switch_updated)\n\nUnderstanding Switch Changes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe ``switch_updated`` signal can be connected to in order to be notified when a switch has been changed. To know *what* changed in the switch, you can consult its ``changes`` property:\n\n.. code:: python\n\n >>> from gutter.client.models import Switch\n >>> switch = Switch('test')\n >>> switch.concent\n True\n >>> switch.concent = False\n >>> switch.name = 'new name'\n >>> switch.changes\n {'concent': {'current': False, 'previous': True}, 'name': {'current': 'new name', 'previous': 'test'}}\n\nAs you can see, when we changed the Switch's ``concent`` setting and ``name``, ``switch.changes`` reflects that in a dictionary of changed properties. You can also simply ask the switch if anything has changed with the ``changed`` property. It returns ``True`` or ``False`` if the switch has any changes as all.\n\nYou can use these values inside your signal callback to make decisions based on what changed. I.e., email out a diff only if the changes include changed conditions.\n\nCondition Application Error Signal\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWhen a ``Switch`` checks an input object against its conditions, there is a good possibility that the ``Argument`` value may be some sort of unexpected value, and can cause an exception. Whenever there is an exception raised during ``Condition`` checking itself against an input, the ``Condition`` will catch that exception and return ``False``.\n\nWhile catching all exceptions is generally bad form and hides error, most of the time you do not want to fail an application request just because there was an error checking a switch condition, *especially* if there was an error during checking a ``Condition`` for which a user would not have applied in the first place.\n\nThat said, you would still probably want to know if there was an error checking a Condition. To accomplish this, ``gutter``-client provides a ``condition_apply_error`` signal which is called when there was an error checking a ``Condition``. The signal is called with an instance of the condition, the input which caused the error and the instance of the Exception class itself:\n\n.. code:: python\n\n signals.condition_apply_error.call(condition, inpt, error)\n\nIn your connected callback, you can do whatever you would like: log the error, report the exception, etc.\n\nNamespaces\n==========\n\n``gutter`` allows the use of \"namespaces\" to group switches under a single umbrella, while both not letting one namespace see the switches of another namespace, but allowing them to share the same storage instance, operators and other configuration.\n\nGiven an existing vanilla ``Manager`` instance, you can create a namespaced manager by calling the ``namespaced()`` method:\n\n.. code:: python\n\n notifications = gutter.namespaced('notifications')\n\nAt this point, ``notifications`` is a copy of ``gutter``, inheriting all of its:\n\n* storage\n* ``autocreate`` setting\n* Global inputs\n* Operators\n\nIt does **not**, however, share the same switches. Newly constructed ``Manager`` instances are in the ``default`` namespace. When ``namespaced()`` is called, ``gutter`` changes the manager's namespace to ``notifications``. Any switches in the previous ``default`` namespace are not visible in the ``notifications`` namespace, and vice versa.\n\nThis allows you to have separate namespaced \"views\" of switches, possibly named the exact same name, and not have them conflict with each other.\n\nDecorators\n==========\n\nGutter features a ``@switch_active`` decorator you can use to decorate your Django views. When decorated, if the switch named as the first argument of the ``@switch_decorated`` decorator is False, a ``Http404`` exception is raised. However, if you also pass a ``redirect_to=`` kwarg, the decorator will return a ``HttpResponseRedirect`` instance, redirecting to that location. If the switch is active, then the view runs as normal.\n\nFor example, here is a view decorated with ``@switch_active``:\n\n\n.. code:: python\n\n from gutter.client.decorators import switch_active\n\n @switch_active('cool_feature')\n def my_view(request):\n return 'foo'\n\nAs stated above, if the ``cool_feature`` switch is inactive, this view will raise a ``Http404`` exception.\n\nIf, however, the decorator was constructed with a ``redirect_to=`` kwarg:\n\n.. code:: python\n\n @switch_active('cool_feature', redirect_to=reverse('upsell-page'))\n\nThen a ``HttpResponseRedirect`` instance will be returned, redirecting to ``reverse('upsell-page')``.\n\nTesting Utilities\n===============\n\nIf you would like to test code that uses ``gutter`` and have the ``gutter`` manager return predictable results, you can use the ``switches`` object from the ``testutils`` module.\n\nThe ``swtiches`` object can be used as both a context manager and a decorator. It is passed ``kwargs`` of switch names and their``active`` return values.\n\nFor instance, with this code here, by passing ``cool_feature=True`` to the ``switches`` object as a context manager, any call to ``gutter.active('cool_feature')`` will return ``True``. Calls to ``active()`` with other switch names will return their actual live switch status:\n\n.. code:: python\n\n from gutter.client.testutils import switches\n from gutter.client.default import gutter\n\n with switches(cool_feature=True):\n gutter.active('cool_feature') # True\n\n\nAnd when using ``switches`` as a decorator:\n\n.. code:: python\n\n from gutter.client.testutils import switches\n from gutter.client.default import gutter\n\n @switches(cool_feature=True)\n def run(self):\n gutter.active('cool_feature') # True\n\nAdditionally, you may pass an alternate ``Manager`` instance to ``switches`` to use that manager instead of the default one:\n\n.. code:: python\n\n from gutter.client.testutils import switches\n from gutter.client.models import Manager\n\n my_manager = Manager({})\n\n @switches(my_manager, cool_feature=True)\n def run(self):\n gutter.active('cool_feature') # True\n", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://github.com/disqus/gutter", "keywords": null, "license": "Apache License 2.0", "maintainer": null, "maintainer_email": null, "name": "gutter", "package_url": "https://pypi.org/project/gutter/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/gutter/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://github.com/disqus/gutter" }, "release_url": "https://pypi.org/project/gutter/0.5.0/", "requires_dist": null, "requires_python": null, "summary": "Client to gutter feature switches backend", "version": "0.5.0" }, "last_serial": 3962828, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "1d4c5a0b3051533ad5451d1265351d1f", "sha256": "bd31f7f1cb21355f9bf1053a6c22e7a28e74180949c717e1c2b3804b7bc64310" }, "downloads": -1, "filename": "gutter-0.1.0.tar.gz", "has_sig": false, "md5_digest": "1d4c5a0b3051533ad5451d1265351d1f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 17696, "upload_time": "2012-12-28T19:49:05", "url": "https://files.pythonhosted.org/packages/91/d3/2fcd04ed6d697a3e1e74391f25def50422042292169e64e9ab42f6f0fbef/gutter-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "8398d7f7fc3b95c80f150e444ee4b269", "sha256": "b834054a1db5759123afdfc40fd581a33017531de5e04a132be45b55dd2966ec" }, "downloads": -1, "filename": "gutter-0.1.1.tar.gz", "has_sig": false, "md5_digest": "8398d7f7fc3b95c80f150e444ee4b269", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 17695, "upload_time": "2012-12-28T23:01:27", "url": "https://files.pythonhosted.org/packages/0d/2b/4b2a9f0dd781a5cb463ca448dec39387c954398818daa966872754ba39c4/gutter-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "d3b9ba758034d0b3d58ba17e412d79c3", "sha256": "6edcf2163eb72fd2e67ed321ee6d543c629e2580e954338b3f6f8e7c5a8b1a02" }, "downloads": -1, "filename": "gutter-0.1.2.tar.gz", "has_sig": false, "md5_digest": "d3b9ba758034d0b3d58ba17e412d79c3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 17688, "upload_time": "2013-01-25T00:49:22", "url": "https://files.pythonhosted.org/packages/1d/a7/d34e3160e28eb31716caaf83338bc9670936b1e76a7a45e46ea6629bc35c/gutter-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "507686ef96841ba066198ea988074a5c", "sha256": "df4e9a7c407c66cde197c6769b6468b8f7bfca2b19a859985f0fd92ef051c747" }, "downloads": -1, "filename": "gutter-0.1.3.tar.gz", "has_sig": false, "md5_digest": "507686ef96841ba066198ea988074a5c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 17923, "upload_time": "2013-05-20T21:01:28", "url": "https://files.pythonhosted.org/packages/17/7b/217541ba2845a6a3008bfd1a4333b04260a972ade83d2af97a4341661f55/gutter-0.1.3.tar.gz" } ], "0.1.4": [ { "comment_text": "", "digests": { "md5": "3c13ee2a280b1e6d0021a6da0e915c58", "sha256": "7ebf927615839e34cb82e598d506f4538b4b0a9389d1537a6a8fec63fa9b1d44" }, "downloads": -1, "filename": "gutter-0.1.4.tar.gz", "has_sig": false, "md5_digest": "3c13ee2a280b1e6d0021a6da0e915c58", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 17968, "upload_time": "2013-06-03T04:36:12", "url": "https://files.pythonhosted.org/packages/7c/90/13ae298a62b4cc6c2b0b89e129cb027f46f1ad46478d8186cf745e77fb4b/gutter-0.1.4.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "d32576807b53ab9b1bd9d5b414d5486e", "sha256": "b1d632a66b93eb057f35e3c044045a121a8858b9fd86650ec63027fa970ebe1e" }, "downloads": -1, "filename": "gutter-0.2.0.tar.gz", "has_sig": false, "md5_digest": "d32576807b53ab9b1bd9d5b414d5486e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9493, "upload_time": "2013-07-12T22:37:28", "url": "https://files.pythonhosted.org/packages/3d/0d/f4d678a575586b6bafc39aa35897ad0e62a20f4e597cb4a76152f5fdb4e1/gutter-0.2.0.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "0dc9243a6073b0ebb3ae6a19b58a8fcf", "sha256": "480f9b3f0e833ea8b23104db1dcb920dc9eb787d31a7218d6d6f556f219b12f8" }, "downloads": -1, "filename": "gutter-0.4.0.tar.gz", "has_sig": false, "md5_digest": "0dc9243a6073b0ebb3ae6a19b58a8fcf", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18384, "upload_time": "2014-06-09T21:55:50", "url": "https://files.pythonhosted.org/packages/ff/02/32f4c1b644ec9ce0f9f8c2d780be5f2397dc0f2e4563e204337d79970586/gutter-0.4.0.tar.gz" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "90f94fad3c297764f4439ec9f9657126", "sha256": "f1a2643dd71ccdf588bd532c722767ac246315286cd559eacc6a167126049133" }, "downloads": -1, "filename": "gutter-0.5.0.tar.gz", "has_sig": false, "md5_digest": "90f94fad3c297764f4439ec9f9657126", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19647, "upload_time": "2015-04-28T17:02:48", "url": "https://files.pythonhosted.org/packages/2a/51/e9cd57c6a8d679316c0970c3c9c918b615dbd9822e96df1141c140e1817b/gutter-0.5.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "90f94fad3c297764f4439ec9f9657126", "sha256": "f1a2643dd71ccdf588bd532c722767ac246315286cd559eacc6a167126049133" }, "downloads": -1, "filename": "gutter-0.5.0.tar.gz", "has_sig": false, "md5_digest": "90f94fad3c297764f4439ec9f9657126", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19647, "upload_time": "2015-04-28T17:02:48", "url": "https://files.pythonhosted.org/packages/2a/51/e9cd57c6a8d679316c0970c3c9c918b615dbd9822e96df1141c140e1817b/gutter-0.5.0.tar.gz" } ] }