跳转到主要内容

客户端到间距功能开关后端

项目描述

.. image:: https://api.travis-ci.org/disqus/gutter.png?branch=master
:target: http://travis-ci.org/disqus/gutter

间距
------

**注意**:此仓库是Gargoyle 2的客户端,也称为“Gutter”。它不与现有的`Gargoyle 1代码库 <https://github.com/disqus/gargoyle/>`_ 兼容。

间距是功能开关管理库。它允许用户创建功能开关并设置开关将被启用的条件。一旦配置,可以检查开关是否针对输入(请求、用户对象等)处于活动状态。

要配置Gutter的UI,请查看`gutter-django项目 <https://github.com/disqus/gutter-django>`_

目录
=================

* 配置_
* 设置_
* 参数_
* `Switches`_
* `Conditions`_
* `检查开关是否激活`_
* 信号_
* 命名空间_
模板_
装饰器_
`测试工具`_

配置
=============

在开始使用之前,Gutter 需要一点配置。

选择存储
~~~~~~~~~~~~~~~~

开关被保存在一个 ``storage`` 对象中,它是一个 `dict` 或任何提供 ``types.MappingType`` 接口的对象(即拥有 ``__setitem__`` 和 ``__getitem__`` 方法)。默认情况下,``gutter`` 使用来自 `durabledict` 库的 `MemoryDict` 实例。这个引擎 **一旦进程结束就不会持久化数据**,因此应该使用更持久的存储。

自动创建
~~~~~~~~~~

``gutter`` 也可以“自动创建”开关。如果启用了 ``autocreate``,并且 ``gutter`` 被询问开关是否活跃但开关尚未创建时,``gutter`` 将自动创建开关。在自动创建后,开关的状态将被设置为“禁用”。

此行为默认是关闭的,但可以通过设置启用。更多关于“设置”的内容见下文。

配置设置
~~~~~~~~~~~~~~~~~~~~

要更改 ``storage`` 和/或 ``autocreate`` 设置,只需导入设置模块并设置相应的变量

.. 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

在这种情况下,我们将引擎更改为 durabledict 的 ``RedisDict`` 并打开 ``autocreate``。这些设置将适用于所有新构造的 ``Manager`` 实例。关于 ``Manager`` 是什么以及如何在本文档的后面部分使用它的更多信息。

设置
=====

一旦配置了 ``Manager`` 的存储引擎,您就可以导入 gutter 的默认 ``Manager`` 对象,这是您与 ``gutter`` 的主要接口

.. code:: python

from gutter.client.default import gutter

此时,``gutter`` 对象是 ``Manager`` 类的实例,其中包含所有注册开关和检查它们是否活跃的方法。在大多数安装和用法场景中,``gutter.client.gutter`` 管理员将是您的首选接口。

使用不同的默认 Manager
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

如果您想构建并使用不同的默认管理器,但仍然可以通过 ``gutter.client.gutter`` 访问它,您可以将 ``Manager`` 实例赋给 ``settings.manager.default`` 值

.. code:: python

from gutter.client.settings import manager as manager_settings
from gutter.client.models import Manager

manager_settings.default = Manager({}) # 必须在导入默认 manager 之前完成

from gutter.client.default import gutter

assert manager_settings.default is gutter

.. WARNING:

:warning::warning
请注意,必须 **在导入默认 ``gutter`` 实例之前** 设置 ``settings.manager.default`` 值。
:warning::warning

参数
=========

您使用 ``gutter`` 的第一步是定义您将检查开关的参数。一个“参数”是一个理解您系统中的业务逻辑和对象(用户、请求等)的对象,并且知道如何从这些业务对象中验证、转换和提取变量以用于 ``Switch`` 条件。例如,您的系统可能有一个具有 ``is_admin``、``date_joined`` 等属性的 ``User`` 对象。为了与它切换,您将为每个这些值创建参数。

为此,您构建一个从 ``gutter.client.arguments.Container`` 继承的类。在类的主体内部,您通过使用 ``gutter.client.arguments`` 函数创建您需要的“参数”类变量。

.. 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)

这里有一些事情正在进行,让我们逐个解释它们。

1. ``UserArgument`` 类是从 ``Container`` 继承的。这种继承是必需的,因为 ``Container`` 实现了一些必需的 API。
2. 这个类包含一些类变量,它们是对 `arguments.TYPE` 的调用,其中 `TYPE` 是这个参数的类型。目前有3种类型:`Value` 用于一般值,`Boolean` 用于布尔值,`String` 用于字符串值。
3. `arguments.TYPE()` 调用了一个可调用的对象,该对象返回值。在上面的例子中,我们将根据用户的 `name`、`is_admin` 状态和 `age` 来激活一些开关。
4. 这些 `argument` 返回实际值,该值是从 `self.input` 解引用得到的,`self.input` 是输入对象(在这个例子中是一个 `User` 实例)。
5. `Variable` 对象理解 `Switch` 条件和运算符,并实现正确的 API 以允许它们被适当地比较。
6. `COMPATIBLE_TYPE` 声明此参数仅适用于 `User` 实例。这通过与基类参数中 `applies` 的默认实现一起工作,该实现检查输入的 `type` 是否与 `COMPATIBLE_TYPE` 相同。

由于构造仅引用 `self.input` 属性的参数非常常见,如果你将字符串作为 `argument()` 的第一个参数传递,当访问参数时,它将简单地从 `self.input` 返回该属性。你还必须将 `Variable` 传递给 `variable=` 关键字参数,以便知道要包装值的 `Variable`。

.. 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('name')
age = arguments.Value('name')


参数的合理性
~~~~~~~~~~~~~~~~~~~~~~~

你可能想知道,为什么需要这些 `Argument` 对象?它们似乎只是在我的系统中包装了一个对象,并提供了相同的 API。为什么我不能直接使用我的业务对象 **itself** 并将其与我的开关条件进行比较呢?

简短的回答是,`Argument` 对象提供了一个翻译层,将您的业务对象转换为 `gutter` 理解的对象。这有几个原因很重要。

首先,这意味着您不会在业务逻辑/对象中添加代码来支持 `gutter`。您在(一个 `Argument`)的一个位置声明您希望提供给开关的所有参数,该 `Argument` 的唯一责任是与 `gutter` 接口。您还可以构建更智能的 `Argument` 对象,这些对象可能是多个业务对象的组合、咨询第三方服务等。所有这些都不会弄乱您的主要应用程序代码或业务对象。

其次,也是最重要的,`Arguments` 返回 `Variable` 对象,这确保 `gutter` 条件能够正确工作。这主要与基于百分比的运算符相关,以下是一个例子。

想象您有一个 `User` 类,它有一个 `is_vip` 布尔字段。假设您只想为 10% 的 VIP 客户启用一个功能。为此,您会编写一个条件,表示,“每次调用变量时,10% 的时间应该为真。”这一行代码可能做如下操作

.. code:: python

return 0 <= (hash(variable) % 100) < 10

问题是,如果 `variable = True`,则 `hash(variable) % 100` 对于 **每个** `is_vip` 为 `True` 的 `User` 都将是相同的值

.. code:: python

>>> hash(True)
1
>>> hash(True) % 100
1

这是因为 Python 中 `True` 对象总是有相同的哈希值,因此百分比检查不起作用。这不是您想要的行为。

对于 10% 的百分比范围,您希望它在 10% 的输入上激活。因此,每个输入都必须有一个唯一的哈希值,这正是 `Boolean` 变量提供的特性。每个 `Variable` 都有针对条件的已知特性,而您的对象可能没有。

虽然如此,您**不一定**必须使用“变量”对象。对于像“use.age > some_value”这样的明显情况,您的“User”实例也能正常工作,但为了安全起见,您应该使用“变量”对象。使用“变量”对象还可以确保,如果您更新“gutter”,任何新添加的“操作符”类型都能正确地与您的“变量”对象一起工作。

开关
============================================

开关封装了“开”或“关”的概念,这取决于输入。开关通过检查其“条件”来确定其开/关状态,看这些条件是否适用于某个特定的输入。

开关只需要一个必需的参数,即“名称”

.. code:: python

from gutter.client.models import Switch

switch = Switch('my cool feature')

开关可以有3个核心状态:“全局”、“禁用”和“选择性”。在“全局”状态下,无论什么输入,开关都是启用的。无论什么情况下,“禁用”开关都不会对任何输入进行禁用。“选择性”开关根据其条件进行启用。

开关可以在某个状态构建,或者稍后可以更改其属性

.. code:: python

switch = Switch('new feature', state=Switch.states.DISABLED)
another_switch = Switch('new feature')
another_switch.state = Switch.states.DISABLED

复合
~~~~~~~~~~

在“选择性”状态下,通常只需一个条件为真,开关才能为特定输入启用。如果“switch.compounded”设置为“True”,那么**所有**开关的条件都必须为真才能启用:

switch = Switch('require alll conditions', compounded=True)

分层开关
~~~~~~~~~~~~~~~~~~~~~

您可以使用特定的分层命名方案创建开关。开关命名空间由冒号字符(“:”)分隔,开关的分层可以按这种方式构建

.. code:: python

parent = Switch('movies')
child1 = Switch('movies:star_wars')
child2 = Switch('movies:die_hard')
grandchild = Switch('movies:star_wars:a_new_hope')

在上面的例子中,“child1”开关是“movies”开关的子代,因为它在开关名称前有“movies:”作为前缀。两者“child1”和“child2”都是“parent”开关的“子代”。而“grandchild”是“child1”开关的子代,但不是“child2”开关的子代。

集中
~~~~~~~

默认情况下,每个开关都独立于管理器中的其他开关(包括其父代)做出“我是活跃的?”的决定,并且只咨询其自己的条件以检查它是否启用了输入。但这并不总是这样。也许您有一个只有特定用户类才能使用的新功能。在这些用户中,您希望10%的用户接触到一个不同的用户界面,以了解他们的行为与其他90%的用户相比如何。

“gutter”允许您在开关上设置“concent”标志,指示它首先检查其父代开关,然后再检查自己。如果它检查父代开关并发现它对于相同的输入没有启用,则开关将立即返回“False”。如果父代开关**确实**启用了输入,那么开关将继续并检查自己的条件,像通常一样返回。

例如

.. code:: python

parent = Switch('cool_new_feature')
child = Switch('cool_new_feature:new_ui', concent=True)

例如,因为“child”是以“concent=True”构建的,所以即使“child”对于输入启用了,它也只有在“parent”也启用了相同的输入时才会返回“True”。

**注意:**即使在“全局”或“禁用”状态下的开关(见上述“开关”部分)也会在检查自己之前同意其父代。这意味着,即使某个开关是“全局”的,如果它将“concent”设置为“True”并且其父代对于输入没有启用,则该开关本身将返回“False”。

注册开关
~~~~~~~~~~~~~~~~~~~~

一旦您的“Switch”构建了正确的条件,您需要将其注册到“Manager”实例中,以便将来使用。否则,它将只存在于当前进程的内存中。通过“Manager”实例上的“register”方法注册开关。

.. code:: python

gutter.register(switch)

开关现在存储在管理器的存储中,可以通过 `gutter.active(switch)` 检查其是否处于活动状态。

更新开关
~~~~~~~~~~~~~~~~~

如果您需要更新开关,只需修改 `Switch` 对象,然后调用管理器的 `update()` 方法,将其与新对象一起更新开关。

.. code:: python

switch = Switch('cool switch')
manager.register(switch)

switch.name = 'even cooler switch' # 开关尚未在管理器中更新

manager.update(switch) # 开关现在已在管理器中更新

由于这是一个常见的模式(从管理器检索开关,然后更新),gutter 提供了一个简化的 API,您可以通过名称请求管理器中的开关,然后在 **switch** 上调用 `save()` 以在检索它的管理器中更新。

.. code:: python

switch = manager.switch('existing switch')
switch.name = 'a new name' # 开关尚未在管理器中更新
switch.save() # 与调用 manager.update(switch) 相同

注销开关
~~~~~~~~~~~~~~~~~~~~~~

可以通过调用 `unregister()` 并传入开关名称或实例来从管理器中移除现有的开关。

.. code:: python

gutter.unregister('deprecated switch')
gutter.unregister(a_switch_instance)

**注意**:如果开关是层次结构的一部分并且有子开关(见上面的“层次结构开关”部分),所有后代开关(子代、孙代等)也将被注销并删除。


条件
==========

每个开关可以有 0 个或更多条件,这些条件描述了开关在哪些条件下处于活动状态。条件对象使用三个值构建:一个 `argument`、`attribute` 和 `operator`。

`argument` 是任何 `Argument` 类,就像您之前定义的那样。在上面的示例中,`UserArgument` 是一个参数对象。`attribute` 是参数实例上的属性,您希望此条件进行检查。`operator` 是对该属性应用的某种检查。例如,`UserArgument.age` 是否大于某个值?等于某个值?在值的范围内?等等。

假设您想创建一个检查用户年龄是否大于 65 岁的条件,您将这样构建条件

.. code:: python

from gutter.client.operators.comparable import MoreThan

condition = Condition(argument=UserArgument, attribute='age', operator=MoreThan(65))

此条件在任意输入实例的 `age` 大于 `65` 时为 `True`。

请参阅 `gutter.operators` 了解可用操作符的列表。

条件也可以使用 `negative` 参数构建,该参数取反条件。例如

.. code:: python

from gutter.client.operators.comparable import MoreThan

condition = Condition(argument=UserArgument, attribute='age', operator=MoreThan(65), negative=True)

此条件现在在条件评估为 `False` 时为 `True`。在这种情况下,如果用户的 `age` 不是大于 `65`。

然后将条件附加到开关实例,如下所示

.. code:: python

switch.conditions.append(condition)

您可以将任意数量的条件附加到开关,没有限制。

检查开关是否处于活动状态
===========================

如前所述,开关将与输入对象进行比对。为此,您可以用 `User` 实例等调用开关的 `enabled_for()` 方法。您可以用任何输入对象调用 `enabled_for()`,它将忽略它对不知道的输入。如果 `Switch` 对您的输入处于活动状态,则 `enabled_for` 将返回 `True`。否则,它将返回 `False`。

`gutter.active()` API
~~~~~~~~~~~~~~~~~~~~~~~~~

gutter 的常见用法是在处理 Web 请求期间使用。在代码执行过程中,根据某些开关是否处于活动状态,不同的代码路径会被采取。通常在任何给定时间都会存在多个开关,并且它们都需要与多个参数进行比对。为了处理这种情况,Gutter 提供了一个更高级的 API。

要检查 `Switch` 是否处于活动状态,只需用开关名称调用 `gutter.active()`。

.. code:: python

gutter.active('my cool feature')
>>> True

开关将与一些输入对象进行比较。可以通过两种方式将输入添加到“active()”检查中:本地方式,通过传递给“active()”调用,或者全局方式,在事先配置。

为了检查本地输入,"active()"在开关名称之后接受任意数量的输入对象,以检查开关。在这个例子中,名为“my cool feature”的开关与输入对象“input1”和“input2”进行比较。

.. code:: python

gutter.active('my cool feature', input1, input2)
>>> True

如果您有希望用于每次检查的全局输入对象,可以通过调用管理器的“input()”方法来设置它们。

.. code:: python

gutter.input(input1, input2)

现在,“input1”和“input2”将被用于每次“active”调用。例如,假设“input1”和“input2”如上配置,此“active()”调用将按顺序检查开关是否为“input1”、“input2”和“input3”启用:

gutter.active('my cool feature', input3)

一旦不再使用全局输入,可能是在请求结束时,应调用管理器的“flush()”方法来删除所有输入。

.. code:: python

gutter.flush()

管理器现在已设置并准备好接收下一组输入。

当使用本地输入调用“active()”时,您可以通过将“exclusive=True”作为关键字参数传递给“active()”来跳过将“Switch”与全局输入比较,并且**仅**检查本地传入的输入。

.. code:: python

gutter.input(input1, input2)
gutter.active('my cool feature', input3, exclusive=True)

在上面的例子中,由于传递了“exclusive=True”,因此仅将名为“my cool feature”的开关与“input3”进行比较,而不是“input1”或“input2”。“exclusive=True”参数不是持久的,因此下一次没有“exclusive=True”的“active()”调用将再次使用全局定义的输入。

信号
=======

Gutter提供了4个总信号以连接:3个与开关更改相关,1个与应用条件时的错误相关。它们都可在“gutter.signals”模块中找到。

开关信号
~~~~~~~~~~~~~~
有3个与开关更改相关的信号。

1. “switch_registered” - 当新开关与管理器注册时调用。
2. “switch_unregistered” - 当开关与管理器注销时调用。
3. “switch_updated” - 当开关被更新时调用。

要使用信号,只需调用信号对象的“connect()”方法,并传入一个可调用的对象。当信号被触发时,它将调用您的可调用对象,传入正在注册/注销/更新的开关。例如:

.. code:: python

from gutter.client.signals import switch_updated

def log_switch_update(switch)
Syslog.log("Switch %s updated" % switch.name)

switch_updated.connect(log_switch_updated)

理解开关更改
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

可以通过连接到“switch_updated”信号来通知开关已更改。为了知道开关中发生了什么更改,可以查看其“changes”属性。

.. code:: python

>>> from gutter.client.models import Switch
>>> switch = Switch('test')
>>> switch.concent
True
>>> switch.concent = False
>>> switch.name = 'new name'
>>> switch.changes
{'concent': {'current': False, 'previous': True}, 'name': {'current': 'new name', 'previous': 'test'}}

如你所见,当我们更改了开关的“concent”设置和“name”时,“switch.changes”以字典的形式反映了更改的属性。您也可以简单地使用“changed”属性来询问开关是否发生了任何更改。它返回“True”或“False”,表示开关是否有任何更改。

您可以在信号回调函数中使用这些值来做出基于更改内容的决策。例如,仅当更改包括条件更改时才发送电子邮件差异。

条件应用错误信号
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

当“开关”检查输入对象是否满足其条件时,很有可能会遇到一些意外的“参数”值,这可能导致异常。每当在将“条件”应用于输入对象时发生异常,该“条件”会捕获该异常并返回“False”。

虽然捕获所有异常通常不是好的做法,因为它会隐藏错误,但在大多数情况下,您不希望因为检查开关条件时出现错误而导致应用程序请求失败,尤其是如果用户本就未应用该“条件”时。

话虽如此,您仍然可能想知道是否在检查“条件”时出现了错误。为了实现这一点,“gutter”客户端提供了一个在检查“条件”时出现错误时被调用的“condition_apply_error”信号。该信号以“条件”实例、引起错误的输入以及异常类的实例为参数被调用。

.. code:: python

signals.condition_apply_error.call(condition, inpt, error)

在您的连接回调中,您可以执行任何您想做的事情:记录错误、报告异常等。

命名空间
==========

“gutter”允许使用“命名空间”将开关组合在一起,同时不允许一个命名空间看到另一个命名空间的开关,但允许它们共享相同的存储实例、操作员和其他配置。

给定现有的普通“Manager”实例,您可以通过调用“namespaced()”方法创建一个命名空间化的管理器。

.. code:: python

notifications = gutter.namespaced('notifications')

在此阶段,“notifications”是“gutter”的一个副本,继承了其所有的

* 存储
* “autocreate”设置
* 全局输入
* 操作员

然而,它不共享相同的开关。新构建的“Manager”实例位于“default”命名空间中。当调用“namespaced()”时,“gutter”将管理器的命名空间更改为“notifications”。在“default”命名空间中的任何开关在“notifications”命名空间中都是不可见的,反之亦然。

这允许您拥有独立的命名空间化“视图”的开关,可能具有完全相同的名字,而不会相互冲突。

装饰器
==========

“Gutter”提供了一个名为“@switch_active”的装饰器,您可以使用它来装饰Django视图。当装饰后,如果作为“@switch_decorated”装饰器第一个参数的开关名称为False,则会引发“Http404”异常。但是,如果您还传递了“redirect_to=”,则装饰器将返回一个“HttpResponseRedirect”实例,将其重定向到该位置。如果开关处于活动状态,则视图将正常运行。

例如,以下是一个使用“@switch_active”装饰的视图


.. code:: python

from gutter.client.decorators import switch_active

@switch_active('cool_feature')
def my_view(request)
return 'foo'

如前所述,如果“cool_feature”开关处于非活动状态,此视图将引发“Http404”异常。

然而,如果装饰器被构造为包含“redirect_to=”参数

.. code:: python

@switch_active('cool_feature', redirect_to=reverse('upsell-page'))

则将返回一个“HttpResponseRedirect”实例,将其重定向到“reverse('upsell-page')”。

测试实用工具
===============

如果您想测试使用“gutter”的代码,并让“gutter”管理器返回可预测的结果,您可以使用“testutils”模块中的“switches”对象。

“swtiches”对象可以用作上下文管理器和装饰器。它传递开关名称及其“active”返回值的“kwargs”。

例如,在此代码中,通过将“cool_feature=True”传递给“switches”对象作为上下文管理器,任何对“gutter.active('cool_feature')”的调用都将返回“True”。对其他开关名称的“active()”调用将返回其实际的实时开关状态

.. code:: python

from gutter.client.testutils import switches
from gutter.client.default import gutter

with switches(cool_feature=True)
gutter.active('cool_feature') # True


并且当使用“switches”作为装饰器时

.. code:: python

from gutter.client.testutils import switches
from gutter.client.default import gutter

@switches(cool_feature=True)
定义 run(self)
gutter.active('cool_feature') # True

此外,您可以将一个替代的 ``Manager`` 实例传递给 ``switches``,以使用该管理器而不是默认的管理器

.. code:: python

from gutter.client.testutils import switches
from gutter.client.models import Manager

my_manager = Manager({})

@switches(my_manager, cool_feature=True)
定义 run(self)
gutter.active('cool_feature') # True

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源分布

gutter-0.5.0.tar.gz (19.6 kB 查看哈希值)

上传时间

由以下组织支持