{"id":18763796,"url":"https://github.com/roverdotcom/feats.py","last_synced_at":"2025-04-13T04:32:52.301Z","repository":{"id":146066904,"uuid":"226403675","full_name":"roverdotcom/feats.py","owner":"roverdotcom","description":"A Feature Flag Library for Python Applications","archived":true,"fork":false,"pushed_at":"2020-04-06T17:09:20.000Z","size":248,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":41,"default_branch":"master","last_synced_at":"2025-02-19T12:52:18.948Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/roverdotcom.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-12-06T20:21:41.000Z","updated_at":"2024-01-12T18:13:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"e0e92890-9fa3-400b-81ae-d437602887e8","html_url":"https://github.com/roverdotcom/feats.py","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roverdotcom%2Ffeats.py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roverdotcom%2Ffeats.py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roverdotcom%2Ffeats.py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roverdotcom%2Ffeats.py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roverdotcom","download_url":"https://codeload.github.com/roverdotcom/feats.py/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248664229,"owners_count":21141917,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T18:27:29.035Z","updated_at":"2025-04-13T04:32:52.288Z","avatar_url":"https://github.com/roverdotcom.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"![](https://github.com/roverdotcom/feats.py/workflows/Unit%20Tests/badge.svg?branch=master) ![](https://github.com/roverdotcom/feats.py/workflows/Integration%20Tests/badge.svg?branch=master)\n\n\n# COMING SOON!\n\nFeats.py is still actively being developed and as such, is **not** yet ready for\nreal world applications. Until then, please feel free to review this\ndocumentation and keep an eye on this repo for an initial release!\n\n# feats.py (WIP)\nFeats.py is a feature flag library for Python applications. We built it\nbased on our learnings from using the [Gargoyle](https://github.com/adamchainz/gargoyle)\nlibrary in production within a medium-sized engineering organization.\n\nInstead of giving the ability to turn features on or off, feats is based on\nthe ability to choose between implementations of a feature. This allows for\nnon-binary choices, which can help reduce the need for feature flags which depend\non other feature flags and encourge a more object-oriented programming style.\nIn the situations where there is a need to simply turn something on or off,\nfeats.py supports that as well.\n\n# Requirements\n\n* `\u003e=` Python 3.6\n* `\u003e=` Redis 5.0\n\n# Table of Contents\n\n* [App Setup](#app-Setup)\n* [Features](#features)\n* [Segments](#segments)\n* [Configuration](#configuration)\n* [Examples](#examples)\n  * [Ops Example](#operations)\n  * [Rollout Example](#rollout)\n  * [Experiment Example](#product-experiments)\n\n# App Setup\n\nIn order to use Feats, we must first configure an App with which to register\nfeatures and segments. At the moment, the only configuration of the App is the\nstorage backend to use.\n\nFeats contains two backend storages at the moment, an in-memory store and a redis\nbacked store. The in-memory store is only useful for testing environments. Redis\nshould be used in all other cases. Our Redis usage is based on streams, which require\nRedis 5.0 or higher.\n\nBy convention, the feats app should be placed in the \"feats.py\" file of your\nmodule.\n\n```python\n#myapp/feats.py\nimport feats\nimport myapp.config\napp = feats.App(storage=RedisStorage(redis=Redis(decode_responses=True)))\n```\n\nWhen we need to declare features and segments, we will then always use the\napp we have defined in myapp/feats.py `from myapp.feats import app`\n\n# Features\n\nNow that we have an App, we can start declaring Features.\n\nA Feature declares all of the implementations that can be interchangebly used.\nImplementations can be as simple as the button text to use, or as large as\nan entire replacement View to render.\n\nTo declare a feature, decorate a class with `app.feature`.\n```python\nfrom myapp.feats import app\n\n@app.feature\nclass ConfirmText:\n    @app.default\n    def submit(self) -\u003e str:\n        return \"Submit\"\n\n    def save(self) -\u003e str:\n        return \"Save\"\n```\n\nThe decorator replaces the class declaration with a factory-style object. We can\ncall the `create` method on this object to receive the confirmation text to use.\n\n```python\ntext = ConfirmText.create()\n```\n\nThe above will by default return \"Submit\" because the submit method was decorated\nas the default. All features are required to have a default.\n\nIn order to have \"Save\" returned, we can [configure](#configuration) feats to do so.\n\n## Boolean Features\n\nSometimes all we need is the ability to turn something on or off. For instance,\nwe may want to just disable processing images during a DDOS attack.\nFor this, we can use boolean features. These simply return `True` or `False`\ndepending on if they are enabled or disabled.\n\nThey can be declared using a single function instead of a class\n\n```python\n@app.boolean\ndef ImageProcessing() -\u003e bool:\n    return True # This is the default value\n```\n\nAnd can be used like so\n\n```python\nif ImageProcessing.is_enabled():\n    process_image()\n```\n\n# Segments\n\nWe will normally want to pass in data to a feature. This allows us to select\ncertain users to receive an implementation. Without segments, all users will\nhave to receive a single implementation.\n\nSegments tell feats how to group input objects. A segment declaration\nholds functions which can convert all of your business objects into that grouping.\n\nAll segments have a single typed input argument and must return strings.\n\nFor instance,\n```python\nfrom myapp.feats import app\n@app.segment\nclass Subdivision:\n    \"\"\"\n    The ISO 3166-2 Subdivision Code, e.g US-WA\n    \"\"\"\n    def user(self, user: User) -\u003e str:\n        return self.address(user.address)\n\n    def address(self, address: Address) -\u003e str:\n        return \"{}-{}\".format(address.country_code, address.subdivision_code)\n```\nis a valid segment. It declares how to convert both a user and an address into\na subdivision code.\n\nHowever, the following is not valid.\n```python\nfrom myapp.feats import app\n@app.segment\nclass Subdivision:\n    \"\"\"\n    The ISO 3166-2 Subdivision Code, e.g US-WA\n    \"\"\"\n    def user(self, user) -\u003e str: # Invalid, must declare the input type\n        return self.address(user.address)\n\n    def address(self, address: Address): # Invalid, must declare return type as str\n        return \"{}-{}\".format(address.country_code, address.subdivision_code)\n```\n\nIt is also possible to designate specific options for a segment. If, for\nexample, we wished to create a segment on device type and we knew the two\noptions we cared about were `\"android\"` and `\"ios\"`, we could create the\nfollowing segment:\n\n```python\n@app.segment\nclass UserDevice:\n    \"\"\"\n    The user device data from the Request, e.g \"ios\"\n    \"\"\"\n    OPTIONS = ['ios', 'android']\n\n    def request(self, request: Request) -\u003e str:\n        return request['device']\n```\n\nThis allows us to select from that list of `OPTIONS` when we define how this\nsegment should be routed to feature implementations.\n\n\n## Feature Inputs\n\nOnce we have segments declared, we can extend our features to take in objects.\n\nBoth class based features and function based features support this.\n\n```python\n@app.feature\nclass ConfirmText:\n    def submit(self, user: User) -\u003e str:\n        return translate(user.language, \"Submit\")\n\n    def save(self, user: User) -\u003e str:\n        return translate(user.language, \"Save\")\n\nConfirmText.create(user) # create will now require a user argument\n\n@app.boolean\ndef ImageProcessing(user: User) -\u003e bool:\n    return True\n\nImageProcessing.is_enabled(user) # is_enabled will also require a user argument\n```\n\nInside of a class, all of the implementations must take exactly the same arguments.\nLike segments, they also must be strictly typed. This typing lets feats know which\nsegments are valid for which features.\n\n```python\n@app.feature\nclass ConfirmText:\n    def submit(self, user) -\u003e str: # Invalid, must declare input type\n        return \"Submit\"\n    def save(self) -\u003e str: # Invalid, must have same number of inputs\n        return \"Save\"\n    def persist(self, request: HttpRequest) -\u003e str # Invalid, must have same input types\n        return \"Persist\"\n```\n\n## Preselecting Implementations\n\nWhen dealing with client-side applications, it can be beneficial for the client to poll ahead-of-time the implementation\nto use, without actually marking a user as \"bucketed\" for experiments. The client can then asyncronously notify the server\nthat the client has used that implementation to persist that data. This removes the network latency required to display a feature\nat the cost of some additional latency between the time a feature is updated and the client sees that update.\n\nIn order to do this, we can combine the Feature's `find_implementation` method to preselect implementations, alongside that Feature's\n`used_implementation` method to notify that the client has used a certain implementation.\n\nFor Features using an experiment, `find_implementation` will return a random implementation until that user has used an implementation. Calling a feature's `create` or `is_enabled` method will mark that user as having used the implementation returned.\n\n```python\nimpl_name = MyFeature.find_implementation(user) # Returns mapping from name of feature to name of implementation\n```\n\n```python\nMyFeature.used_implementation(impl_name, user)\n```\n\n# Configuration\n\n# Examples\n\n## Operations\n\nLet's say our application has the ability to charge credit cards using a generic\npayment provider. We have negotiated a good rate with a certain provider, and\nprefer to use them whenever possible.\nHowever, if they have technical difficulties, we still want the ability to\ncharge cards, even if it means more overhead.\n\nTo do this, we could declare a PaymentProcessor feature like so\n\n```python\n@app.feature\nclass PaymentProcessor:\n    @app.default\n    def acme(self, user: User) -\u003e AcmeProcessor:\n        \"\"\"\n        2% + $0.30 / Transaction\n        \"\"\"\n        return AcmeProcessor()\n\n    def premium(self, user: User) -\u003e PremiumProcessor:\n        \"\"\"\n        4% + $0.50 / Transaction\n        \"\"\"\n        return PremiumProcessor()\n```\n\nHere, we have defined the cheaper processor as our default, and can take in a user\nto segment off of. When asked for a payment processor, feats will always return\nAcmeProcessor, unless we have configured the app otherwise.\n\nIt is important to understand that PaymentProcessor has been replaced by a handle\ninto the feats app. Production code creates payment processors in a factory-style.\n\n```python\nprocessor = PaymentProcessor.create(user)\nprocessor.charge()\n```\n\nThe acme and premium methods are no longer available to call directly\n```python\nprocessor = PaymentProcessor.acme(user) # AttributeError\nprocessor = PaymentProcessor().acme(user) # AttributeError\n```\n\nBecause our payment processor sometimes has trouble only in certain regions, we\nwill define segmentation of that user object based on their country.\n\n```python\n@app.segment\nclass Country:\n    \"\"\"\n    The ISO-3166 2 char country code of the object\n    \"\"\"\n    def user(self, user: User) -\u003e str:\n        return self.address(user.address)\n\n    def address(self, address: Address) -\u003e str:\n        return address.country_code\n```\n\nNow, if our monitoring system alerts of payment difficulties in Canada, we can\nchange the payment processor for Canadian users to the premium one.\n\n[TODO: Screenshots of changing to premium in CA]\n\n\nAfter Acme has resolved their issues, we can rollback to the previous state to\nhave Acme process charges in Canada again.\n\n[TODO: Screenshots of rollback]\n\n## Rollouts\n\nContinuing our example from before, we have now negotiated an even better rate\nwith a third provider. Any number of things can go wrong when integrating with\na new third party. We'd like to start using them in production by slowly\nrolling them out to users.\n\nWe can extend our previous feature to add a third implementation like so\n```python\n@app.feature\nclass PaymentProcessor:\n    @app.default\n    def acme(self, user: User) -\u003e AcmeProcessor:\n        \"\"\"\n        2% + $0.30 / Transaction\n        \"\"\"\n        return AcmeProcessor()\n\n    def premium(self, user: User) -\u003e PremiumProcessor:\n        \"\"\"\n        4% + $0.50 / Transaction\n        \"\"\"\n        return PremiumProcessor()\n\n    def aperture(self, user: User) -\u003e ApertureProcessor:\n        \"\"\"\n        1% + $0.00 / Transaction\n        \"\"\"\n        return ApertureProcessor()\n```\n\nWhen rolling out, we will want to start in the US and give 5% of users the new\nAperture payment processor. In order to do this, we'll need to provide a second\nsegmentation of user ids. The users ids is what we will take 5% of, if we were\nto only use countries, we would be taking 5% of all countries.\n\n```python\n@app.segment\nclass UserId:\n    def user(self, user: User) -\u003e str:\n        return str(user.id)\n```\n\nWe can then configure the payment processor to give 5% of American users Aperture\nby doing the following\n\n[TODO: Screenshots of multi-segmented rollout]\n\n\nAs we are certain there are no integration issues on either side, we can give\nmore users the new processor\n\n[TODO: Screenshots of increasing rollout]\n\n## Product Experiments\n\nLet's say our application is a TODO list.\nWe think our users will respond positively to having priorities of items TODO,\nand would like to perform a randomized experiment to understand if that is the\ncase.\n\nAs before, we start off with the feature.\n\n```python\n@app.feature\nclass AllowedPriorities:\n    @app.default\n    def no_priority(self, user: User) -\u003e List[str]:\n        return []\n\n    def two_priorities(self, user: User) -\u003e List[str]:\n        return [\"Important\", \"Normal\"]\n\n    def three_priorities(self, user: User) -\u003e List[str]:\n        return [\"Important\", \"Normal\", \"Unimportant\"]\n```\n\n[TODO: Screenshots of experiment]\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froverdotcom%2Ffeats.py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froverdotcom%2Ffeats.py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froverdotcom%2Ffeats.py/lists"}