{ "info": { "author": "", "author_email": "", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: Apache Software License" ], "description": "Flipper\n=======\n![Circle CI Status](https://circleci.com/gh/carta/flipper-client/tree/master.svg?style=shield&circle-token=e401445db3e99e8fac7555bd9ba5040e6a2eb4bd)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)\n\nFlipper is a lightweight, easy to use, and flexible library for feature flags in python. It is intended to allow developers to push code to production in a disabled state and carefully control whether or not the code is enabled or disabled without doing additional releases.\n\n# Quickstart\n\n`pip install flipper-client`\n\n\n```python\nfrom flipper import FeatureFlagClient, MemoryFeatureFlagStore\n\n\nfeatures = FeatureFlagClient(MemoryFeatureFlagStore())\n\nMY_FEATURE = 'MY_FEATURE'\n\nfeatures.create(MY_FEATURE)\nfeatures.enable(MY_FEATURE)\n\nif features.is_enabled(MY_FEATURE):\n run_my_feature()\nelse:\n run_old_feature()\n```\n\n# API\n\n## FeatureFlagClient\n\n**`is_enabled(feature_name: str, **conditions) -> bool`**\n\nCheck if a feature is enabled. Also supports conditional enabling of features. To check for conditionally enabled features, pass keyword arguments with conditions you wish to supply. For more information, see the conditions section.\n\nExample:\n\n```python\nfeatures.is_enabled(MY_FEATURE)\n\n# With conditons\nfeatures.is_enabled(FEATURE_IMPROVED_HORSE_SOUNDS, is_horse_lover=True)\n```\n\n**`create(feature_name: str, is_enabled: bool=False, client_data: dict=None) -> FeatureFlag`**\n\nCreate a new feature flag and optionally set value (is_enabled is false/disabled).\n\nFor advanced implementations, you can also specify user-defined key-value pairs as a `dict` via the client_data keyword argument. These values should be json serializable and will be stored in the metadata section of the flag object.\n\nExample:\n\n```python\nflag = features.create(MY_FEATURE)\n```\n\n**`exists(feature_name: str) -> bool`**\n\nCheck if a feature flag already exists by name. Feature flag names must be unique.\n\nExample:\n\n```python\n if not features.exists(MY_FEATURE):\n features.create(MY_FEATURE)\n```\n\n**`get(feature_name: str) -> FeatureFlag`**\n\nReturns an instance of `FeatureFlag` for the requested flag.\n\nExample:\n\n```python\nflag = features.get(MY_FEATURE)\n```\n\n**`enable(feature_name: str) -> void`**\n\nEnables the specified flag. Subsequent calls to `is_enabled` should return true.\n\nExample:\n\n```python\nfeatures.enable(MY_FEATURE)\n```\n\n**`disable(feature_name: str) -> void`**\n\nDisables the specified flag. Subsequent calls to `is_enabled` should return false.\n\nExample:\n\n```python\nfeatures.disable(MY_FEATURE)\n```\n\n**`destroy(feature_name: str) -> void`**\n\nDestroys the specified flag. Subsequent calls to `is_enabled` should return false.\n\nExample:\n\n```python\nfeatures.destroy(MY_FEATURE)\n```\n\n**`list(limit: Optional[int] = None, offset: int = 0) -> Iterator[FeatureFlag]`**\n\nLists all flags subject to the limit and offset you provide. The results are not guaranteed to be in order. Ordering depends on the backend you choose so plan accordingly.\n\nExample:\n\n```python\nfor feature in features.list(limit=100):\n print(feature.name, feature.is_enabled())\n```\n\n**`set_client_data(feature_name: str, client_data: dict) -> void`**\n\nSet key-value pairs to be stored as metadata with the flag. Can be retrieved using `get_client_data`. This will merge the supplied values with anything that already exists.\n\nExample:\n\n```python\nfeatures.set_client_data(MY_FEATURE, { 'ttl': 3600 })\n```\n\n**`get_client_data(feature_name: str) -> dict`**\n\nRetrieve key-value any key-value pairs stored in the metadata for this flag.\n\nExample:\n\n```python\nfeatures.get_client_data(MY_FEATURE)\n```\n\n**`get_meta(feature_name: str) -> dict`**\n\nSimilar to `get_client_data` but instead of returning only client-supplied metadata, it will return all metadata for the flag, including system-set values such as `created_date`.\n\nExample:\n\n```python\nfeatures.get_meta(MY_FEATURE)\n```\n\n**`add_condition(feature_name: str, condition: Condition) -> void`**\n\nAdds a condition to for enabled checks, such that `is_enabled` will only return true if all the conditions are satisfied when it is called.\n\nExample:\n\n```python\nfrom flipper import Condition\n\n\nfeatures.add_condition(MY_FEATURE, Condition(is_administrator=True))\n\nfeatures.is_enabled(MY_FEATURE, is_administrator=True) # returns True\nfeatures.is_enabled(MY_FEATURE, is_administrator=False) # returns False\n\n```\n\n**`set_bucketer(feature_name: str, bucketer: Bucketer) -> void`**\n\nSet the bucketer that used to bucket requests based on the checks passed to `is_enabled`. This is useful if you want to segment your traffic based on percentages or other heuristics that cannot be enforced with `Condition`s. See the `Bucketing` section for more details.\n\n```python\nfrom flipper.bucketing import Percentage, PercentageBucketer\n\n\n# Create a bucketer that will randomly enable a feature for 10% of traffic\nbucketer = PercentageBucketer(percentage=Percentage(0.1))\n\nclient.set_bucketer(MY_FEATURE, bucketer)\n\nclient.is_enabled(MY_FEATURE) # returns False 90% of the time\n```\n\n## FeatureFlag\n\n**`is_enabled() -> bool`**\n\nCheck if a feature is enabled. Also supports conditional enabling of features. To check for conditionally enabled features, pass keyword arguments with conditions you wish to supply. For more information, see the conditions section.\n\nExample:\n\n```python\nflag.is_enabled()\n\n# With conditons\nflag.is_enabled(is_horse_lover=True, horse_type__in=['Stallion', 'Mare'])\n```\n\n**`enable() -> void`**\n\nEnables the flag. Subsequent calls to `is_enabled` should return true.\n\nExample:\n\n```python\nflag.enable()\n```\n\n**`disable() -> void`**\n\nDisables the specified flag. Subsequent calls to `is_enabled` should return false.\n\nExample:\n\n```python\nflag.disable()\n```\n\n**`destroy() -> void`**\n\nDestroys the flag. Subsequent calls to `is_enabled` should return false.\n\nExample:\n\n```python\nflag.destroy()\n```\n\n**`set_client_data(client_data: dict) -> void`**\n\nSet key-value pairs to be stored as metadata with the flag. Can be retrieved using `get_client_data`. This will merge the supplied values with anything that already exists.\n\nExample:\n\n```python\nflag.set_client_data({ 'ttl': 3600 })\n```\n\n**`get_client_data() -> dict`**\n\nRetrieve key-value any key-value pairs stored in the metadata for this flag.\n\nExample:\n\n```python\nflag.get_client_data()\n```\n\n**`get_meta() -> dict`**\n\nSimilar to `get_client_data` but instead of returning only client-supplied metadata, it will return all metadata for the flag, including system-set values such as `created_date`.\n\nExample:\n\n```python\nflag.get_meta()\n```\n\n**`add_condition(condition: Condition) -> void`**\n\nAdds a condition to for enabled checks, such that `is_enabled` will only return true if all the conditions are satisfied when it is called.\n\nExample:\n\n```python\nfrom flipper import Condition\n\n\nflag.add_condition(Condition(is_administrator=True))\n\nflag.is_enabled(is_administrator=True) # returns True\nflag.is_enabled(is_administrator=False) # returns False\n```\n\n**`set_bucketer(bucketer: Bucketer) -> void`**\n\nSet the bucketer that used to bucket requests based on the checks passed to `is_enabled`. This is useful if you want to segment your traffic based on percentages or other heuristics that cannot be enforced with `Condition`s. See the `Bucketing` section for more details.\n\n```python\nfrom flipper.bucketing import Percentage, PercentageBucketer\n\n\n# Create a bucketer that will enable a feature for 10% of traffic\nbucketer = PercentageBucketer(percentage=Percentage(0.1))\n\nflag.set_bucketer(bucketer)\n\nflag.is_enabled() # returns False 90% of the time\n```\n\n## decorators\n\n**`is_enabled(features: FeatureFlagClient, feature_name: str, redirect: Optional[Callable]=None)`**\n\nThis is a decorator that can be used on any function (including django/flask views). If the feature is enabled then the function will be called. If the feature is not enabled, the function will not be called. If a callable `redirect` function is provided, then the `redirect` function will be called instead when the feature is not enabled.\n\nExample:\n\n```python\nfrom flipper.decorators import is_enabled\n\nfrom myapp.feature_flags import (\n FEATURE_IMPROVED_HORSE_SOUNDS,\n features,\n)\n\n\n@is_enabled(\n features.instance,\n FEATURE_IMPROVED_HORSE_SOUNDS,\n redirect=old_horse_sound,\n)\ndef new_horse_sound(request):\n return HttpResponse('Whinny')\n\ndef old_horse_sound(request):\n return HttpResponse('Neigh')\n```\n\n## Conditions\n\nFlipper supports conditionally enabled feature flags. These are useful if you want to enable a feature for a subset of users or based on some other condition within your application. Its usage is very simple. First, import the `Condition` class:\n\n\n```python\nfrom flipper import Condition\n```\n\nThen add the condition to a flag using the `add_condition` method of the `FeatureFlagClient` or `FeatureFlag` interface. You can add as many conditions as you like, and each condition may specify multiple checks:\n\n\n```python\nflag = client.get(FEATURE_IMPROVED_HORSE_SOUNDS)\n\n# Feature is only enabled for horse lovers\nflag.add_condition(Condition(is_horse_lover=True))\n\n# Feature is only enabled for people with more than 9000 horses who don't live in the city\nflag.add_condition(Condition(number_of_horses_owned__gt=9000, location__ne='city'))\n```\n\nThen you can specify these checks when calling `is_enabled`. The checks are not required, and if not specified `is_enabled` will return the base `enabled` status of the feature.\n\n```python\nflag.enable()\nflag.is_enabled() # True\nflag.is_enabled(is_horse_lover=True) # True\nflag.is_enabled(is_horse_lover=True, number_of_horses_owned=8000) # False\nflag.is_enabled(location='city') # False\n```\n\n### Condition operators supported\n\nIn addition to equality conditions, `Conditions` support the following operator comparisons:\n\n- `__gt` Greater than\n- `__gte` Greater than or equal to\n- `__lt` Less than\n- `__lte` Less than or equal to\n- `__ne` Not equals\n- `__in` Set membership\n- `__not_in` Set non-membership\n\nOperators must be a suffix of the argument name and must include `__`.\n\n## Bucketing\n\nBucketing is useful if you ever want the result of `is_enabled` to vary depending on a pre-defined percentage value. Examples might include A/B testing or canary releases. Out of the box, flipper supports percentage-based bucketing for both random-assignment cases and consistent-assignment cases. Flipper also supports linear ramps for variable percentage cases.\n\nConditions always take precedence over bucketing when applied together.\n\n### Random assignment\n\nThis is the simplest possible bucketing scenario, where you want to randomly segment traffic using a constant percentage.\n\n```python\nfrom flipper.bucketing import Percentage, PercentageBucketer\n\nfrom myapp import features\n\n\nFEATURE_NAME = 'HOMEPAGE_AB_TEST'\n\nflag = features.create(FEATURE_NAME)\n\nbucketer = PercentageBucketer(Percentage(0.5))\n\nflag.set_bucketer(bucketer)\nflag.enable() # global enabled status overrides buckets\n\nflag.is_enabled() # has a 50% shot of returning True each time it is called\n```\n\n### Consistent assignment\n\nThis mechanism works like percentage-based bucket assignment, except the bucketer will always return the same value for the values it is provided. In other words, if you pass the same keyword arguments to `is_enabled` it will always return the same result for those arguments. This works using consistent hashing of the keyword arguments. The keyword arguments get serialized as json and hashed. This hash is then mod-ded by 100 to give a value in the range 0-100. This value is then compared to the current percentage to derminine whether or not that bucket is enabled.\n\nWhen is this useful? Any time you want to randomize traffic, but you want each individual client/user to always receive the same experience.\n\nThis is perhaps easisest to illustrate with and example:\n\n```python\nfrom flipper.bucketing import Percentage, ConsistentHashPercentageBucketer\n\nfrom myapp import features\n\n\nFEATURE_NAME = 'HOMEPAGE_AB_TEST'\n\nflag = features.create(FEATURE_NAME)\n\nbucketer = ConsistentHashPercentageBucketer(\n key_whitelist=['user_id'],\n percentage=Percentage(0.5),\n)\n\nflag.set_bucketer(bucketer)\nflag.enable() # global enabled status overrides buckets\n\nflag.is_enabled(user_id=1) # Always returns True (bucket is 0.48)\nflag.is_enabled(user_id=2) # Always returns False (bucket is 0.94)\n```\n\n#### Combining with Conditions\n\nThese can also be combined with conditions. When a bucketer is combined with one or more conditions, the conditions take precedence. That is, if any of the conditions evaluate to `False`, then `is_enabled` will return `False` regardless of what the bucketing status is. The converse also holds: If if all of the conditions evaluate to `True`, then `is_enabled` will return `True` regardless of what the bucketing status is.\n\nHowever, if no keyword arguments that match current conditions are supplied to `is_enabled`, any conditions will evaluate to `True`.\n\n```python\nbucketer = ConsistentHashPercentageBucketer(\n key_whitelist=['user_id'],\n percentage=Percentage(0.5),\n)\ncondition = Condition(is_admin=True)\n\n# This will enable the flag for 50% of traffic and all administrators\nflag.enable()\nflag.add_condition(condition)\nflag.set_bucketer(bucketer)\n\nflag.is_enabled(user_id=2) # False\nflag.is_enabled(user_id=2, is_admin=True) # True\n\nflag.is_enabled(user_id=1) # True\nflag.is_enabled(user_id=1, is_admin=False) # False\n```\n\n#### Key whitelists\n\nIf you want bucketers to inspect a subset of the keyword arguments that `is_enabled` receives, use the `key_whitelist` parameter when initializing the `ConsistentHashPercentageBucketer`.\n\n```python\nbucketer = ConsistentHashPercentageBucketer(\n key_whitelist=['user_id'],\n percentage=Percentage(0.5),\n)\ncondition = Condition(number_of_horses_owned__lt=9000)\n\nflag.enable()\nflag.set_bucketer(bucketer)\n\n# Ignore all keys except user_id\nflag.is_enabled(user_id=1, number_of_horses_owned=9001) # True\n```\n\n### Ramping percentages over time\n\nIf you want to increase or decrease the percentage value over time, you can use the `LinearRampPercentage` class. This class takes the following parameters:\n\n- `initial_value: float=0.0`: The starting percentage\n- `final_value: float=1.0`: The ending percentage\n- `ramp_duration: int=3600`: The time (in seconds) for the ramp to complete\n- `initial_time: Optional[int]=now()`: The timestamp of when the ramp \"started\". Not common. Defaults to now.\n\nThis class can be used anywhere you would use a `Percentage`:\n\n\n```python\nfrom flipper.bucketing import LinearRampPercentage, PercentageBucketer\n\nfrom myapp import features\n\n\nFEATURE_NAME = 'HOMEPAGE_AB_TEST'\n\nflag = features.create(FEATURE_NAME)\n\n# Ramp from 20% to 80% over 30 minutes\nbucketer = PercentageBucketer(\n percentage=LinearRampPercentage(\n initial_value=0.2,\n final_value=0.8,\n ramp_duration=1800,\n ),\n)\n\nflag.set_bucketer(bucketer)\nflag.enable() # global enabled status overrides buckets\n\nflag.is_enabled() # has \u2248 0% chance\n\n# Wait 10 minutes\nflag.is_enabled() # has \u2248 33% chance\n\n# Wait another 10 minutes\nflag.is_enabled() # has \u2248 67% chance\n\n# Wait another 10 minutes\nflag.is_enabled() # has 100% chance\n```\n\nIt works with `ConsistentHashPercentageBucketer` as well.\n\n# Initialization\n\nflipper is designed to provide a common interface that is agnostic to the storage backend you choose. To create a client simply import the `FeatureFlagClient` class and your storage backend of choice.\n\nOut of the box, we support the following backends:\n\n- `MemoryFeatureFlagStore` (an in-memory store useful for development and tests)\n- `ConsulFeatureFlagStore` (Requires a running consul cluster. Provides the lowest latency of all the options)\n- `RedisFeatureFlagStore` (Requires a running redis cluster. Can be combined with `CachedFeatureFlagStore` to reduce average latency.)\n- `ThriftRPCFeatureFlagStore` (Requires a server that implements the `FeatureFlagStore` thrift service)\n\n\n## Usage with in-memory backend\n\nThis backend is useful for unit tests or development environments where you don't require data durability. It is the simplest of the stores.\n\n```python\nfrom flipper import FeatureFlagClient, MemoryFeatureFlagStore\n\n\nclient = FeatureFlagClient(MemoryFeatureFlagStore())\n```\n\n## Usage with Consul backend\n\n[consul](https://www.consul.io/intro/index.html), among other things, is a key-value storage system with an easy to use interface. The consul backend maintains a persistent connection to your consul cluster and watches for changes to the base key you specify. For example, if your base key is `features`, it will look for changes to any key one level beneath. This means that the consul backend has lower latency than the other supported backends.\n\n```python\nimport consul\nfrom flipper import ConsulFeatureFlagStore, FeatureFlagClient\n\n\nc = consul.Consul(host='127.0.0.1', port=32769)\n\n# default base_key is 'features'\nstore = ConsulFeatureFlagStore(c, base_key='feature-flags')\nclient = FeatureFlagClient(store)\n```\n\n## Usage with Redis backend\n\nTo connect flipper to redis just create an instance of `Redis` and supply it to the `RedisFeatureFlagStore` backend. Features will be tracked under the base key your provide (default is `features`).\n\nKeep in mind, this will do a network call every time a feature flag is checked, so you may want to add a local in-memory cache (see below).\n\n\n```python\nimport redis\nfrom flipper import FeatureFlagClient, RedisFeatureFlagStore\n\n\nr = redis.Redis(host='localhost', port=6379, db=0)\n\n# default base_key is 'features'\nstore = RedisFeatureFlagStore(r, base_key='feature-flags')\nclient = FeatureFlagClient(store)\n```\n\n## Usage with Redis backend and in-memory cache\n\nTo reduce the average network latency associated with storing feature flags in a remote redis cluster, you can wrap the `RedisFeatureFlagStore` in `CachedFeatureFlagStore`. This class takes a `FeatureFlagStore` as an argument at initialization. When the client checks a flag, it will first look in its local cache, and if it cannot find a value for the specified feature, it will look in Redis. When a value is retrieved from Redis, it will be inserted into the local cache for quick retrieval. The cache is implemented as an LRU cache, and it has a default expiration time of 15 minutes. To customize the expiration, or any other of the [cache properties](https://github.com/stucchio/Python-LRU-cache), simply pass them as keyworkd arguments to the `CachedFeatureFlagStore` constructor.\n\n\n```python\nimport redis\nfrom flipper import (\n CachedFeatureFlagStore,\n FeatureFlagClient,\n RedisFeatureFlagStore,\n)\n\n\nr = redis.Redis(host='localhost', port=6379, db=0)\n\nstore = RedisFeatureFlagStore(r)\n\n# Cache options are:\n# size (number of items to store, default=5000)\n# ttl (seconds before key expires, default=None, i.e. No expiration)\ncache = CachedFeatureFlagStore(store, ttl=30)\n\nclient = FeatureFlagClient(cache)\n```\n\n## Usage with a Thrift RPC server\n\nIf you would like to manage feature flags with a custom service that is possible by using the `ThriftRPCFeatureFlagStore` backend. To do this, you will need to implement the `FeatureFlagStore` service defined in `thrift/feature_flag_store.thrift`. Then when you intialize the `ThriftRPCFeatureFlagStore` you will need to pass an instance of a compatible thrift client.\n\nFirst, install the `thrift` package:\n\n```\npip install thrift\n```\n\nExample:\n\n```python\nfrom flipper import FeatureFlagClient, ThriftRPCFeatureFlagStore\nfrom flipper_thrift.python.feature_flag_store import (\n FeatureFlagStore as TFeatureFlagStore\n)\nfrom thrift import Thrift\nfrom thrift.transport import TSocket\nfrom thrift.transport import TTransport\nfrom thrift.protocol import TBinaryProtocol\n\n\ntransport = TSocket.TSocket('localhost', 9090)\ntransport = TTransport.TBufferedTransport(transport)\nprotocol = TBinaryProtocol.TBinaryProtocol(transport)\n\nthrift_client = TFeatureFlagStore.Client(protocol)\n\ntransport.open()\n\nstore = ThriftRPCFeatureFlagStore(thrift_client)\nclient = FeatureFlagClient(store)\n```\n\n*Note: this can also be optimized with the `CachedFeatureFlagStore`. See the redis examples above.*\n\nYou will also be required to implement the server, like so:\n\n```python\nimport re\n\nfrom flipper_thrift.python.feature_flag_store import (\n FeatureFlagStore as TFeatureFlagStore\n)\nfrom thrift.transport import TSocket\nfrom thrift.transport import TTransport\nfrom thrift.protocol import TBinaryProtocol\nfrom thrift.server import TServer\n\n\nclass FeatureFlagStoreServer(object):\n # Convert TitleCased calls like .Get() to snake_case calls like .get()\n def __getattribute__(self, attr):\n try:\n return object.__getattribute__(self, attr)\n except AttributeError:\n return object.__getattribute__(self, self._convert_case(attr))\n\n def _convert_case(self, name):\n s1 = re.sub('(.)([A-Z][a-z]+)', r'\\1_\\2', name)\n return re.sub('([a-z0-9])([A-Z])', r'\\1_\\2', s1).lower()\n\n def create(self, feature_name, is_enabled):\n pass\n def delete(self, feature_name):\n pass\n def get(self, feature_name):\n return True\n def set(self, feature_name, is_enabled):\n pass\n\nif __name__ == '__main__':\n server = FeatureFlagStoreServer()\n processor = TFeatureFlagStore.Processor(server)\n transport = TSocket.TServerSocket(host='127.0.0.1', port=9090)\n tfactory = TTransport.TBufferedTransportFactory()\n pfactory = TBinaryProtocol.TBinaryProtocolFactory()\n TServer.TSimpleServer(processor, transport, tfactory, pfactory)\n```\n\n## Usage with Replicated backend\n\nThe `ReplicatedFeatureFlagStore` is meant for cases where you have a primary store and one or more secondary stores that you want to replicate your writes to. For example, if you wanted to write to redis, but also record these writes to an auditing system somewhere else.\n\nThis store takes a primary store, which must be an instance of `AbstractFeatureFlagStore`, and then 0 or more other instances to act as the replicas.\n\nWhen you do any write operations, such as `create`, `set`, `delete`, or `set_meta`, these actions are first performed on the primary store, and then repeated on each of the secondary stores. **Important: no attempt is made to provide transaction-style consistency or rollbacks across writes**. Read operations will always pull from the primary store.\n\nBy default, the write operations are replicated asynchronously. To replicate synchronously, pass `asynch=False` to any of the methods.\n\n\n```python\nimport redis\nfrom flipper import (\n FeatureFlagClient,\n RedisFeatureFlagStore,\n ReplicatedFeatureFlagStore,\n)\n\n\nprimary_redis = redis.Redis(host='localhost', port=6379, db=0)\nbackup_redis = redis.Redis(host='localhost', port=6379, db=1)\n\nprimary = RedisFeatureFlagStore(primary_redis, base_key='feature-flags')\nreplica = RedisFeatureFlagStore(backup_redis, base_key='feature-flags')\n\nstore = ReplicatedFeatureFlagStore(primary, replica)\n\nclient = FeatureFlagClient(store)\n```\n\n## Usage with S3 backend\n\nTo store flag data in S3, use the `S3FeatureFlagStore`. Simply create the bucket, initialize a `boto3` (not `boto`) S3 client, and launch an instance of `S3FeatureFlagStore`, passing the client and the bucket name. The store will write to the root of the bucket using the flag name as the object key. For example usage, take a look at the tests. For more information on working with boto3, see [the documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html).\n\nKeep in mind that S3 is not an ideal choice for a production backend due to higher latency when compared to something like redis. However, it can be useful when used with [`ReplicatedFeatureFlagStore`](#usage-with-replicated-backend) as a replica for cold storage and backups. That way if your hot storage gets wiped out you have a backup or if you need an easy way to copy all the feature flag data it can be retrieved from S3.\n\n\n```python\nimport boto3\nfrom flipper import FeatureFlagClient, S3FeatureFlagStore\n\n\ns3 = boto3.client('s3')\n\nstore = S3FeatureFlagStore(s3, 'my-flipper-bucket')\n\nclient = FeatureFlagClient(store)\n```\n\n# Creating a custom backend\n\nDon't see the backend you like? You can easily implement your own. If you define a class that implements the `AbstractFeatureFlagStore` interface, located in `flipper.contrib.store` then you can pass an instance of it to the `FeatureFlagClient` constructor.\n\nPull requests welcome.\n\n# Events\n\nFlipper ships with a system for hooking into events. It is set up as an event emitter. You can register subscribers with the event emitter in order to react to events. The supported events are:\n\n- `PRE_CREATE`\n- `POST_CREATE`\n- `PRE_ENABLE`\n- `POST_ENABLE`\n- `PRE_DISABLE`\n- `POST_DISABLE`\n- `PRE_DESTROY`\n- `POST_DESTROY`\n- `PRE_ADD_CONDITION`\n- `POST_ADD_CONDITION`\n- `PRE_SET_CONDITIONS`\n- `POST_SET_CONDITIONS`\n- `PRE_SET_CLIENT_DATA`\n- `POST_SET_CLIENT_DATA`\n- `PRE_SET_BUCKETER`\n- `POST_SET_BUCKETER`\n\nTo register for these events, simply register listeners with the `events` property of `FeatureFlagClient` and use it like an [event emitter](https://pyee.readthedocs.io/en/latest/#pyee.BaseEventEmitter).\n\n```python\nfrom flipper import FeatureFlagClient, MemoryFeatureFlagStore\nfrom flipper.events import EventType\n\n\ndef on_post_create(feature_name, is_enabled, client_data):\n print(feature_name, is_enabled, client_data)\n\n\nclient = FeatureFlagClient(MemoryFeatureFlagStore())\nclient.events.on(EventType.POST_CREATE, f=on_post_create)\n\nclient.create('HOMEPAGE_AB_TEST', is_enabled=True, client_data={\"creator\": \"adambom\"})\n# > HOMEPAGE_AB_TEST True {\"creator\": \"adambom\"}\n```\n\nThe event emitter also works as a decorator:\n\n```python\nclient.events.on(EventType.POST_CREATE)\ndef on_post_create(feature_name, is_enabled, client_data):\n print(feature_name, is_enabled, client_data)\n```\n\nYou can substitute your own event emitter for the default by setting the events property. The custom event emitter must implement `flipper.events.IEventEmitter`.\n\n```python\nclient.events = MyCustomEventEmitter()\n```\n\nFor the full usage of `FlipperEventEmitter` see the [pyee documentation](https://pyee.readthedocs.io/en/latest/#pyee.BaseEventEmitter).\n\n## Subscribers\n\nFlipper also exposes a `FlipperEventSubscriber` interface. It allows you to implement a method for each event type. You can then register this subscriber with the event emitter and it will call the appropriate methods. The event emitter exposes the methods `register_subscriber` and `remove_subscriber` for this purpose. For example:\n\n```python\nimport logging\n\nfrom flipper import FeatureFlagClient, MemoryFeatureFlagStore\nfrom flipper.events import FlipperEventSubscriber\n\n\nclass LoggingEventSubscriber(FlipperEventSubscriber):\n def __init__(self, logger):\n self._logger = logger\n\n def on_post_create(self, feature_name, is_enabled, client_data):\n self._logger.info(\"flipper.create\", extra={\n \"feature_name\": feature_name,\n \"is_enabled\": is_enabled,\n \"client_data\": client_data,\n })\n\n def on_post_enable(self, feature_name):\n self._logger.info(\"flipper.enable\", extra={\"feature_name\": feature_name})\n\n def on_post_disable(self, feature_name):\n self._logger.info(\"flipper.disable\", extra={\"feature_name\": feature_name})\n\n def on_post_destroy(self, feature_name):\n self._logger.info(\"flipper.destroy\", extra={\"feature_name\": feature_name})\n\n def on_post_add_condition(self, feature_name, condition):\n self._logger.info(\"flipper.add_condition\", extra={\n \"feature_name\": feature_name,\n \"condition\": condition.to_dict(),\n })\n\n def on_post_set_client_data(self, feature_name, client_data):\n self._logger.info(\"flipper.set_client_data\", extra={\n \"feature_name\": feature_name,\n \"client_data\": client_data,\n })\n\n def on_post_set_bucketer(self, feature_name, bucketer):\n self._logger.info(\"flipper.set_bucketer\", extra={\n \"feature_name\": feature_name,\n \"bucketer\": bucketer.to_dict(),\n })\n\n\nlogger = logging.getLogger(\"application\")\n\nclient = FeatureFlagClient(MemoryFeatureFlagStore())\nclient.events.register_subscriber(LoggingEventSubscriber(logger))\n```\n\n# Development\n\nClone the repo and run `make install-dev` to get the environment set up. Test are run with the `pytest` command.\n\n\n## Building thrift files\n\nFirst, [install the thrift compiler](https://thrift.apache.org/tutorial/). On mac, the easiest way is to use homebrew:\n\n```\nbrew install thrift\n```\n\nThen simply run `make thrift`. Remember to commit the results of the compilation step.\n\n# System requirements\n\nThis project requires python version 3 or greater.\n\n# Open Source\n\nThis library is made availble as open source under the Apache 2.0 license. This is not an officially supported Carta product.\n\n## Development status\n\nThis project is actively maintained by the maintainers listed in the MAINTAINERS file. There are no major items on the project roadmap at this time, however bug fixes and new features may be added from time to time. We are open to contributions from the community as well.\n\n## Contacts\n\nThe project maintainers can be reached via email at adam.savitzky@carta.com or luis.montiel@carta.com.\n\n## Discussion\n\nWe use github issues for discussing features, bugs, and other project related issues.\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "", "keywords": "", "license": "Apache License 2.0", "maintainer": "", "maintainer_email": "", "name": "flipper-client", "package_url": "https://pypi.org/project/flipper-client/", "platform": null, "project_url": "https://pypi.org/project/flipper-client/", "project_urls": null, "release_url": "https://pypi.org/project/flipper-client/1.2.9/", "requires_dist": [ "cachetools (~=4.2.1)", "python-consul (~=1.0)", "redis (<5,>=2.10.6)", "thrift (~=0.13)", "boto3 (~=1.9)", "pyee (~=6.0)", "six (>=1.12) ; extra == 'dev'", "fakeredis (~=1.0) ; extra == 'dev'", "pytest (~=7.1.0) ; extra == 'dev'", "ipython ; extra == 'dev'", "thrift ; extra == 'dev'", "setuptools ; extra == 'dev'", "wheel ; extra == 'dev'", "ipdb ; extra == 'dev'", "black (==22.1.0) ; extra == 'dev'", "pre-commit ; extra == 'dev'", "isort ; extra == 'dev'", "flake8 ; extra == 'dev'", "mypy ; extra == 'dev'", "moto ; extra == 'dev'", "bandit ; extra == 'dev'", "twine ; extra == 'dev'" ], "requires_python": "", "summary": "", "version": "1.2.9", "yanked": false, "yanked_reason": null }, "last_serial": 13350109, "releases": { "0.2.8": [ { "comment_text": "", "digests": { "md5": "0b9d94d90a0c9ef43632b3ffeb64e536", "sha256": "2b9b125738c026696c40d91aa439d97ab2dcd94031f1ac7e521b7c837d46c4d7" }, "downloads": -1, "filename": "flipper_client-0.2.8-py3-none-any.whl", "has_sig": false, "md5_digest": "0b9d94d90a0c9ef43632b3ffeb64e536", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 80769, "upload_time": "2019-03-27T16:59:41", "upload_time_iso_8601": "2019-03-27T16:59:41.173950Z", "url": "https://files.pythonhosted.org/packages/91/10/031ed8cfdc0a98edf0daf2088e3cd8ca093c21a76feb9eb662f5a13d3981/flipper_client-0.2.8-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "5971303f7611edd209be10855f4def9f", "sha256": "457ac337efc4db3ab75204794e235f04ad312533c4bf26ce285802aa6385daec" }, "downloads": -1, "filename": "flipper-client-0.2.8.tar.gz", "has_sig": false, "md5_digest": "5971303f7611edd209be10855f4def9f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48005, "upload_time": "2019-03-27T16:59:43", "upload_time_iso_8601": "2019-03-27T16:59:43.445180Z", "url": "https://files.pythonhosted.org/packages/d0/3d/f63aa4c4d6ef3346d229a6ad7e40ba69fd5d5beeceb924f9a0274f15be65/flipper-client-0.2.8.tar.gz", "yanked": false, "yanked_reason": null } ], "0.2.9": [ { "comment_text": "", "digests": { "md5": "de8f1da0c95fbd1379069d9a9520f6c4", "sha256": "2fa028a09cf11333eb0369bb173e140064f00e3182a88cd3fa9d9919c1b978c5" }, "downloads": -1, "filename": "flipper_client-0.2.9-py3-none-any.whl", "has_sig": false, "md5_digest": "de8f1da0c95fbd1379069d9a9520f6c4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 80808, "upload_time": "2019-05-10T20:08:11", "upload_time_iso_8601": "2019-05-10T20:08:11.936594Z", "url": "https://files.pythonhosted.org/packages/43/e7/55a30c1e03be660675655ce66053866784f9e8ad41493c712d136a1a141e/flipper_client-0.2.9-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "027b2c4289e79411e6508f63a8a86f68", "sha256": "075afc8f5bd2f495ba270c2fa2a6382f22a096aa939056fd5a543a335c8d491f" }, "downloads": -1, "filename": "flipper-client-0.2.9.tar.gz", "has_sig": false, "md5_digest": "027b2c4289e79411e6508f63a8a86f68", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48122, "upload_time": "2019-05-10T20:08:13", "upload_time_iso_8601": "2019-05-10T20:08:13.481608Z", "url": "https://files.pythonhosted.org/packages/9c/7f/586e7456f6be5f07a1ca306991ea7da3cc2fb70022f224c3bbd50904bce0/flipper-client-0.2.9.tar.gz", "yanked": false, "yanked_reason": null } ], "1.0.0": [ { "comment_text": "", "digests": { "md5": "636a3344455bd8f3c8d8af656cd0f0be", "sha256": "adc6972fc1b746ef6098e8ac61d0dead1daa63643762cbcdffc1863934cecbe6" }, "downloads": -1, "filename": "flipper_client-1.0.0-py3-none-any.whl", "has_sig": false, "md5_digest": "636a3344455bd8f3c8d8af656cd0f0be", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 82999, "upload_time": "2019-07-18T21:29:12", "upload_time_iso_8601": "2019-07-18T21:29:12.684312Z", "url": "https://files.pythonhosted.org/packages/bb/3e/1e33ab2385a1e84939a39762095c1a7d2c3cc309ee58ab74a6529d7158f9/flipper_client-1.0.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "8f8fba9759646a1b3cd8ba8661e7b005", "sha256": "c8a1189c774182ec57b55e23573a452796ccf3fb32ec4ad76fc18cf1d0988a4c" }, "downloads": -1, "filename": "flipper-client-1.0.0.tar.gz", "has_sig": false, "md5_digest": "8f8fba9759646a1b3cd8ba8661e7b005", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 50549, "upload_time": "2019-07-18T21:29:14", "upload_time_iso_8601": "2019-07-18T21:29:14.760026Z", "url": "https://files.pythonhosted.org/packages/73/a4/b37d5b7a88f03abe615ed8c54c86f5d9192d578ee12396040af933d95de8/flipper-client-1.0.0.tar.gz", "yanked": false, "yanked_reason": null } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "47d65d919a42a2b7a62c17f0ff85128c", "sha256": "14029f19b585bd9396e7ad157b166f521097e829533f9045035b88a983174528" }, "downloads": -1, "filename": "flipper_client-1.0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "47d65d919a42a2b7a62c17f0ff85128c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 83156, "upload_time": "2019-08-10T00:06:17", "upload_time_iso_8601": "2019-08-10T00:06:17.107148Z", "url": "https://files.pythonhosted.org/packages/de/bc/723c93f6efc870baed03f4fc9b576a5b96741f5ec9d60fc72ba3d46b4e5d/flipper_client-1.0.1-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "bbc896dc21212640dab5b0e00a4502ba", "sha256": "88b730bbad090ccdabb1d849abdda04a9498d76a19e2c1cb7bd87be1cf84bc68" }, "downloads": -1, "filename": "flipper-client-1.0.1.tar.gz", "has_sig": false, "md5_digest": "bbc896dc21212640dab5b0e00a4502ba", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 50688, "upload_time": "2019-08-10T00:06:19", "upload_time_iso_8601": "2019-08-10T00:06:19.303874Z", "url": "https://files.pythonhosted.org/packages/f9/a2/b2676838c33c3f23d832c04df703b5f564d02bc7f523b09c37f5d2fcb9a3/flipper-client-1.0.1.tar.gz", "yanked": false, "yanked_reason": null } ], "1.0.2": [ { "comment_text": "", "digests": { "md5": "cec837c5890f13dfe11018449e8d991b", "sha256": "07a517e941fa8bc56adf99e798f4d70531d877004054014caf3d83ec1e46a2ca" }, "downloads": -1, "filename": "flipper_client-1.0.2-py3-none-any.whl", "has_sig": false, "md5_digest": "cec837c5890f13dfe11018449e8d991b", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 83167, "upload_time": "2019-08-12T16:15:32", "upload_time_iso_8601": "2019-08-12T16:15:32.018432Z", "url": "https://files.pythonhosted.org/packages/f2/3f/49a617a5b1fa90b9f56fd7af1002d7378a5e0907e1bdf011b27f9a4882ee/flipper_client-1.0.2-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "cc6a49f172c01ed941b00ab3f7360564", "sha256": "09802df19a7192962ce15758e24ebbf0f4c3f4f6f81cdb90b66c8775ecff395e" }, "downloads": -1, "filename": "flipper-client-1.0.2.tar.gz", "has_sig": false, "md5_digest": "cc6a49f172c01ed941b00ab3f7360564", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 50774, "upload_time": "2019-08-12T16:15:34", "upload_time_iso_8601": "2019-08-12T16:15:34.271291Z", "url": "https://files.pythonhosted.org/packages/6f/06/311d4d47541bb4e21a813a44df67ea839cca8b941852a091996cdfb1ea9c/flipper-client-1.0.2.tar.gz", "yanked": false, "yanked_reason": null } ], "1.0.3": [ { "comment_text": "", "digests": { "md5": "326d6bf88562449c6f3b0a2a8bc9fcd8", "sha256": "2fe5dfa60c4baba36d48d973af0b6adbedc10e951b2ee515d1110221f9b8f3b1" }, "downloads": -1, "filename": "flipper_client-1.0.3-py3-none-any.whl", "has_sig": false, "md5_digest": "326d6bf88562449c6f3b0a2a8bc9fcd8", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 83234, "upload_time": "2019-08-12T20:21:00", "upload_time_iso_8601": "2019-08-12T20:21:00.434582Z", "url": "https://files.pythonhosted.org/packages/67/6e/877b82cfb0f61e35eb2ca0ca061b1380d070cc39c0eca9da8cf2af7a4a18/flipper_client-1.0.3-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "86c9b3bf235d256abc0200ed2d5d3764", "sha256": "bac642dda1e59b16d67b20fca22e8cc4cebee19200b4753ebedc050bb750a326" }, "downloads": -1, "filename": "flipper-client-1.0.3.tar.gz", "has_sig": false, "md5_digest": "86c9b3bf235d256abc0200ed2d5d3764", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 50830, "upload_time": "2019-08-12T20:21:02", "upload_time_iso_8601": "2019-08-12T20:21:02.194801Z", "url": "https://files.pythonhosted.org/packages/5e/d3/37cc5318d1426b0d2547cc9f759c66c1f95249293826eeacc87b7ffc7ed9/flipper-client-1.0.3.tar.gz", "yanked": false, "yanked_reason": null } ], "1.0.4": [ { "comment_text": "", "digests": { "md5": "a27042953926e980db9556640b50527f", "sha256": "d2b78ab58e84c0ee6613128e71c8f625648e64235567c00fe09662c9bb4ea70f" }, "downloads": -1, "filename": "flipper_client-1.0.4-py3-none-any.whl", "has_sig": false, "md5_digest": "a27042953926e980db9556640b50527f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 84849, "upload_time": "2019-10-28T17:12:44", "upload_time_iso_8601": "2019-10-28T17:12:44.820750Z", "url": "https://files.pythonhosted.org/packages/a9/b5/cff08fe83f4fc784b2ed5f10f38776d8a9c127307ae9fe3dcb56fdc24cd2/flipper_client-1.0.4-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "9165e53f171cd9c0ad67e11dae61199c", "sha256": "4ed158465c249d3a320409b01f10dc5193900006a7486bb5d577a69919cef6f1" }, "downloads": -1, "filename": "flipper-client-1.0.4.tar.gz", "has_sig": false, "md5_digest": "9165e53f171cd9c0ad67e11dae61199c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 51649, "upload_time": "2019-10-28T17:12:47", "upload_time_iso_8601": "2019-10-28T17:12:47.101527Z", "url": "https://files.pythonhosted.org/packages/c2/bd/01bfa20da7f62146fa175475370de392e91c9d449f7eb24df593c61ca54d/flipper-client-1.0.4.tar.gz", "yanked": false, "yanked_reason": null } ], "1.0.6": [ { "comment_text": "", "digests": { "md5": "16140c9d8df3058663821c11b5528151", "sha256": "96a8559064f5bdd240189613f4977b5ce1d7d3b9d56191d2b7a1c9b41feb7753" }, "downloads": -1, "filename": "flipper_client-1.0.6-py3-none-any.whl", "has_sig": false, "md5_digest": "16140c9d8df3058663821c11b5528151", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 90899, "upload_time": "2019-11-14T20:17:00", "upload_time_iso_8601": "2019-11-14T20:17:00.090680Z", "url": "https://files.pythonhosted.org/packages/78/34/ccc5efa6004365e3bc3a2e2b8d6edca8d4850d33a45ea468f74f217dba26/flipper_client-1.0.6-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "bfbff190c01fb89553c06732ee09ca8a", "sha256": "3454e1c80416872662aa498eb5eed43557110e146e73d057bc60640fd750a75b" }, "downloads": -1, "filename": "flipper-client-1.0.6.tar.gz", "has_sig": false, "md5_digest": "bfbff190c01fb89553c06732ee09ca8a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57008, "upload_time": "2019-11-14T20:17:03", "upload_time_iso_8601": "2019-11-14T20:17:03.731640Z", "url": "https://files.pythonhosted.org/packages/c5/70/3e3afed8641c5aa3d940663c0b7f575bf3b605cb56e22ed4b5363c0a63c7/flipper-client-1.0.6.tar.gz", "yanked": false, "yanked_reason": null } ], "1.1.0": [ { "comment_text": "", "digests": { "md5": "f6cd7bae16f0448ccfba55f363072cf7", "sha256": "63f781c79c8b3f43d98048f257512d4b4f515a372e0faac1bd8a6ec19aca6962" }, "downloads": -1, "filename": "flipper_client-1.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "f6cd7bae16f0448ccfba55f363072cf7", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 91726, "upload_time": "2020-02-06T16:49:08", "upload_time_iso_8601": "2020-02-06T16:49:08.608838Z", "url": "https://files.pythonhosted.org/packages/53/95/3f16b40dde53ef8ba94f1661828927cc9ff1a7a7d937ea12567b033a1610/flipper_client-1.1.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "0443a7505d0b18da4a52f5742bc5df8c", "sha256": "14b8f346da66849e02e14e53ddc489a4d8ef0a9a184685ed78b4d4cee6cafcf3" }, "downloads": -1, "filename": "flipper-client-1.1.0.tar.gz", "has_sig": false, "md5_digest": "0443a7505d0b18da4a52f5742bc5df8c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57799, "upload_time": "2020-02-06T16:49:10", "upload_time_iso_8601": "2020-02-06T16:49:10.473810Z", "url": "https://files.pythonhosted.org/packages/0f/68/9778352101ae9f6c3f23b2dc88c01cce22bf48d0aae43087099997cc2e72/flipper-client-1.1.0.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.0": [ { "comment_text": "", "digests": { "md5": "16d3d63db0c71030fa9f4a7bc1ac9277", "sha256": "44ab0971f31f1ae7d69bf2aa713fd400eb734bacc94215e0aceae9baa7992943" }, "downloads": -1, "filename": "flipper_client-1.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "16d3d63db0c71030fa9f4a7bc1ac9277", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 91824, "upload_time": "2020-05-26T16:03:47", "upload_time_iso_8601": "2020-05-26T16:03:47.601196Z", "url": "https://files.pythonhosted.org/packages/88/db/6ea2a35b312177b10e7ccb2649c1cb394e2ed5c4364adee9617a1ce59991/flipper_client-1.2.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "4d11c140d900428b0c15b73c070d951c", "sha256": "d417a87abf1fc0f1a060dfd044899645d7c86c33d9c34f8d09f25dcd5453ebc4" }, "downloads": -1, "filename": "flipper-client-1.2.0.tar.gz", "has_sig": false, "md5_digest": "4d11c140d900428b0c15b73c070d951c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57916, "upload_time": "2020-05-26T16:03:49", "upload_time_iso_8601": "2020-05-26T16:03:49.230400Z", "url": "https://files.pythonhosted.org/packages/82/7a/8b4d9fdaffa0885250a4336192cc45d47ec2f541faf1e34db2be27273635/flipper-client-1.2.0.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.1": [ { "comment_text": "", "digests": { "md5": "f899622cf5bad443e64d0e2379f325d6", "sha256": "9e99304290cb99fce395e4896d2cca7b7efe31942ae4c09a985a774616af167d" }, "downloads": -1, "filename": "flipper_client-1.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "f899622cf5bad443e64d0e2379f325d6", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 91832, "upload_time": "2020-05-27T21:20:12", "upload_time_iso_8601": "2020-05-27T21:20:12.848221Z", "url": "https://files.pythonhosted.org/packages/53/6a/68a5a77e311d6062be9e2a0574f5fa83dcb38951cf6bc6cec2c7e7cdd7ab/flipper_client-1.2.1-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "d59c3a1e0f97221cbbb519c8504c0a57", "sha256": "05818db82b4608f314892d76bb931fe720a99df213291db4f81a0e659ee46ff4" }, "downloads": -1, "filename": "flipper-client-1.2.1.tar.gz", "has_sig": false, "md5_digest": "d59c3a1e0f97221cbbb519c8504c0a57", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57923, "upload_time": "2020-05-27T21:20:14", "upload_time_iso_8601": "2020-05-27T21:20:14.597483Z", "url": "https://files.pythonhosted.org/packages/4e/de/bbf7c3e8c3ffec5d7f6d3bf2cd2f7f162bed3e8056f19b07665971d06adc/flipper-client-1.2.1.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.2": [ { "comment_text": "", "digests": { "md5": "2106d0aa09d5a83610ae1615565feee6", "sha256": "23ceb6faf0942bc76498d311b1a675e599afeddb29f487a32d3ca767601cef4e" }, "downloads": -1, "filename": "flipper_client-1.2.2-py3-none-any.whl", "has_sig": false, "md5_digest": "2106d0aa09d5a83610ae1615565feee6", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 91925, "upload_time": "2020-06-04T20:05:14", "upload_time_iso_8601": "2020-06-04T20:05:14.136147Z", "url": "https://files.pythonhosted.org/packages/8b/a4/ef4c5e98ac58e07e6162970de41e3d2aea62e147626de04de3ad7e8f6fdb/flipper_client-1.2.2-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "18fd1c5025145d633bda498a7097b047", "sha256": "89199475fb43b8a737cffd8d480ce378311bdd4f0b64a633db033eab203bab00" }, "downloads": -1, "filename": "flipper-client-1.2.2.tar.gz", "has_sig": false, "md5_digest": "18fd1c5025145d633bda498a7097b047", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57992, "upload_time": "2020-06-04T20:05:15", "upload_time_iso_8601": "2020-06-04T20:05:15.913987Z", "url": "https://files.pythonhosted.org/packages/88/6b/def4e6add2312384c63f3288075e2c77621275fa8d6ff8e59430c7e5753a/flipper-client-1.2.2.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.3": [ { "comment_text": "", "digests": { "md5": "7424a0ddc909ee952c66120968e0cb39", "sha256": "08483ff2061c578c33e2fac9830bc8bfd511aaaff5deccc77dede9cdb852436e" }, "downloads": -1, "filename": "flipper_client-1.2.3-py3-none-any.whl", "has_sig": false, "md5_digest": "7424a0ddc909ee952c66120968e0cb39", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 91928, "upload_time": "2020-08-04T19:28:24", "upload_time_iso_8601": "2020-08-04T19:28:24.619391Z", "url": "https://files.pythonhosted.org/packages/3e/d9/23156de700bd866dde92ed4ac34fa13869005ad964cf3f67a7a75cf93504/flipper_client-1.2.3-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "321a26a071e33ebc585607afd156da7d", "sha256": "81f4ea7f894e22b0b1fbcd947b97b9649578e23044812c2fc0110024d17f00d7" }, "downloads": -1, "filename": "flipper-client-1.2.3.tar.gz", "has_sig": false, "md5_digest": "321a26a071e33ebc585607afd156da7d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57992, "upload_time": "2020-08-04T19:28:26", "upload_time_iso_8601": "2020-08-04T19:28:26.277213Z", "url": "https://files.pythonhosted.org/packages/94/a0/b1d48bfec92e95f95e3696ff07cd34ff2aac103a651ed382cd399411edf3/flipper-client-1.2.3.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.4": [ { "comment_text": "", "digests": { "md5": "9aa52b37cfd6e2e98a345dfbb3d61d2c", "sha256": "6bd7bddda65fea84f07ae8a0cd39ffe18777017cf169bd56df327eb51409a2aa" }, "downloads": -1, "filename": "flipper_client-1.2.4-py3-none-any.whl", "has_sig": false, "md5_digest": "9aa52b37cfd6e2e98a345dfbb3d61d2c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 91927, "upload_time": "2020-08-13T20:06:26", "upload_time_iso_8601": "2020-08-13T20:06:26.428542Z", "url": "https://files.pythonhosted.org/packages/71/da/7e3e72d3db8120381fde04a094036c9debc0975f6d18419de28d5d023f48/flipper_client-1.2.4-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "967aec68603621d8b15a7bf575bdb185", "sha256": "cec12b7aaffd3198b28d96a195b897cd3c50eb6c545d5e0e5a54b81ec9b5e217" }, "downloads": -1, "filename": "flipper-client-1.2.4.tar.gz", "has_sig": false, "md5_digest": "967aec68603621d8b15a7bf575bdb185", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57978, "upload_time": "2020-08-13T20:06:28", "upload_time_iso_8601": "2020-08-13T20:06:28.111329Z", "url": "https://files.pythonhosted.org/packages/0a/73/85ed817bb8a8e2d58b78fcfec201f7a89fe70c13b336cc2a729104385f1c/flipper-client-1.2.4.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.5": [ { "comment_text": "", "digests": { "md5": "e25660553399966ac905b28414575f8e", "sha256": "14fa88c1a5ab99445835a7c60147716cceca6674f420021bf1fffe1d707c23c5" }, "downloads": -1, "filename": "flipper_client-1.2.5-py3-none-any.whl", "has_sig": false, "md5_digest": "e25660553399966ac905b28414575f8e", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 92431, "upload_time": "2020-11-19T17:58:22", "upload_time_iso_8601": "2020-11-19T17:58:22.131366Z", "url": "https://files.pythonhosted.org/packages/b2/1e/3d1c8a38a6ca67479bb398c6fa7b4f7d67a22d88a144a72a0fe27b5ed4f8/flipper_client-1.2.5-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "7b8b9700189268a3f9080a8fa4b7e3c4", "sha256": "2543102593260a518dd4bd1e639d433b0472fcfb3f680f8284e8e36d8316504f" }, "downloads": -1, "filename": "flipper-client-1.2.5.tar.gz", "has_sig": false, "md5_digest": "7b8b9700189268a3f9080a8fa4b7e3c4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 58214, "upload_time": "2020-11-19T17:58:24", "upload_time_iso_8601": "2020-11-19T17:58:24.000629Z", "url": "https://files.pythonhosted.org/packages/13/62/31fbdc65cf7e941755d2b5d6c1f1084b277cde3944bcd7afccc2ee7fd283/flipper-client-1.2.5.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.7": [ { "comment_text": "", "digests": { "md5": "301e253863c8c8667f5da76187064b84", "sha256": "27ddeb7d75a8904d093d4cafe0a38593862ff5f3ff97e6d421dd0e09042ac864" }, "downloads": -1, "filename": "flipper_client-1.2.7-py3-none-any.whl", "has_sig": false, "md5_digest": "301e253863c8c8667f5da76187064b84", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 92419, "upload_time": "2021-02-12T17:43:56", "upload_time_iso_8601": "2021-02-12T17:43:56.035637Z", "url": "https://files.pythonhosted.org/packages/c7/c1/702ba63f8b39494678e6a51f9f75abb03c74446061248501143411e29e2f/flipper_client-1.2.7-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "9348cc932a9b39e9ee48252f18a70c3f", "sha256": "a88cd079588ee106fa858874f20c8439978d835c5dc1da0fe4c73d13f41256e8" }, "downloads": -1, "filename": "flipper-client-1.2.7.tar.gz", "has_sig": false, "md5_digest": "9348cc932a9b39e9ee48252f18a70c3f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 58377, "upload_time": "2021-02-12T17:43:57", "upload_time_iso_8601": "2021-02-12T17:43:57.903822Z", "url": "https://files.pythonhosted.org/packages/58/e8/9617ce37697d09c56a2f19567b7d07a8185100d62b2415321d1333163b7f/flipper-client-1.2.7.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.8": [ { "comment_text": "", "digests": { "md5": "60c7d1af117560f4a3809bd0bff87265", "sha256": "1fac4c099f3aaede1fdb3622e1a564f7562b4e1d7479b85431d203dfb55eb27b" }, "downloads": -1, "filename": "flipper_client-1.2.8-py3-none-any.whl", "has_sig": false, "md5_digest": "60c7d1af117560f4a3809bd0bff87265", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 92980, "upload_time": "2021-09-30T18:06:37", "upload_time_iso_8601": "2021-09-30T18:06:37.160888Z", "url": "https://files.pythonhosted.org/packages/95/27/29a9bc7561890f8797385c55d9cb7e0295c3fc6a5400076b4b8c8a892095/flipper_client-1.2.8-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "b5af85e6f0f3f2f56bd1c6d6b745798a", "sha256": "07ee588817858cb55950f5bd2963c110c424f06d608beade0112fa65def4b645" }, "downloads": -1, "filename": "flipper-client-1.2.8.tar.gz", "has_sig": false, "md5_digest": "b5af85e6f0f3f2f56bd1c6d6b745798a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 55470, "upload_time": "2021-09-30T18:06:39", "upload_time_iso_8601": "2021-09-30T18:06:39.696897Z", "url": "https://files.pythonhosted.org/packages/eb/9b/5522a4035822217c7897d4b8ea381cc57018e75fb3c6bfdafde89d5b75ed/flipper-client-1.2.8.tar.gz", "yanked": false, "yanked_reason": null } ], "1.2.9": [ { "comment_text": "", "digests": { "md5": "ccc7e94266e3aca958adc2ae7bb860eb", "sha256": "7ce98b49140bb3d133e63a44a0b57785b9510b6481d90420cfb56d3d418952d2" }, "downloads": -1, "filename": "flipper_client-1.2.9-py3-none-any.whl", "has_sig": false, "md5_digest": "ccc7e94266e3aca958adc2ae7bb860eb", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 92976, "upload_time": "2022-03-30T18:59:03", "upload_time_iso_8601": "2022-03-30T18:59:03.816883Z", "url": "https://files.pythonhosted.org/packages/55/d8/eaad4db15500fd7c0d93167d377d2ac09b4935614ebc75cfcfe9f515ead2/flipper_client-1.2.9-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "753f5cf3ac9d76be9c5de2761e07dc21", "sha256": "b7dee461daff9956f4f9d5e9b03b6047c62d7f8e5ca4b033798137ade3f65a53" }, "downloads": -1, "filename": "flipper-client-1.2.9.tar.gz", "has_sig": false, "md5_digest": "753f5cf3ac9d76be9c5de2761e07dc21", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 55466, "upload_time": "2022-03-30T18:59:06", "upload_time_iso_8601": "2022-03-30T18:59:06.714701Z", "url": "https://files.pythonhosted.org/packages/74/d5/1c72630e4ed4106a0f1771a1bb7721a79f1b9bf6f6bd26138459c947f9b1/flipper-client-1.2.9.tar.gz", "yanked": false, "yanked_reason": null } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "ccc7e94266e3aca958adc2ae7bb860eb", "sha256": "7ce98b49140bb3d133e63a44a0b57785b9510b6481d90420cfb56d3d418952d2" }, "downloads": -1, "filename": "flipper_client-1.2.9-py3-none-any.whl", "has_sig": false, "md5_digest": "ccc7e94266e3aca958adc2ae7bb860eb", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 92976, "upload_time": "2022-03-30T18:59:03", "upload_time_iso_8601": "2022-03-30T18:59:03.816883Z", "url": "https://files.pythonhosted.org/packages/55/d8/eaad4db15500fd7c0d93167d377d2ac09b4935614ebc75cfcfe9f515ead2/flipper_client-1.2.9-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "753f5cf3ac9d76be9c5de2761e07dc21", "sha256": "b7dee461daff9956f4f9d5e9b03b6047c62d7f8e5ca4b033798137ade3f65a53" }, "downloads": -1, "filename": "flipper-client-1.2.9.tar.gz", "has_sig": false, "md5_digest": "753f5cf3ac9d76be9c5de2761e07dc21", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 55466, "upload_time": "2022-03-30T18:59:06", "upload_time_iso_8601": "2022-03-30T18:59:06.714701Z", "url": "https://files.pythonhosted.org/packages/74/d5/1c72630e4ed4106a0f1771a1bb7721a79f1b9bf6f6bd26138459c947f9b1/flipper-client-1.2.9.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] }