"""Feature model. This is how we model features. A feature is comprised of multiple feature variants. A variant can either be completely active or inactive, or be forced to be enabled using a force rule. Code sample:: Feature( 'ads', list( FeatureVariant('control'), FeatureVariant('catchy'), FeatureVariant('minimalist', True)), force_rules=list( FeatureVariantForceRule( 'catchy', user_id, ['user id 1', 'user id 2']))) """ from features import exceptions class Feature: """Feature. The Feature Class represents the simple composition of a feature which is its name, and its different variants. """ def __init__(self, name, variants, force_rules=None): """Create a feature. Args: name (str): the name of the feature. variants (list): the list of all the variants. force_rules (list): all the force_rules. """ self.name = name self.variants = variants self.force_rules = force_rules or list() self._validate() def _validate(self): """Validate the structure of the feature. Misconfiguration of features should throw an exception that indicates they need to be fixed immediately. Some issues could be: force rule on a non existing variant, duplicate variant names and more. Raise: Exception: whenever an error is found. """ if not self.name: raise Exception('Name of the feature is mandatory.') variant_names = [] for variant in self.variants: variant_name = variant.name if variant_name in variant_names: raise exceptions.DuplicateFeatureVariantException( 'Duplicate variant for {name}'.format(name=variant_name) ) variant_names.append(variant_name) if len(variant_names) == 0: raise exceptions.MissingFeatureVariant( 'No feature variants were defined for {name}'.format(name=self.name) ) for force_rule in self.force_rules: if force_rule.variant_name not in variant_names: raise exceptions.MissingFeatureVariant( 'Feature Variant for Force Rule {name} does not exist.'.format( name=variant_name ) ) def is_variant_active(self, variant_name, context): """Check if a variant is active. Args: variant_name (str): the name of the variant. context (dict): the context to pass to the pool and force rules. It can contain user information (and more.) Return: boolean: if the variant is active or disabled. """ active_variant = self.get_active_variant(context) return active_variant == variant_name def get_active_variant(self, context): """Get the active feature variant. The list of feature variants is iterated over, until one is identified as active. Then, it continues to iterate over the force rules for variants until one is identified as active. Force rules for variants always take precedence. Args: context (dict): the context to determine whether a variant is active. Return: str: the name of the variant that is currently enabled. """ active_variant = None for variant in self.variants: if variant.is_active(): active_variant = variant.name break for force_rule in self.force_rules: if force_rule.matches(context): active_variant = force_rule.variant_name break return active_variant def get_variants(self, context): """Get all the feature variants and their statuses. Args: context (dict): the context to pass to the pool and force rules. It can contain user information (and more.) Return: dict: dictonary of feature variants and their state """ active_variant = self.get_active_variant(context) return { variant.name: variant.name == active_variant for variant in self.variants } def to_dict(self): """Return a dictionary representation of the object. Return: dict: the dictionary representation of the object. """ return dict( name=self.name, variants=[variant.to_dict() for variant in self.variants] ) def get_all_features_and_variants(features, context): """Get all the features with their variants status. Args: features (dict): dictionary of Feature objects. context (dict): the context (same context is passed to the Feature). Return: dict: dictionary of variants and their state. """ status = {} for feature in features.values(): status.update({feature.name: feature.get_variants(context)}) return status