Package: novi
NoviNovi is a simple yet powerfulfeature flagplatform built in Python. Novi is "simple" because all of its core capabilities are built around 2 simple concepts.Flagsand ActivationsConceptsFlagsFlagsare associated with features. Flags turn a feature on or off. The on/off status of a flag is determined by Activations.
Details about flags are store in the Flag table, which has 3 columnsA unique ID which is also the primary key for the flagA unique nameA default status.
If the default status is false, the flag is always turned off, irrespective of how the activations get evaluated.idnamestatus1Date and Random Activated Feature12Random Variant Feature13Combo AND14Combo OR1ActivationsActivationsdetermine the on/off status of a flag when specific conditions are met.
Activations are what make novi a"dynamic"feature flag system, as the runtime conditions are evaluated against an Activations trigger logic
to set the flag status. Some scenarios that Activations are useful:-Features could be activated depending on the deployment environment such as production, development or testFeature could be shown only to certain usernames and disabled for others.Organizations may choose to show a feature to only traffic originating from company IP address ranges during testingFeatures may be shown to a percentage of users selected randomlyThe list of scenarios can be infinitely varied and complex. An activation table has four columns:-A unique ID,A descriptive name,A python class name that is used to instantiate an activation objectA configurationidnameclass_nameconfig1Date Activatednovi.client.activations.date_time_activation.DateTimeActivation{"startDateTime":"11/26/2023 12:00 AM","endDateTime":"11/28/2023 12:00 AM","format": "%m/%d/%Y %I:%M %p"}2Random Split Activatednovi.client.activations.weighted_random_activation.WeightedRandomActivation{ "splits":[100, 0, 0], "variations":["A", "B", "C"]}3Combo AND Activationnovi.client.activations.and_activation.AndActivation[1,2]4Combo OR Activationnovi.client.activations.or_activation.OrActivation[1,2]Activation classes are discovered and registered with Novi using two approaches, users can choose which ever is more convenient:-Using the novi_activations folder - Any python modules or packages found in the novi_activations folder on the PYTHON_PATH will be registered as an activationUsing the @register annotation - Activation classes can be annotated with @register to indicate to Novi to register the class as an activation.For a class to be considered as a valid Activation class it should inherit from the Abstract base classBaseActivation.Out of the box novi comes with a few activations:-A Weighted Random ActivationA Date/Time based ActivationAND a list of ActivationsOR a list of ActivationsAt the time of instantiation Novi passes the data from the configuration column to the class constructor.
Configurations enable the business logic that drives activations. The configuration field provides the parameters necessary to do the evaluation,
in the example of date range check, it could be the start and end date. Think of configurations as a free-form column, containing strings (typically json)
that can be parsed by the Activation class to build its internal configuration object. Please eeCreating Activationsfor details on how to create your own custom activations.Relationship between Flags and ActivationsA given flag can have multiple activations and similarly a given activation can be associated with multiple flags (many to many)
This relationship is captured in the flags_activations tableflag_idactivation_id1112223344ArchitectureComponentsnovi.coreThe core component implements the logic to discover and register activationsnovi.clientThe client component uses SQLAlchemy to retrieve the flags from any supported relational database and evaluates the status of a flag's associated activationsnovi.webThe web component implements a simple flask API server to implement 4 endpointsRetrieve the original flag status as defined in the database[GET] /flags- Retrieve all flags[GET] /flags/<flag_name>- Retrieve a specific flag by nameRetrieve the evaluated flag status[POST] /evaluatedFlags[POST] /evaluatedFlags/<flag_name>Implementing ActivationsNovi scans a folder by name "novi_activations" on the python path and registers all classes inheriting fromBaseActivationfound within this folder.classBaseActivation(object):def__init__(self,cfg:Any=None):self.config=cfgdefevaluate(self,context:dict=None)->bool:passNovi's power comes from being able to implement pretty much any complex logic within these activation classes.
A flag can be associated with multiple activations:-Row Based AssociationIf the association is expressed as a many-to-many relationship in the flags_activations tables the status of evaluating each activation is 'AND'ed with the others to calculate the final status.
In the example aboveDate and Random Activated Feature(id=1) is associated with 2 activations Date based and Random weight as capture in the
flags_activations table.A List of ActivationsFlags can also be associated with Activations by inheriting fromBaseCombinationActivationclassclassBaseCombinationActivation(BaseActivation):def__init__(self,config:str=None):logging.getLogger(__name__).debug(f"{self.__module__}={self.__class__}")activationIds:list[int]=json.loads(config)super().__init__(flag.get_activation_by_ids(activationIds))@abstractmethoddefevaluate(self,context:dict=None)->bool:passThis approach gives implementers more fine grained ability to control how the final status of the flag is calculated.
Rows 3 and 4 in the Activation table shown above are an example of ComboActivations, the configuration column is a list of activations that need to be combined
They activations can be combined using any custom logic implemented in thedef evaluate(..)methodInstallation and UsageInstallationpip install noviUsageThere are a few ways to check the status of a flag and turn on/of features.Using novi.clientThenovi.client.flagretrieves feature flags from a database. Novi uses sqlalchemy to query from a variety of
databases.ConfigurationBefore you query a database, you need to tell Novi how to connect to your database. This is done in novi.ini.
An example file ishere. To create the tables you can either run the DDL SQL from the
fileschema.sqlor use thenovi.web. Instructions to create and seed the table using thesample_data.sqlscript arehere.Once the tables are created and seeded with your feature flag data. You can either use the decorator or the is_enabled
method to check the status of a feature flag using it's name. The example python file demonstrates both waysimportlogging.configfromnovi.clientimportflagdateToTest="11/26/2023 12:00 AM"@flag.enabled("Date Activated Feature",{'currentDateTime':dateToTest})defdate_based_activated_func():print(f"Feature active as of{dateToTest}")# Only the variationA will be evaluated as the split is [100,0, 0] in the database#@flag.enabled("Random Variant Feature",{"seed":333,"variant":"A"})defvariationA():print('variation A')@flag.enabled("Random Variant Feature",{"seed":333,"variant":"B"})defvariationB():print('variation B')defvariationC():print('variation C')if__name__=='__main__':logging.config.fileConfig("logging.conf")date_based_activated_func()variationA()variationB()ifflag.is_enabled("Random Variant Feature",{"seed":333,"variant":"C"}):variationC()Given the sample tables as shown aboveThe output will beFeature active as of 11/26/2023 12:00 AM
variation AUsing API endpointsnovi.webexposes 4 endpoints.The\flagsand\flags\<flag_name>endpoints return the raw flag data as retrieved
from the database table. The status of the flag is not updated. These endpoints are useful if you are integrating with an
authoring tool, so as to allow users to view\edit the flag data as persisted in the database. These endpoints are restful and support
GET, POST, PUT and DELETE http methodsThe\evaluatedFlagsand\evaluatedFlags\<flag_name>endpoints do the actual evaluation of the status flag
by applying the context against each of the activations associated with the flag. The context is passed as part of the request
body in the POST request. Seenovi.webfor details