From 92f5c75b7c1d43cbbca19265e1ea3499914f3cf8 Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sat, 6 May 2023 22:36:41 +0100 Subject: [PATCH 01/17] docs: refactoring examples - getting started --- docs/utilities/feature_flags.md | 129 ++---------------- examples/feature_flags/sam/template.yaml | 60 ++++++++ .../getting_started_single_feature_flag.py | 17 +++ ...ted_single_feature_flag_configuration.json | 20 +++ ...g_started_single_feature_flag_payload.json | 5 + 5 files changed, 116 insertions(+), 115 deletions(-) create mode 100644 examples/feature_flags/sam/template.yaml create mode 100644 examples/feature_flags/src/getting_started_single_feature_flag.py create mode 100644 examples/feature_flags/src/getting_started_single_feature_flag_configuration.json create mode 100644 examples/feature_flags/src/getting_started_single_feature_flag_payload.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 89393ddd54f..d095eec8bcd 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -6,7 +6,7 @@ description: Utility The feature flags utility provides a simple rule engine to define when one or multiple features should be enabled depending on the input. ???+ info - We currently only support AppConfig using [freeform configuration profile](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html#appconfig-creating-configuration-and-profile-free-form-configurations). + We currently only support AppConfig using [freeform configuration profile](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html#appconfig-creating-configuration-and-profile-free-form-configurations){target="_blank"} . ## Terminology @@ -24,18 +24,19 @@ Feature flags are used to modify behaviour without changing the application's co If you want to learn more about feature flags, their variations and trade-offs, check these articles: -* [Feature Toggles (aka Feature Flags) - Pete Hodgson](https://martinfowler.com/articles/feature-toggles.html) -* [AWS Lambda Feature Toggles Made Simple - Ran Isenberg](https://isenberg-ran.medium.com/aws-lambda-feature-toggles-made-simple-580b0c444233) -* [Feature Flags Getting Started - CloudBees](https://www.cloudbees.com/blog/ultimate-feature-flag-guide) +* [Feature Toggles (aka Feature Flags) - Pete Hodgson](https://martinfowler.com/articles/feature-toggles.html){target="_blank"} +* [AWS Lambda Feature Toggles Made Simple - Ran Isenberg](https://isenberg-ran.medium.com/aws-lambda-feature-toggles-made-simple-580b0c444233){target="_blank"} +* [Feature Flags Getting Started - CloudBees](https://www.cloudbees.com/blog/ultimate-feature-flag-guide){target="_blank"} ???+ note - AWS AppConfig requires two API calls to fetch configuration for the first time. You can improve latency by consolidating your feature settings in a single [Configuration](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html). + AWS AppConfig requires two API calls to fetch configuration for the first time. You can improve latency by consolidating your feature settings in a single [Configuration](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html){target="_blank"} . ## Key features * Define simple feature flags to dynamically decide when to enable a feature * Fetch one or all feature flags enabled for a given application context * Support for static feature flags to simply turn on/off a feature without rules +* Support for time based feature flags ## Getting started @@ -45,73 +46,14 @@ Your Lambda function IAM Role must have `appconfig:GetLatestConfiguration` and ` ### Required resources -By default, this utility provides [AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html) as a configuration store. +By default, this utility provides [AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html){target="_blank"} as a configuration store. The following sample infrastructure will be used throughout this documentation: === "template.yaml" ```yaml hl_lines="5 11 18 25 31-50 54" - AWSTemplateFormatVersion: "2010-09-09" - Description: Lambda Powertools for Python Feature flags sample template - Resources: - FeatureStoreApp: - Type: AWS::AppConfig::Application - Properties: - Description: "AppConfig Application for feature toggles" - Name: product-catalogue - - FeatureStoreDevEnv: - Type: AWS::AppConfig::Environment - Properties: - ApplicationId: !Ref FeatureStoreApp - Description: "Development Environment for the App Config Store" - Name: dev - - FeatureStoreConfigProfile: - Type: AWS::AppConfig::ConfigurationProfile - Properties: - ApplicationId: !Ref FeatureStoreApp - Name: features - LocationUri: "hosted" - - HostedConfigVersion: - Type: AWS::AppConfig::HostedConfigurationVersion - Properties: - ApplicationId: !Ref FeatureStoreApp - ConfigurationProfileId: !Ref FeatureStoreConfigProfile - Description: 'A sample hosted configuration version' - Content: | - { - "premium_features": { - "default": false, - "rules": { - "customer tier equals premium": { - "when_match": true, - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - } - } - }, - "ten_percent_off_campaign": { - "default": false - } - } - ContentType: 'application/json' - - ConfigDeployment: - Type: AWS::AppConfig::Deployment - Properties: - ApplicationId: !Ref FeatureStoreApp - ConfigurationProfileId: !Ref FeatureStoreConfigProfile - ConfigurationVersion: !Ref HostedConfigVersion - DeploymentStrategyId: "AppConfig.AllAtOnce" - EnvironmentId: !Ref FeatureStoreDevEnv + --8<-- "examples/feature_flags/sam/template.yaml" ``` === "CDK" @@ -187,64 +129,21 @@ The `evaluate` method supports two optional parameters: * **context**: Value to be evaluated against each rule defined for the given feature * **default**: Sentinel value to use in case we experience any issues with our store, or feature doesn't exist -=== "app.py" +=== "getting_started_single_feature_flag.py" ```python hl_lines="3 9 13 17-19" - from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore - - app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="features" - ) - - feature_flags = FeatureFlags(store=app_config) - - def lambda_handler(event, context): - # Get customer's tier from incoming request - ctx = { "tier": event.get("tier", "standard") } - - # Evaluate whether customer's tier has access to premium features - # based on `has_premium_features` rules - has_premium_features: bool = feature_flags.evaluate(name="premium_features", - context=ctx, default=False) - if has_premium_features: - # enable premium features - ... + --8<-- "examples/feature_flags/src/getting_started_single_feature_flag.py" ``` -=== "event.json" +=== "getting_started_single_feature_flag_payload.json" ```json hl_lines="3" - { - "username": "lessa", - "tier": "premium", - "basked_id": "random_id" - } + --8<-- "examples/feature_flags/src/getting_started_single_feature_flag_payload.json" ``` -=== "features.json" +=== "getting_started_single_feature_flag_configuration.json" ```json hl_lines="2 6 9-11" - { - "premium_features": { - "default": false, - "rules": { - "customer tier equals premium": { - "when_match": true, - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - } - } - }, - "ten_percent_off_campaign": { - "default": false - } - } + --8<-- "examples/feature_flags/src/getting_started_single_feature_flag_configuration.json" ``` #### Static flags diff --git a/examples/feature_flags/sam/template.yaml b/examples/feature_flags/sam/template.yaml new file mode 100644 index 00000000000..944183975ec --- /dev/null +++ b/examples/feature_flags/sam/template.yaml @@ -0,0 +1,60 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: Lambda Powertools for Python Feature flags sample template +Resources: + FeatureStoreApp: + Type: AWS::AppConfig::Application + Properties: + Description: "AppConfig Application for feature toggles" + Name: product-catalogue + + FeatureStoreDevEnv: + Type: AWS::AppConfig::Environment + Properties: + ApplicationId: !Ref FeatureStoreApp + Description: "Development Environment for the App Config Store" + Name: dev + + FeatureStoreConfigProfile: + Type: AWS::AppConfig::ConfigurationProfile + Properties: + ApplicationId: !Ref FeatureStoreApp + Name: features + LocationUri: "hosted" + + HostedConfigVersion: + Type: AWS::AppConfig::HostedConfigurationVersion + Properties: + ApplicationId: !Ref FeatureStoreApp + ConfigurationProfileId: !Ref FeatureStoreConfigProfile + Description: 'A sample hosted configuration version' + Content: | + { + "premium_features": { + "default": false, + "rules": { + "customer tier equals premium": { + "when_match": true, + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + } + } + }, + "ten_percent_off_campaign": { + "default": false + } + } + ContentType: 'application/json' + + ConfigDeployment: + Type: AWS::AppConfig::Deployment + Properties: + ApplicationId: !Ref FeatureStoreApp + ConfigurationProfileId: !Ref FeatureStoreConfigProfile + ConfigurationVersion: !Ref HostedConfigVersion + DeploymentStrategyId: "AppConfig.AllAtOnce" + EnvironmentId: !Ref FeatureStoreDevEnv diff --git a/examples/feature_flags/src/getting_started_single_feature_flag.py b/examples/feature_flags/src/getting_started_single_feature_flag.py new file mode 100644 index 00000000000..19927d975f1 --- /dev/null +++ b/examples/feature_flags/src/getting_started_single_feature_flag.py @@ -0,0 +1,17 @@ +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags + +app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event, context): + # Get customer's tier from incoming request + ctx = {"tier": event.get("tier", "standard")} + + # Evaluate whether customer's tier has access to premium features + # based on `has_premium_features` rules + has_premium_features: bool = feature_flags.evaluate(name="premium_features", context=ctx, default=False) + if has_premium_features: + # enable premium features + ... diff --git a/examples/feature_flags/src/getting_started_single_feature_flag_configuration.json b/examples/feature_flags/src/getting_started_single_feature_flag_configuration.json new file mode 100644 index 00000000000..8f7a7615db3 --- /dev/null +++ b/examples/feature_flags/src/getting_started_single_feature_flag_configuration.json @@ -0,0 +1,20 @@ +{ + "premium_features": { + "default": false, + "rules": { + "customer tier equals premium": { + "when_match": true, + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + } + } + }, + "ten_percent_off_campaign": { + "default": false + } +} diff --git a/examples/feature_flags/src/getting_started_single_feature_flag_payload.json b/examples/feature_flags/src/getting_started_single_feature_flag_payload.json new file mode 100644 index 00000000000..d63f3bff11a --- /dev/null +++ b/examples/feature_flags/src/getting_started_single_feature_flag_payload.json @@ -0,0 +1,5 @@ +{ + "username": "lessa", + "tier": "premium", + "basked_id": "random_id" +} From 342819234d02c3efcca8b80cf4b560876ebddc72 Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sat, 6 May 2023 23:33:18 +0100 Subject: [PATCH 02/17] docs: refactoring examples - static flag --- docs/utilities/feature_flags.md | 35 ++++++------------- .../getting_started_single_feature_flag.py | 3 +- ...started_single_feature_flag_features.json} | 0 .../src/getting_started_static_flag.py | 18 ++++++++++ .../getting_started_static_flag_features.json | 5 +++ .../getting_started_static_flag_payload.json | 4 +++ 6 files changed, 39 insertions(+), 26 deletions(-) rename examples/feature_flags/src/{getting_started_single_feature_flag_configuration.json => getting_started_single_feature_flag_features.json} (100%) create mode 100644 examples/feature_flags/src/getting_started_static_flag.py create mode 100644 examples/feature_flags/src/getting_started_static_flag_features.json create mode 100644 examples/feature_flags/src/getting_started_static_flag_payload.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index d095eec8bcd..b1aa6772609 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -140,10 +140,10 @@ The `evaluate` method supports two optional parameters: ```json hl_lines="3" --8<-- "examples/feature_flags/src/getting_started_single_feature_flag_payload.json" ``` -=== "getting_started_single_feature_flag_configuration.json" +=== "getting_started_single_feature_flag_features.json" ```json hl_lines="2 6 9-11" - --8<-- "examples/feature_flags/src/getting_started_single_feature_flag_configuration.json" + --8<-- "examples/feature_flags/src/getting_started_single_feature_flag_features.json" ``` #### Static flags @@ -152,36 +152,21 @@ We have a static flag named `ten_percent_off_campaign`. Meaning, there are no co In this case, we could omit the `context` parameter and simply evaluate whether we should apply the 10% discount. -=== "app.py" +=== "getting_started_static_flag.py" ```python hl_lines="12-13" - from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore - - app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="features" - ) - - feature_flags = FeatureFlags(store=app_config) - - def lambda_handler(event, context): - apply_discount: bool = feature_flags.evaluate(name="ten_percent_off_campaign", - default=False) + --8<-- "examples/feature_flags/src/getting_started_static_flag.py" + ``` +=== "getting_started_static_flag_payload.json" - if apply_discount: - # apply 10% discount to product - ... + ```json hl_lines="2-3" + --8<-- "examples/feature_flags/src/getting_started_static_flag_payload.json" ``` -=== "features.json" +=== "getting_started_static_flag_features.json" ```json hl_lines="2-3" - { - "ten_percent_off_campaign": { - "default": false - } - } + --8<-- "examples/feature_flags/src/getting_started_static_flag_features.json" ``` ### Getting all enabled features diff --git a/examples/feature_flags/src/getting_started_single_feature_flag.py b/examples/feature_flags/src/getting_started_single_feature_flag.py index 19927d975f1..53fc95bdd81 100644 --- a/examples/feature_flags/src/getting_started_single_feature_flag.py +++ b/examples/feature_flags/src/getting_started_single_feature_flag.py @@ -1,11 +1,12 @@ from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") feature_flags = FeatureFlags(store=app_config) -def lambda_handler(event, context): +def lambda_handler(event: dict, context: LambdaContext): # Get customer's tier from incoming request ctx = {"tier": event.get("tier", "standard")} diff --git a/examples/feature_flags/src/getting_started_single_feature_flag_configuration.json b/examples/feature_flags/src/getting_started_single_feature_flag_features.json similarity index 100% rename from examples/feature_flags/src/getting_started_single_feature_flag_configuration.json rename to examples/feature_flags/src/getting_started_single_feature_flag_features.json diff --git a/examples/feature_flags/src/getting_started_static_flag.py b/examples/feature_flags/src/getting_started_static_flag.py new file mode 100644 index 00000000000..1c54ffc06c1 --- /dev/null +++ b/examples/feature_flags/src/getting_started_static_flag.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + apply_discount: bool = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + price: float = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/examples/feature_flags/src/getting_started_static_flag_features.json b/examples/feature_flags/src/getting_started_static_flag_features.json new file mode 100644 index 00000000000..fe692cdf0c3 --- /dev/null +++ b/examples/feature_flags/src/getting_started_static_flag_features.json @@ -0,0 +1,5 @@ +{ + "ten_percent_off_campaign": { + "default": true + } +} diff --git a/examples/feature_flags/src/getting_started_static_flag_payload.json b/examples/feature_flags/src/getting_started_static_flag_payload.json new file mode 100644 index 00000000000..b2a71282f8e --- /dev/null +++ b/examples/feature_flags/src/getting_started_static_flag_payload.json @@ -0,0 +1,4 @@ +{ + "product": "laptop", + "price": 1000 +} From dd809456c722446865580bda29ed1fb6006b3bf7 Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 00:08:21 +0100 Subject: [PATCH 03/17] docs: refactoring examples - getting all enabled --- docs/utilities/feature_flags.md | 90 ++----------------- .../src/getting_all_enabled_features.py | 42 +++++++++ ...getting_all_enabled_features_features.json | 41 +++++++++ .../getting_all_enabled_features_payload.json | 10 +++ .../getting_started_single_feature_flag.py | 4 +- .../src/getting_started_static_flag.py | 6 +- 6 files changed, 108 insertions(+), 85 deletions(-) create mode 100644 examples/feature_flags/src/getting_all_enabled_features.py create mode 100644 examples/feature_flags/src/getting_all_enabled_features_features.json create mode 100644 examples/feature_flags/src/getting_all_enabled_features_payload.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index b1aa6772609..0f8b3bb02df 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -175,96 +175,22 @@ As you might have noticed, each `evaluate` call means an API call to the Store a You can use `get_enabled_features` method for scenarios where you need a list of all enabled features according to the input context. -=== "app.py" - - ```python hl_lines="17-20 23" - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore - - app = APIGatewayRestResolver() - - app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="features" - ) - - feature_flags = FeatureFlags(store=app_config) - - @app.get("/products") - def list_products(): - ctx = { - **app.current_event.headers, - **app.current_event.json_body - } - - # all_features is evaluated to ["geo_customer_campaign", "ten_percent_off_campaign"] - all_features: list[str] = feature_flags.get_enabled_features(context=ctx) - - if "geo_customer_campaign" in all_features: - # apply discounts based on geo - ... +=== "getting_all_enabled_features.py" - if "ten_percent_off_campaign" in all_features: - # apply additional 10% for all customers - ... - - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="12-13" + --8<-- "examples/feature_flags/src/getting_all_enabled_features.py" ``` -=== "event.json" +=== "getting_all_enabled_features_payload.json" ```json hl_lines="2 8" - { - "body": "{\"username\": \"lessa\", \"tier\": \"premium\", \"basked_id\": \"random_id\"}", - "resource": "/products", - "path": "/products", - "httpMethod": "GET", - "isBase64Encoded": false, - "headers": { - "CloudFront-Viewer-Country": "NL" - } - } + --8<-- "examples/feature_flags/src/getting_all_enabled_features_payload.json" ``` -=== "features.json" + +=== "getting_all_enabled_features_features.json" ```json hl_lines="17-18 20 27-29" - { - "premium_features": { - "default": false, - "rules": { - "customer tier equals premium": { - "when_match": true, - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - } - } - }, - "ten_percent_off_campaign": { - "default": true - }, - "geo_customer_campaign": { - "default": false, - "rules": { - "customer in temporary discount geo": { - "when_match": true, - "conditions": [ - { - "action": "KEY_IN_VALUE", - "key": "CloudFront-Viewer-Country", - "value": ["NL", "IE", "UK", "PL", "PT"] - } - ] - } - } - } - } + --8<-- "examples/feature_flags/src/getting_all_enabled_features_features.json" ``` ### Beyond boolean feature flags diff --git a/examples/feature_flags/src/getting_all_enabled_features.py b/examples/feature_flags/src/getting_all_enabled_features.py new file mode 100644 index 00000000000..e19258452d1 --- /dev/null +++ b/examples/feature_flags/src/getting_all_enabled_features.py @@ -0,0 +1,42 @@ +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver() + +app_config = AppConfigStore(environment="dev", application="comments", name="config") + +feature_flags = FeatureFlags(store=app_config) + + +@app.get("/products") +def list_products(): + # getting fields from request + # https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/#accessing-request-details + json_body = app.current_event.json_body + headers = app.current_event.headers + + ctx = {**headers, **json_body} + + # getting price from payload + price: float = float(json_body.get("price")) + percent_discount: int = 0 + + # all_features is evaluated to ["premium_features", "geo_customer_campaign", "ten_percent_off_campaign"] + all_features: list[str] = feature_flags.get_enabled_features(context=ctx) + + if "geo_customer_campaign" in all_features: + # apply 20% discounts for customers in NL + percent_discount += 20 + + if "ten_percent_off_campaign" in all_features: + # apply additional 10% for all customers + percent_discount += 10 + + price = price * (100 - percent_discount) / 100 + + return {"price": price} + + +def lambda_handler(event: dict, context: LambdaContext): + return app.resolve(event, context) diff --git a/examples/feature_flags/src/getting_all_enabled_features_features.json b/examples/feature_flags/src/getting_all_enabled_features_features.json new file mode 100644 index 00000000000..1017b872dfb --- /dev/null +++ b/examples/feature_flags/src/getting_all_enabled_features_features.json @@ -0,0 +1,41 @@ +{ + "premium_features": { + "default": false, + "rules": { + "customer tier equals premium": { + "when_match": true, + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + } + } + }, + "ten_percent_off_campaign": { + "default": true + }, + "geo_customer_campaign": { + "default": false, + "rules": { + "customer in temporary discount geo": { + "when_match": true, + "conditions": [ + { + "action": "KEY_IN_VALUE", + "key": "CloudFront-Viewer-Country", + "value": [ + "NL", + "IE", + "UK", + "PL", + "PT" + ] + } + ] + } + } + } + } diff --git a/examples/feature_flags/src/getting_all_enabled_features_payload.json b/examples/feature_flags/src/getting_all_enabled_features_payload.json new file mode 100644 index 00000000000..cb0a41847e3 --- /dev/null +++ b/examples/feature_flags/src/getting_all_enabled_features_payload.json @@ -0,0 +1,10 @@ +{ + "body": "{\"username\": \"lessa\", \"tier\": \"premium\", \"basked_id\": \"random_id\", \"price\": 1000}", + "resource": "/products", + "path": "/products", + "httpMethod": "GET", + "isBase64Encoded": false, + "headers": { + "CloudFront-Viewer-Country": "NL" + } +} diff --git a/examples/feature_flags/src/getting_started_single_feature_flag.py b/examples/feature_flags/src/getting_started_single_feature_flag.py index 53fc95bdd81..c4ab6275659 100644 --- a/examples/feature_flags/src/getting_started_single_feature_flag.py +++ b/examples/feature_flags/src/getting_started_single_feature_flag.py @@ -1,3 +1,5 @@ +from typing import Any + from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags from aws_lambda_powertools.utilities.typing import LambdaContext @@ -12,7 +14,7 @@ def lambda_handler(event: dict, context: LambdaContext): # Evaluate whether customer's tier has access to premium features # based on `has_premium_features` rules - has_premium_features: bool = feature_flags.evaluate(name="premium_features", context=ctx, default=False) + has_premium_features: Any = feature_flags.evaluate(name="premium_features", context=ctx, default=False) if has_premium_features: # enable premium features ... diff --git a/examples/feature_flags/src/getting_started_static_flag.py b/examples/feature_flags/src/getting_started_static_flag.py index 1c54ffc06c1..dc21a2bddc7 100644 --- a/examples/feature_flags/src/getting_started_static_flag.py +++ b/examples/feature_flags/src/getting_started_static_flag.py @@ -1,3 +1,5 @@ +from typing import Any + from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags from aws_lambda_powertools.utilities.typing import LambdaContext @@ -7,9 +9,9 @@ def lambda_handler(event: dict, context: LambdaContext): - apply_discount: bool = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) - price: float = event.get("price") + price: Any = event.get("price") if apply_discount: # apply 10% discount to product From db868f91f5bdf967aa79d003384df601ba3afa3e Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 00:23:40 +0100 Subject: [PATCH 04/17] docs: refactoring examples - target links --- docs/utilities/feature_flags.md | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 0f8b3bb02df..e82ca319ef5 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -29,7 +29,7 @@ If you want to learn more about feature flags, their variations and trade-offs, * [Feature Flags Getting Started - CloudBees](https://www.cloudbees.com/blog/ultimate-feature-flag-guide){target="_blank"} ???+ note - AWS AppConfig requires two API calls to fetch configuration for the first time. You can improve latency by consolidating your feature settings in a single [Configuration](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html){target="_blank"} . + AWS AppConfig requires two API calls to fetch configuration for the first time. You can improve latency by consolidating your feature settings in a single [Configuration](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html){target="_blank"}. ## Key features @@ -483,11 +483,11 @@ The `action` configuration can have the following values, where the expressions For time based keys, we provide a list of predefined keys. These will automatically get converted to the corresponding timestamp on each invocation of your Lambda function. - | Key | Meaning | - | ------------------- | ------------------------------------------------------------------------ | - | CURRENT_TIME | The current time, 24 hour format (HH:mm) | - | CURRENT_DATETIME | The current datetime ([ISO8601](https://en.wikipedia.org/wiki/ISO_8601)) | - | CURRENT_DAY_OF_WEEK | The current day of the week (Monday-Sunday) | + | Key | Meaning | + | ------------------- | ----------------------------------------------------------------------------------------- | + | CURRENT_TIME | The current time, 24 hour format (HH:mm) | + | CURRENT_DATETIME | The current datetime ([ISO8601](https://en.wikipedia.org/wiki/ISO_8601){target="_blank"}) | + | CURRENT_DAY_OF_WEEK | The current day of the week (Monday-Sunday) | If not specified, the timezone used for calculations will be UTC. @@ -662,19 +662,10 @@ def test_flags_condition_match(mocker): assert flag == expected_value ``` -## Feature flags vs Parameters vs env vars +## Feature flags vs Parameters vs Env vars | Method | When to use | Requires new deployment on changes | Supported services | | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------- | | **[Environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html){target="_blank"}** | Simple configuration that will rarely if ever change, because changing it requires a Lambda function deployment. | Yes | Lambda | | **[Parameters utility](parameters.md)** | Access to secrets, or fetch parameters in different formats from AWS System Manager Parameter Store or Amazon DynamoDB. | No | Parameter Store, DynamoDB, Secrets Manager, AppConfig | | **Feature flags utility** | Rule engine to define when one or multiple features should be enabled depending on the input. | No | AppConfig | - -## Deprecation list when GA - -| Breaking change | Recommendation | -| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `IN` RuleAction | Use `KEY_IN_VALUE` instead | -| `NOT_IN` RuleAction | Use `KEY_NOT_IN_VALUE` instead | -| `get_enabled_features` | Return type changes from `List[str]` to `Dict[str, Any]`. New return will contain a list of features enabled and their values. List of enabled features will be in `enabled_features` key to keep ease of assertion we have in Beta. | -| `boolean_type` Schema | This **might** not be necessary anymore before we go GA. We will return either the `default` value when there are no rules as well as `when_match` value. This will simplify on-boarding if we can keep the same set of validations already offered. | From 4905989c3b00f5421055767c9657e995354b53a5 Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 01:09:07 +0100 Subject: [PATCH 05/17] docs: refactoring examples - adding comments --- docs/utilities/feature_flags.md | 6 ++-- .../feature_flags/src/datetime_feature.py | 29 +++++++++++++-- .../src/getting_all_enabled_features.py | 2 +- .../getting_started_single_feature_flag.py | 14 ++++++++ .../src/getting_started_static_flag.py | 4 +++ .../feature_flags/src/timebased_feature.py | 36 +++++++++++++++++-- .../src/timebased_happyhour_feature.py | 28 +++++++++++++-- 7 files changed, 106 insertions(+), 13 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index e82ca319ef5..791d4b18aba 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -257,7 +257,7 @@ Feature flags can return any JSON values when `boolean_type` parameter is set to } ``` -#### Time based feature flags +### Time based feature flags Feature flags can also return enabled features based on time or datetime ranges. This allows you to have features that are only enabled on certain days of the week, certain time @@ -318,8 +318,8 @@ You can also have features enabled only at specific days, for example: enable ch ``` ???+ info "How should I use timezones?" - You can use any [IANA time zone](https://www.iana.org/time-zones) (as originally specified - in [PEP 615](https://peps.python.org/pep-0615/)) as part of your rules definition. + You can use any [IANA time zone](https://www.iana.org/time-zones){target="_blank"} (as originally specified + in [PEP 615](https://peps.python.org/pep-0615/){target="_blank"}) as part of your rules definition. Powertools takes care of converting and calculate the correct timestamps for you. When using `SCHEDULE_BETWEEN_DATETIME_RANGE`, use timestamps without timezone information, and diff --git a/examples/feature_flags/src/datetime_feature.py b/examples/feature_flags/src/datetime_feature.py index 55c11ea6e7d..7dff14b8008 100644 --- a/examples/feature_flags/src/datetime_feature.py +++ b/examples/feature_flags/src/datetime_feature.py @@ -1,14 +1,37 @@ from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") feature_flags = FeatureFlags(store=app_config) -def lambda_handler(event, context): - # Get customer's tier from incoming request +def lambda_handler(event: dict, context: LambdaContext): + """ + This feature flag is enabled under the following conditions: + - Start date: December 25th, 2022 at 12:00:00 PM EST + - End date: December 31st, 2022 at 11:59:59 PM EST + - Timezone: America/New_York + + Rule condition to be evaluated: + "conditions": [ + { + "action": "SCHEDULE_BETWEEN_DATETIME_RANGE", + "key": "CURRENT_DATETIME", + "value": { + "START": "2022-12-25T12:00:00", + "END": "2022-12-31T23:59:59", + "TIMEZONE": "America/New_York" + } + } + ] + """ + + # Checking if the Christmas discount is enable xmas_discount = feature_flags.evaluate(name="christmas_discount", default=False) if xmas_discount: # Enable special discount on christmas: - pass + return {"message": "The Christmas discount is enabled."} + + return {"message": "The Christmas discount is not enabled."} diff --git a/examples/feature_flags/src/getting_all_enabled_features.py b/examples/feature_flags/src/getting_all_enabled_features.py index e19258452d1..6e3cab50b0d 100644 --- a/examples/feature_flags/src/getting_all_enabled_features.py +++ b/examples/feature_flags/src/getting_all_enabled_features.py @@ -4,7 +4,7 @@ app = APIGatewayRestResolver() -app_config = AppConfigStore(environment="dev", application="comments", name="config") +app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") feature_flags = FeatureFlags(store=app_config) diff --git a/examples/feature_flags/src/getting_started_single_feature_flag.py b/examples/feature_flags/src/getting_started_single_feature_flag.py index c4ab6275659..a3d54324766 100644 --- a/examples/feature_flags/src/getting_started_single_feature_flag.py +++ b/examples/feature_flags/src/getting_started_single_feature_flag.py @@ -9,6 +9,20 @@ def lambda_handler(event: dict, context: LambdaContext): + """ + This feature flag is enabled under the following conditions: + - The request payload contains a field 'tier' with the value 'premium'. + + Rule condition to be evaluated: + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + """ + # Get customer's tier from incoming request ctx = {"tier": event.get("tier", "standard")} diff --git a/examples/feature_flags/src/getting_started_static_flag.py b/examples/feature_flags/src/getting_started_static_flag.py index dc21a2bddc7..5d8c185cf2d 100644 --- a/examples/feature_flags/src/getting_started_static_flag.py +++ b/examples/feature_flags/src/getting_started_static_flag.py @@ -9,6 +9,10 @@ def lambda_handler(event: dict, context: LambdaContext): + """ + This feature flag is enabled by default for all requests. + """ + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) price: Any = event.get("price") diff --git a/examples/feature_flags/src/timebased_feature.py b/examples/feature_flags/src/timebased_feature.py index 0b0963489f4..46fbbc1c3d5 100644 --- a/examples/feature_flags/src/timebased_feature.py +++ b/examples/feature_flags/src/timebased_feature.py @@ -1,16 +1,46 @@ from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") feature_flags = FeatureFlags(store=app_config) -def lambda_handler(event, context): +def lambda_handler(event: dict, context: LambdaContext): + """ + This feature flag is enabled under the following conditions: + - The request payload contains a field 'tier' with the value 'premium'. + - If the current day is either Saturday or Sunday in America/New_York timezone. + + Rule condition to be evaluated: + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + }, + { + "action": "SCHEDULE_BETWEEN_DAYS_OF_WEEK", + "key": "CURRENT_DAY_OF_WEEK", + "value": { + "DAYS": [ + "SATURDAY", + "SUNDAY" + ], + "TIMEZONE": "America/New_York" + } + } + ] + """ + # Get customer's tier from incoming request ctx = {"tier": event.get("tier", "standard")} + # Checking if the weekend premum discount is enable weekend_premium_discount = feature_flags.evaluate(name="weekend_premium_discount", default=False, context=ctx) if weekend_premium_discount: - # Enable special discount for premium members on weekends - pass + # Enable special discount on weekend for premium users: + return {"message": "The weekend premium discount is enabled."} + + return {"message": "The weekend premium discount is not enabled."} diff --git a/examples/feature_flags/src/timebased_happyhour_feature.py b/examples/feature_flags/src/timebased_happyhour_feature.py index b008481c722..8b71062bdff 100644 --- a/examples/feature_flags/src/timebased_happyhour_feature.py +++ b/examples/feature_flags/src/timebased_happyhour_feature.py @@ -1,13 +1,35 @@ from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") feature_flags = FeatureFlags(store=app_config) -def lambda_handler(event, context): +def lambda_handler(event: dict, context: LambdaContext): + """ + This feature flag is enabled under the following conditions: + - Every day between 17:00 to 19:00 in Europe/Copenhagen timezone + + Rule condition to be evaluated: + "conditions": [ + { + "action": "SCHEDULE_BETWEEN_TIME_RANGE", + "key": "CURRENT_TIME", + "value": { + "START": "17:00", + "END": "19:00", + "TIMEZONE": "Europe/Copenhagen" + } + } + ] + """ + + # Checking if the happy hour discount is enable is_happy_hour = feature_flags.evaluate(name="happy_hour", default=False) if is_happy_hour: - # Apply special discount - pass + # Enable special discount on happy hour: + return {"message": "The happy hour discount is enabled."} + + return {"message": "The happy hour discount is not enabled."} From 5cc747d1bf38dc86be2fef9cf4850ca47a02a53c Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 01:23:47 +0100 Subject: [PATCH 06/17] docs: refactoring examples - ordering menu + testing your code --- docs/utilities/feature_flags.md | 199 +++++++----------- ...me_feature.json => datetime_features.json} | 0 .../src/getting_started_with_tests.py | 52 +++++ 3 files changed, 129 insertions(+), 122 deletions(-) rename examples/feature_flags/src/{datetime_feature.json => datetime_features.json} (100%) create mode 100644 examples/feature_flags/src/getting_started_with_tests.py diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 791d4b18aba..c2b97f9ee7b 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -193,6 +193,74 @@ You can use `get_enabled_features` method for scenarios where you need a list of --8<-- "examples/feature_flags/src/getting_all_enabled_features_features.json" ``` +### Time based feature flags + +Feature flags can also return enabled features based on time or datetime ranges. +This allows you to have features that are only enabled on certain days of the week, certain time +intervals or between certain calendar dates. + +Use cases: + +* Enable maintenance mode during a weekend +* Disable support/chat feature after working hours +* Launch a new feature on a specific date and time + +You can also have features enabled only at certain times of the day for premium tier customers + +=== "timebased_feature.py" + + ```python hl_lines="12" + --8<-- "examples/feature_flags/src/timebased_feature.py" + ``` + +=== "timebased_feature_event.json" + + ```json hl_lines="3" + --8<-- "examples/feature_flags/src/timebased_feature_event.json" + ``` + +=== "timebased_features.json" + + ```json hl_lines="9-11 14-21" + --8<-- "examples/feature_flags/src/timebased_features.json" + ``` + +You can also have features enabled only at certain times of the day. + +=== "timebased_happyhour_feature.py" + + ```python hl_lines="9" + --8<-- "examples/feature_flags/src/timebased_happyhour_feature.py" + ``` + +=== "timebased_happyhour_features.json" + + ```json hl_lines="9-15" + --8<-- "examples/feature_flags/src/timebased_happyhour_features.json" + ``` + +You can also have features enabled only at specific days, for example: enable christmas sale discount during specific dates. + +=== "datetime_feature.py" + + ```python hl_lines="10" + --8<-- "examples/feature_flags/src/datetime_feature.py" + ``` + +=== "datetime_features.json" + + ```json hl_lines="9-14" + --8<-- "examples/feature_flags/src/datetime_features.json" + ``` + +???+ info "How should I use timezones?" + You can use any [IANA time zone](https://www.iana.org/time-zones){target="_blank"} (as originally specified + in [PEP 615](https://peps.python.org/pep-0615/){target="_blank"}) as part of your rules definition. + Powertools takes care of converting and calculate the correct timestamps for you. + + When using `SCHEDULE_BETWEEN_DATETIME_RANGE`, use timestamps without timezone information, and + specify the timezone manually. This way, you'll avoid hitting problems with day light savings. + ### Beyond boolean feature flags ???+ info "When is this useful?" @@ -257,74 +325,6 @@ Feature flags can return any JSON values when `boolean_type` parameter is set to } ``` -### Time based feature flags - -Feature flags can also return enabled features based on time or datetime ranges. -This allows you to have features that are only enabled on certain days of the week, certain time -intervals or between certain calendar dates. - -Use cases: - -* Enable maintenance mode during a weekend -* Disable support/chat feature after working hours -* Launch a new feature on a specific date and time - -You can also have features enabled only at certain times of the day for premium tier customers - -=== "app.py" - - ```python hl_lines="12" - --8<-- "examples/feature_flags/src/timebased_feature.py" - ``` - -=== "event.json" - - ```json hl_lines="3" - --8<-- "examples/feature_flags/src/timebased_feature_event.json" - ``` - -=== "features.json" - - ```json hl_lines="9-11 14-21" - --8<-- "examples/feature_flags/src/timebased_features.json" - ``` - -You can also have features enabled only at certain times of the day. - -=== "app.py" - - ```python hl_lines="9" - --8<-- "examples/feature_flags/src/timebased_happyhour_feature.py" - ``` - -=== "features.json" - - ```json hl_lines="9-15" - --8<-- "examples/feature_flags/src/timebased_happyhour_features.json" - ``` - -You can also have features enabled only at specific days, for example: enable christmas sale discount during specific dates. - -=== "app.py" - - ```python hl_lines="10" - --8<-- "examples/feature_flags/src/datetime_feature.py" - ``` - -=== "features.json" - - ```json hl_lines="9-14" - --8<-- "examples/feature_flags/src/datetime_feature.json" - ``` - -???+ info "How should I use timezones?" - You can use any [IANA time zone](https://www.iana.org/time-zones){target="_blank"} (as originally specified - in [PEP 615](https://peps.python.org/pep-0615/){target="_blank"}) as part of your rules definition. - Powertools takes care of converting and calculate the correct timestamps for you. - - When using `SCHEDULE_BETWEEN_DATETIME_RANGE`, use timestamps without timezone information, and - specify the timezone manually. This way, you'll avoid hitting problems with day light savings. - ## Advanced ### Adjusting in-memory cache @@ -477,7 +477,7 @@ The `action` configuration can have the following values, where the expressions | **SCHEDULE_BETWEEN_DAYS_OF_WEEK** | `lambda a, b: day_of_week(a) in b` | ???+ info - The `**key**` and `**value**` will be compared to the input from the `**context**` parameter. + The `key` and `value` will be compared to the input from the `context` parameter. ???+ "Time based keys" @@ -562,9 +562,9 @@ These are the available options for further customization. | Parameter | Default | Description | | -------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **environment** | `""` | AWS AppConfig Environment, e.g. `test` | -| **application** | `""` | AWS AppConfig Application | -| **name** | `""` | AWS AppConfig Configuration name | +| **environment** | `""` | AWS AppConfig Environment, e.g. `dev` | +| **application** | `""` | AWS AppConfig Application, e.g. `product-catalogue` | +| **name** | `""` | AWS AppConfig Configuration name, e.g `features` | | **envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration | | **max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig | | **sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} | @@ -611,56 +611,11 @@ You can unit test your feature flags locally and independently without setting u ???+ warning This excerpt relies on `pytest` and `pytest-mock` dependencies. -```python hl_lines="7-9" title="Unit testing feature flags" -from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore, RuleAction - - -def init_feature_flags(mocker, mock_schema, envelope="") -> FeatureFlags: - """Mock AppConfig Store get_configuration method to use mock schema instead""" - - method_to_mock = "aws_lambda_powertools.utilities.feature_flags.AppConfigStore.get_configuration" - mocked_get_conf = mocker.patch(method_to_mock) - mocked_get_conf.return_value = mock_schema - - app_conf_store = AppConfigStore( - environment="test_env", - application="test_app", - name="test_conf_name", - envelope=envelope, - ) - - return FeatureFlags(store=app_conf_store) - - -def test_flags_condition_match(mocker): - # GIVEN - expected_value = True - mocked_app_config_schema = { - "my_feature": { - "default": False, - "rules": { - "tenant id equals 12345": { - "when_match": expected_value, - "conditions": [ - { - "action": RuleAction.EQUALS.value, - "key": "tenant_id", - "value": "12345", - } - ], - } - }, - } - } - - # WHEN - ctx = {"tenant_id": "12345", "username": "a"} - feature_flags = init_feature_flags(mocker=mocker, mock_schema=mocked_app_config_schema) - flag = feature_flags.evaluate(name="my_feature", context=ctx, default=False) - - # THEN - assert flag == expected_value -``` +=== "Testing your code" + + ```python hl_lines="7-9" + --8<-- "examples/feature_flags/src/getting_started_with_tests.py" + ``` ## Feature flags vs Parameters vs Env vars diff --git a/examples/feature_flags/src/datetime_feature.json b/examples/feature_flags/src/datetime_features.json similarity index 100% rename from examples/feature_flags/src/datetime_feature.json rename to examples/feature_flags/src/datetime_features.json diff --git a/examples/feature_flags/src/getting_started_with_tests.py b/examples/feature_flags/src/getting_started_with_tests.py new file mode 100644 index 00000000000..81152dca104 --- /dev/null +++ b/examples/feature_flags/src/getting_started_with_tests.py @@ -0,0 +1,52 @@ +from aws_lambda_powertools.utilities.feature_flags import ( + AppConfigStore, + FeatureFlags, + RuleAction, +) + + +def init_feature_flags(mocker, mock_schema, envelope="") -> FeatureFlags: + """Mock AppConfig Store get_configuration method to use mock schema instead""" + + method_to_mock = "aws_lambda_powertools.utilities.feature_flags.AppConfigStore.get_configuration" + mocked_get_conf = mocker.patch(method_to_mock) + mocked_get_conf.return_value = mock_schema + + app_conf_store = AppConfigStore( + environment="test_env", + application="test_app", + name="test_conf_name", + envelope=envelope, + ) + + return FeatureFlags(store=app_conf_store) + + +def test_flags_condition_match(mocker): + # GIVEN + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant id equals 12345": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.EQUALS.value, + "key": "tenant_id", + "value": "12345", + } + ], + } + }, + } + } + + # WHEN + ctx = {"tenant_id": "12345", "username": "a"} + feature_flags = init_feature_flags(mocker=mocker, mock_schema=mocked_app_config_schema) + flag = feature_flags.evaluate(name="my_feature", context=ctx, default=False) + + # THEN + assert flag == expected_value From a2032254a2a0d77f8b8a0322084ce47ad3fbbd5e Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 20:26:57 +0100 Subject: [PATCH 07/17] docs: refactoring examples - using jmespath --- docs/utilities/feature_flags.md | 35 ++++------------ .../src/appconfig_provider_options.py | 42 +++++++++++++++++++ .../appconfig_provider_options_features.json | 7 ++++ .../appconfig_provider_options_payload.json | 4 ++ 4 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 examples/feature_flags/src/appconfig_provider_options.py create mode 100644 examples/feature_flags/src/appconfig_provider_options_features.json create mode 100644 examples/feature_flags/src/appconfig_provider_options_payload.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index c2b97f9ee7b..48024acfcd7 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -571,36 +571,17 @@ These are the available options for further customization. | **jmespath_options** | `None` | For advanced use cases when you want to bring your own [JMESPath functions](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank"} | | **logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools Logger. | -```python hl_lines="21-27" title="AppConfigStore sample" -from botocore.config import Config +=== "appconfig_provider_options.py" -import jmespath - -from aws_lambda_powertools.utilities.feature_flags import AppConfigStore - -boto_config = Config(read_timeout=10, retries={"total_max_attempts": 2}) - -# Custom JMESPath functions -class CustomFunctions(jmespath.functions.Functions): - - @jmespath.functions.signature({'types': ['string']}) - def _func_special_decoder(self, s): - return my_custom_decoder_logic(s) - - -custom_jmespath_options = {"custom_functions": CustomFunctions()} + ```python hl_lines="9" + --8<-- "examples/feature_flags/src/appconfig_provider_options.py" + ``` +=== "appconfig_provider_options_features.json" -app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="configuration", - max_age=120, - envelope = "features", - sdk_config=boto_config, - jmespath_options=custom_jmespath_options -) -``` + ```json hl_lines="9-15" + --8<-- "examples/feature_flags/src/appconfig_provider_options_features.json" + ``` ## Testing your code diff --git a/examples/feature_flags/src/appconfig_provider_options.py b/examples/feature_flags/src/appconfig_provider_options.py new file mode 100644 index 00000000000..9236538b03d --- /dev/null +++ b/examples/feature_flags/src/appconfig_provider_options.py @@ -0,0 +1,42 @@ +from typing import Any + +from botocore.config import Config +from jmespath.functions import Functions, signature + +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +boto_config = Config(read_timeout=10, retries={"total_max_attempts": 2}) + + +# Custom JMESPath functions +class CustomFunctions(Functions): + @signature({"types": ["object"]}) + def _func_special_decoder(self, features): + # You can add some logic here + return features + + +custom_jmespath_options = {"custom_functions": CustomFunctions()} + + +app_config = AppConfigStore( + environment="dev", + application="comments", + name="config", + max_age=120, + envelope="special_decoder(features)", # using a custom function defined in CustomFunctions Class + sdk_config=boto_config, + jmespath_options=custom_jmespath_options, +) + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + print(app_config.get_raw_configuration) + + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + print(apply_discount) + return "ok" diff --git a/examples/feature_flags/src/appconfig_provider_options_features.json b/examples/feature_flags/src/appconfig_provider_options_features.json new file mode 100644 index 00000000000..11ffea6ce6a --- /dev/null +++ b/examples/feature_flags/src/appconfig_provider_options_features.json @@ -0,0 +1,7 @@ +{ + "features": { + "ten_percent_off_campaign": { + "default": true + } + } + } diff --git a/examples/feature_flags/src/appconfig_provider_options_payload.json b/examples/feature_flags/src/appconfig_provider_options_payload.json new file mode 100644 index 00000000000..b2a71282f8e --- /dev/null +++ b/examples/feature_flags/src/appconfig_provider_options_payload.json @@ -0,0 +1,4 @@ +{ + "product": "laptop", + "price": 1000 +} From fd66f768d8fbb1fbe74c9ecbc2d70c8cb95b014c Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 20:31:44 +0100 Subject: [PATCH 08/17] docs: refactoring examples - using jmespath --- .../feature_flags/src/appconfig_provider_options.py | 11 +++++++---- .../src/appconfig_provider_options_features.json | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/feature_flags/src/appconfig_provider_options.py b/examples/feature_flags/src/appconfig_provider_options.py index 9236538b03d..58c9164f71a 100644 --- a/examples/feature_flags/src/appconfig_provider_options.py +++ b/examples/feature_flags/src/appconfig_provider_options.py @@ -34,9 +34,12 @@ def _func_special_decoder(self, features): def lambda_handler(event: dict, context: LambdaContext): - print(app_config.get_raw_configuration) - apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) - print(apply_discount) - return "ok" + price: Any = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/examples/feature_flags/src/appconfig_provider_options_features.json b/examples/feature_flags/src/appconfig_provider_options_features.json index 11ffea6ce6a..a26b0d34e53 100644 --- a/examples/feature_flags/src/appconfig_provider_options_features.json +++ b/examples/feature_flags/src/appconfig_provider_options_features.json @@ -1,4 +1,8 @@ { + "logging": { + "level": "INFO", + "sampling_rate": 0.1 + }, "features": { "ten_percent_off_campaign": { "default": true From 9bec46132935701dd048777ef67fd7f17d7017fa Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 20:39:01 +0100 Subject: [PATCH 09/17] docs: refactoring examples - max age --- docs/utilities/feature_flags.md | 22 ++++++++++------- .../src/appconfig_provider_options.py | 4 ++-- .../src/getting_started_with_cache.py | 24 +++++++++++++++++++ .../getting_started_with_cache_features.json | 5 ++++ .../getting_started_with_cache_payload.json | 4 ++++ 5 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 examples/feature_flags/src/getting_started_with_cache.py create mode 100644 examples/feature_flags/src/getting_started_with_cache_features.json create mode 100644 examples/feature_flags/src/getting_started_with_cache_payload.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 48024acfcd7..f8f7a4ade90 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -333,17 +333,21 @@ By default, we cache configuration retrieved from the Store for 5 seconds for pe You can override `max_age` parameter when instantiating the store. -=== "app.py" +=== "getting_started_with_cache.py" - ```python hl_lines="7" - from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore + ```python hl_lines="12-13" + --8<-- "examples/feature_flags/src/getting_started_with_cache.py" + ``` +=== "getting_started_with_cache_payload.json" - app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="features", - max_age=300 - ) + ```json hl_lines="2-3" + --8<-- "examples/feature_flags/src/getting_started_with_cache_payload.json" + ``` + +=== "getting_started_with_cache_features.json" + + ```json hl_lines="2-3" + --8<-- "examples/feature_flags/src/getting_started_with_cache_features.json" ``` ### Getting fetched configuration diff --git a/examples/feature_flags/src/appconfig_provider_options.py b/examples/feature_flags/src/appconfig_provider_options.py index 58c9164f71a..8a41f651fc9 100644 --- a/examples/feature_flags/src/appconfig_provider_options.py +++ b/examples/feature_flags/src/appconfig_provider_options.py @@ -22,8 +22,8 @@ def _func_special_decoder(self, features): app_config = AppConfigStore( environment="dev", - application="comments", - name="config", + application="product-catalogue", + name="features", max_age=120, envelope="special_decoder(features)", # using a custom function defined in CustomFunctions Class sdk_config=boto_config, diff --git a/examples/feature_flags/src/getting_started_with_cache.py b/examples/feature_flags/src/getting_started_with_cache.py new file mode 100644 index 00000000000..1437c7266be --- /dev/null +++ b/examples/feature_flags/src/getting_started_with_cache.py @@ -0,0 +1,24 @@ +from typing import Any + +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features", max_age=300) + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + """ + This feature flag is enabled by default for all requests. + """ + + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + price: Any = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/examples/feature_flags/src/getting_started_with_cache_features.json b/examples/feature_flags/src/getting_started_with_cache_features.json new file mode 100644 index 00000000000..fe692cdf0c3 --- /dev/null +++ b/examples/feature_flags/src/getting_started_with_cache_features.json @@ -0,0 +1,5 @@ +{ + "ten_percent_off_campaign": { + "default": true + } +} diff --git a/examples/feature_flags/src/getting_started_with_cache_payload.json b/examples/feature_flags/src/getting_started_with_cache_payload.json new file mode 100644 index 00000000000..b2a71282f8e --- /dev/null +++ b/examples/feature_flags/src/getting_started_with_cache_payload.json @@ -0,0 +1,4 @@ +{ + "product": "laptop", + "price": 1000 +} From a4be2e574e6765c7a7512124a03416cc95e5208f Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 20:52:56 +0100 Subject: [PATCH 10/17] docs: refactoring examples - beyond boolean --- docs/utilities/feature_flags.md | 129 ++++-------------- examples/feature_flags/src/beyond_boolean.py | 18 +++ .../src/beyond_boolean_features.json | 22 +++ .../src/beyond_boolean_payload.json | 5 + examples/feature_flags/src/conditions.json | 9 ++ .../feature_flags/src/feature_with_rules.json | 32 +++++ .../feature_flags/src/minimal_schema.json | 9 ++ 7 files changed, 119 insertions(+), 105 deletions(-) create mode 100644 examples/feature_flags/src/beyond_boolean.py create mode 100644 examples/feature_flags/src/beyond_boolean_features.json create mode 100644 examples/feature_flags/src/beyond_boolean_payload.json create mode 100644 examples/feature_flags/src/conditions.json create mode 100644 examples/feature_flags/src/feature_with_rules.json create mode 100644 examples/feature_flags/src/minimal_schema.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index f8f7a4ade90..bbc3de7523d 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -268,61 +268,22 @@ You can also have features enabled only at specific days, for example: enable ch Feature flags can return any JSON values when `boolean_type` parameter is set to `false`. These can be dictionaries, list, string, integers, etc. -=== "app.py" - - ```python hl_lines="3 9 13 16 18" - from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore - - app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="features" - ) - - feature_flags = FeatureFlags(store=app_config) +=== "beyond_boolean.py" - def lambda_handler(event, context): - # Get customer's tier from incoming request - ctx = { "tier": event.get("tier", "standard") } - - # Evaluate `has_premium_features` base don customer's tier - premium_features: list[str] = feature_flags.evaluate(name="premium_features", - context=ctx, default=False) - for feature in premium_features: - # enable premium features - ... + ```python hl_lines="12" + --8<-- "examples/feature_flags/src/beyond_boolean.py" ``` -=== "event.json" +=== "beyond_boolean_payload.json" ```json hl_lines="3" - { - "username": "lessa", - "tier": "premium", - "basked_id": "random_id" - } + --8<-- "examples/feature_flags/src/beyond_boolean_payload.json" ``` -=== "features.json" - ```json hl_lines="3-4 7" - { - "premium_features": { - "boolean_type": false, - "default": [], - "rules": { - "customer tier equals premium": { - "when_match": ["no_ads", "no_limits", "chat"], - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - } - } - } - } +=== "beyond_boolean_features.json" + + ```json hl_lines="9-11 14-21" + --8<-- "examples/feature_flags/src/beyond_boolean_features.json" ``` ## Advanced @@ -384,17 +345,11 @@ This utility expects a certain schema to be stored as JSON within AWS AppConfig. A feature can simply have its name and a `default` value. This is either on or off, also known as a [static flag](#static-flags). -```json hl_lines="2-3 5-7" title="minimal_schema.json" -{ - "global_feature": { - "default": true - }, - "non_boolean_global_feature": { - "default": {"group": "read-only"}, - "boolean_type": false - }, -} -``` +=== "minimal_schema.json" + + ```json hl_lines="2-3 5-7" + --8<-- "examples/feature_flags/src/minimal_schema.json" + ``` If you need more control and want to provide context such as user group, permissions, location, etc., you need to add rules to your feature flag configuration. @@ -406,40 +361,11 @@ When adding `rules` to a feature, they must contain: 2. `when_match` boolean or JSON value that should be used when conditions match 3. A list of `conditions` for evaluation - ```json hl_lines="4-11 19-26" title="feature_with_rules.json" - { - "premium_feature": { - "default": false, - "rules": { - "customer tier equals premium": { - "when_match": true, - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - } - } - }, - "non_boolean_premium_feature": { - "default": [], - "rules": { - "customer tier equals premium": { - "when_match": ["remove_limits", "remove_ads"], - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - } - } - } - } - ``` +=== "feature_with_rules.json" + + ```json hl_lines="4-11 19-26" + --8<-- "examples/feature_flags/src/feature_with_rules.json" + ``` You can have multiple rules with different names. The rule engine will return the first result `when_match` of the matching rule configuration, or `default` value when none of the rules apply. @@ -447,18 +373,11 @@ You can have multiple rules with different names. The rule engine will return th The `conditions` block is a list of conditions that contain `action`, `key`, and `value` keys: -```json hl_lines="5-7" title="conditions.json" -{ - ... - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] -} -``` +=== "conditions.json" + + ```json hl_lines="5-7" + --8<-- "examples/feature_flags/src/conditions.json" + ``` The `action` configuration can have the following values, where the expressions **`a`** is the `key` and **`b`** is the `value` above: diff --git a/examples/feature_flags/src/beyond_boolean.py b/examples/feature_flags/src/beyond_boolean.py new file mode 100644 index 00000000000..a7436079019 --- /dev/null +++ b/examples/feature_flags/src/beyond_boolean.py @@ -0,0 +1,18 @@ +from typing import Any + +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +app_config = AppConfigStore(environment="dev", application="comments", name="config") + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + # Get customer's tier from incoming request + ctx = {"tier": event.get("tier", "standard")} + + # Evaluate `has_premium_features` base don customer's tier + premium_features: Any = feature_flags.evaluate(name="premium_features", context=ctx, default=[]) + + return {"Premium features enabled": premium_features} diff --git a/examples/feature_flags/src/beyond_boolean_features.json b/examples/feature_flags/src/beyond_boolean_features.json new file mode 100644 index 00000000000..c48754a15f9 --- /dev/null +++ b/examples/feature_flags/src/beyond_boolean_features.json @@ -0,0 +1,22 @@ +{ + "premium_features": { + "boolean_type": false, + "default": [], + "rules": { + "customer tier equals premium": { + "when_match": [ + "no_ads", + "no_limits", + "chat" + ], + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + } + } + } + } diff --git a/examples/feature_flags/src/beyond_boolean_payload.json b/examples/feature_flags/src/beyond_boolean_payload.json new file mode 100644 index 00000000000..d63f3bff11a --- /dev/null +++ b/examples/feature_flags/src/beyond_boolean_payload.json @@ -0,0 +1,5 @@ +{ + "username": "lessa", + "tier": "premium", + "basked_id": "random_id" +} diff --git a/examples/feature_flags/src/conditions.json b/examples/feature_flags/src/conditions.json new file mode 100644 index 00000000000..30eda640e0f --- /dev/null +++ b/examples/feature_flags/src/conditions.json @@ -0,0 +1,9 @@ +{ + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] +} diff --git a/examples/feature_flags/src/feature_with_rules.json b/examples/feature_flags/src/feature_with_rules.json new file mode 100644 index 00000000000..60765ebd59b --- /dev/null +++ b/examples/feature_flags/src/feature_with_rules.json @@ -0,0 +1,32 @@ +{ + "premium_feature": { + "default": false, + "rules": { + "customer tier equals premium": { + "when_match": true, + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + } + } + }, + "non_boolean_premium_feature": { + "default": [], + "rules": { + "customer tier equals premium": { + "when_match": ["remove_limits", "remove_ads"], + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + } + } + } +} diff --git a/examples/feature_flags/src/minimal_schema.json b/examples/feature_flags/src/minimal_schema.json new file mode 100644 index 00000000000..7302ab2784a --- /dev/null +++ b/examples/feature_flags/src/minimal_schema.json @@ -0,0 +1,9 @@ +{ + "global_feature": { + "default": true + }, + "non_boolean_global_feature": { + "default": {"group": "read-only"}, + "boolean_type": false + } +} From 5317232db3020dbfbcc1dc8723e38227608507ed Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 20:59:06 +0100 Subject: [PATCH 11/17] docs: refactoring examples - envelope --- docs/utilities/feature_flags.md | 56 ++++++------------- .../feature_flags/src/extracting_envelope.py | 22 ++++++++ .../src/extracting_envelope_features.json | 11 ++++ .../src/extracting_envelope_payload.json | 4 ++ 4 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 examples/feature_flags/src/extracting_envelope.py create mode 100644 examples/feature_flags/src/extracting_envelope_features.json create mode 100644 examples/feature_flags/src/extracting_envelope_payload.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index bbc3de7523d..44464af1b87 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -428,48 +428,22 @@ There are scenarios where you might want to include feature flags as part of an For this to work, you need to use a JMESPath expression via the `envelope` parameter to extract that key as the feature flags configuration. -=== "app.py" +=== "extracting_envelope.py" - ```python hl_lines="7" - from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore + ```python hl_lines="9" + --8<-- "examples/feature_flags/src/extracting_envelope.py" + ``` - app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="configuration", - envelope = "feature_flags" - ) +=== "extracting_envelope_payload.json" + + ```json hl_lines="9-15" + --8<-- "examples/feature_flags/src/extracting_envelope_payload.json" ``` -=== "configuration.json" - - ```json hl_lines="6" - { - "logging": { - "level": "INFO", - "sampling_rate": 0.1 - }, - "feature_flags": { - "premium_feature": { - "default": false, - "rules": { - "customer tier equals premium": { - "when_match": true, - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - } - } - }, - "feature2": { - "default": false - } - } - } +=== "extracting_envelope_features.json" + + ```json hl_lines="9-15" + --8<-- "examples/feature_flags/src/extracting_envelope_features.json" ``` ### Built-in store provider @@ -500,6 +474,12 @@ These are the available options for further customization. --8<-- "examples/feature_flags/src/appconfig_provider_options.py" ``` +=== "appconfig_provider_options_payload.json" + + ```json hl_lines="9-15" + --8<-- "examples/feature_flags/src/appconfig_provider_options_payload.json" + ``` + === "appconfig_provider_options_features.json" ```json hl_lines="9-15" diff --git a/examples/feature_flags/src/extracting_envelope.py b/examples/feature_flags/src/extracting_envelope.py new file mode 100644 index 00000000000..3c3194c0c1a --- /dev/null +++ b/examples/feature_flags/src/extracting_envelope.py @@ -0,0 +1,22 @@ +from typing import Any + +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +app_config = AppConfigStore( + environment="dev", application="product-catalogue", name="features", envelope="feature_flags" +) + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + price: Any = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/examples/feature_flags/src/extracting_envelope_features.json b/examples/feature_flags/src/extracting_envelope_features.json new file mode 100644 index 00000000000..a26b0d34e53 --- /dev/null +++ b/examples/feature_flags/src/extracting_envelope_features.json @@ -0,0 +1,11 @@ +{ + "logging": { + "level": "INFO", + "sampling_rate": 0.1 + }, + "features": { + "ten_percent_off_campaign": { + "default": true + } + } + } diff --git a/examples/feature_flags/src/extracting_envelope_payload.json b/examples/feature_flags/src/extracting_envelope_payload.json new file mode 100644 index 00000000000..b2a71282f8e --- /dev/null +++ b/examples/feature_flags/src/extracting_envelope_payload.json @@ -0,0 +1,4 @@ +{ + "product": "laptop", + "price": 1000 +} From 44164f874d2e3ab89766867f3a441fdf243bf8de Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 21:02:38 +0100 Subject: [PATCH 12/17] docs: refactoring examples - getting stored features --- docs/utilities/feature_flags.md | 17 +++-------------- .../src/getting_stored_features.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 examples/feature_flags/src/getting_stored_features.py diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 44464af1b87..23376ac88ed 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -320,21 +320,10 @@ You can override `max_age` parameter when instantiating the store. You can access the configuration fetched from the store via `get_raw_configuration` property within the store instance. -=== "app.py" +=== "getting_stored_features.py" - ```python hl_lines="12" - from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore - - app_config = AppConfigStore( - environment="dev", - application="product-catalogue", - name="configuration", - envelope = "feature_flags" - ) - - feature_flags = FeatureFlags(store=app_config) - - config = app_config.get_raw_configuration + ```python hl_lines="12-13" + --8<-- "examples/feature_flags/src/getting_stored_features.py" ``` ### Schema diff --git a/examples/feature_flags/src/getting_stored_features.py b/examples/feature_flags/src/getting_stored_features.py new file mode 100644 index 00000000000..07f115375a6 --- /dev/null +++ b/examples/feature_flags/src/getting_stored_features.py @@ -0,0 +1,10 @@ +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags + +app_config = AppConfigStore( + environment="dev", application="product-catalogue", name="configuration", envelope="feature_flags" +) + +feature_flags = FeatureFlags(store=app_config) + +config = app_config.get_raw_configuration +... From 8898a15b7bc021b529098d735823a31d37c63191 Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 21:41:13 +0100 Subject: [PATCH 13/17] docs: refactoring examples - custom feature flags provider --- docs/utilities/feature_flags.md | 34 +++++++++++++++-- .../src/custom_s3_store_provider.py | 38 +++++++++++++++++++ .../src/working_with_own_s3_store_provider.py | 22 +++++++++++ ...g_with_own_s3_store_provider_features.json | 5 +++ ...ng_with_own_s3_store_provider_payload.json | 4 ++ 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 examples/feature_flags/src/custom_s3_store_provider.py create mode 100644 examples/feature_flags/src/working_with_own_s3_store_provider.py create mode 100644 examples/feature_flags/src/working_with_own_s3_store_provider_features.json create mode 100644 examples/feature_flags/src/working_with_own_s3_store_provider_payload.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 23376ac88ed..5ce48572e5b 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -435,10 +435,38 @@ For this to work, you need to use a JMESPath expression via the `envelope` param --8<-- "examples/feature_flags/src/extracting_envelope_features.json" ``` -### Built-in store provider +### Create your own store provider -???+ info - For GA, you'll be able to bring your own store. +You can create your own custom FeatureFlags store provider by inheriting the `StoreProvider` class, and implementing both `get_raw_configuration()` and `get_configuration()` methods to retrieve the configuration from your custom store. + +Here are an example of implementing a custom store provider using Amazon S3, a popular object storage. + +???+ note + This is just one example of how you can create your own store provider. Before creating a custom store provider, carefully evaluate your requirements and consider factors such as performance, scalability, and ease of maintenance. + +=== "working_with_own_s3_store_provider.py" + + ```python hl_lines="9" + --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider.py" + ``` + +=== "custom_s3_store_provider.py" + + ```python hl_lines="9" + --8<-- "examples/feature_flags/src/custom_s3_store_provider.py" + ``` + +=== "working_with_own_s3_store_provider_payload.json" + + ```json hl_lines="9-15" + --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_payload.json" + ``` + +=== "working_with_own_s3_store_provider_features.json" + + ```json hl_lines="9-15" + --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_features.json" + ``` #### AppConfig diff --git a/examples/feature_flags/src/custom_s3_store_provider.py b/examples/feature_flags/src/custom_s3_store_provider.py new file mode 100644 index 00000000000..ea2c8a876be --- /dev/null +++ b/examples/feature_flags/src/custom_s3_store_provider.py @@ -0,0 +1,38 @@ +import json +from typing import Any, Dict + +import boto3 +from botocore.exceptions import ClientError + +from aws_lambda_powertools.utilities.feature_flags.base import StoreProvider +from aws_lambda_powertools.utilities.feature_flags.exceptions import ( + ConfigurationStoreError, +) + + +class S3StoreProvider(StoreProvider): + def __init__(self, bucket_name: str, object_key: str): + # Initialize the client to your custom store provider + + super().__init__() + + self.bucket_name = bucket_name + self.object_key = object_key + self.client = boto3.client("s3") + + def _get_s3_object(self) -> Dict[str, Any]: + # Retrieve the object content + parameters = {"Bucket": self.bucket_name, "Key": self.object_key} + + try: + response = self.client.get_object(**parameters) + return json.loads(response["Body"].read().decode()) + except ClientError as exc: + raise ConfigurationStoreError("Unable to get S3 Store Provider configuration file") from exc + + def get_configuration(self) -> Dict[str, Any]: + return self._get_s3_object() + + @property + def get_raw_configuration(self) -> Dict[str, Any]: + return self._get_s3_object() diff --git a/examples/feature_flags/src/working_with_own_s3_store_provider.py b/examples/feature_flags/src/working_with_own_s3_store_provider.py new file mode 100644 index 00000000000..ad7488388a4 --- /dev/null +++ b/examples/feature_flags/src/working_with_own_s3_store_provider.py @@ -0,0 +1,22 @@ +from typing import Any + +from custom_s3_store_provider import S3StoreProvider + +from aws_lambda_powertools.utilities.feature_flags import FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +s3_config_store = S3StoreProvider("your-bucket-name", "working_with_own_s3_store_provider_features.json") + +feature_flags = FeatureFlags(store=s3_config_store) + + +def lambda_handler(event: dict, context: LambdaContext): + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + price: Any = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/examples/feature_flags/src/working_with_own_s3_store_provider_features.json b/examples/feature_flags/src/working_with_own_s3_store_provider_features.json new file mode 100644 index 00000000000..fe692cdf0c3 --- /dev/null +++ b/examples/feature_flags/src/working_with_own_s3_store_provider_features.json @@ -0,0 +1,5 @@ +{ + "ten_percent_off_campaign": { + "default": true + } +} diff --git a/examples/feature_flags/src/working_with_own_s3_store_provider_payload.json b/examples/feature_flags/src/working_with_own_s3_store_provider_payload.json new file mode 100644 index 00000000000..b2a71282f8e --- /dev/null +++ b/examples/feature_flags/src/working_with_own_s3_store_provider_payload.json @@ -0,0 +1,4 @@ +{ + "product": "laptop", + "price": 1000 +} From 7be9d20fb1267c4f5d7e110f68b0abc7d08c2927 Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Sun, 7 May 2023 21:56:35 +0100 Subject: [PATCH 14/17] docs: refactoring examples - highlighting --- docs/utilities/feature_flags.md | 55 +++++++++++++++++---------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 5ce48572e5b..4bdeec87074 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -6,7 +6,7 @@ description: Utility The feature flags utility provides a simple rule engine to define when one or multiple features should be enabled depending on the input. ???+ info - We currently only support AppConfig using [freeform configuration profile](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html#appconfig-creating-configuration-and-profile-free-form-configurations){target="_blank"} . + When using `AppConfigStore`, we currently only support AppConfig using [freeform configuration profile](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html#appconfig-creating-configuration-and-profile-free-form-configurations){target="_blank"} . ## Terminology @@ -37,12 +37,13 @@ If you want to learn more about feature flags, their variations and trade-offs, * Fetch one or all feature flags enabled for a given application context * Support for static feature flags to simply turn on/off a feature without rules * Support for time based feature flags +* Bring Your Own Feature Flags Store Provider ## Getting started ### IAM Permissions -Your Lambda function IAM Role must have `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` IAM permissions before using this feature. +When using the default store `AppConfigStore`, your Lambda function IAM Role must have `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` IAM permissions before using this feature. ### Required resources @@ -131,7 +132,7 @@ The `evaluate` method supports two optional parameters: === "getting_started_single_feature_flag.py" - ```python hl_lines="3 9 13 17-19" + ```python hl_lines="3 8 27 31" --8<-- "examples/feature_flags/src/getting_started_single_feature_flag.py" ``` @@ -154,7 +155,7 @@ In this case, we could omit the `context` parameter and simply evaluate whether === "getting_started_static_flag.py" - ```python hl_lines="12-13" + ```python hl_lines="3 8 16" --8<-- "examples/feature_flags/src/getting_started_static_flag.py" ``` === "getting_started_static_flag_payload.json" @@ -165,7 +166,7 @@ In this case, we could omit the `context` parameter and simply evaluate whether === "getting_started_static_flag_features.json" - ```json hl_lines="2-3" + ```json hl_lines="2-4" --8<-- "examples/feature_flags/src/getting_started_static_flag_features.json" ``` @@ -177,7 +178,7 @@ You can use `get_enabled_features` method for scenarios where you need a list of === "getting_all_enabled_features.py" - ```python hl_lines="12-13" + ```python hl_lines="2 9 26" --8<-- "examples/feature_flags/src/getting_all_enabled_features.py" ``` @@ -189,7 +190,7 @@ You can use `get_enabled_features` method for scenarios where you need a list of === "getting_all_enabled_features_features.json" - ```json hl_lines="17-18 20 27-29" + ```json hl_lines="2 8-12 17-18 20 27-28 30" --8<-- "examples/feature_flags/src/getting_all_enabled_features_features.json" ``` @@ -209,7 +210,7 @@ You can also have features enabled only at certain times of the day for premium === "timebased_feature.py" - ```python hl_lines="12" + ```python hl_lines="1 6 40" --8<-- "examples/feature_flags/src/timebased_feature.py" ``` @@ -229,13 +230,13 @@ You can also have features enabled only at certain times of the day. === "timebased_happyhour_feature.py" - ```python hl_lines="9" + ```python hl_lines="1 6 29" --8<-- "examples/feature_flags/src/timebased_happyhour_feature.py" ``` === "timebased_happyhour_features.json" - ```json hl_lines="9-15" + ```json hl_lines="9-14" --8<-- "examples/feature_flags/src/timebased_happyhour_features.json" ``` @@ -243,7 +244,7 @@ You can also have features enabled only at specific days, for example: enable ch === "datetime_feature.py" - ```python hl_lines="10" + ```python hl_lines="1 6 31" --8<-- "examples/feature_flags/src/datetime_feature.py" ``` @@ -270,7 +271,7 @@ Feature flags can return any JSON values when `boolean_type` parameter is set to === "beyond_boolean.py" - ```python hl_lines="12" + ```python hl_lines="3 8 16" --8<-- "examples/feature_flags/src/beyond_boolean.py" ``` @@ -282,7 +283,7 @@ Feature flags can return any JSON values when `boolean_type` parameter is set to === "beyond_boolean_features.json" - ```json hl_lines="9-11 14-21" + ```json hl_lines="7-11 14-16" --8<-- "examples/feature_flags/src/beyond_boolean_features.json" ``` @@ -296,7 +297,7 @@ You can override `max_age` parameter when instantiating the store. === "getting_started_with_cache.py" - ```python hl_lines="12-13" + ```python hl_lines="6" --8<-- "examples/feature_flags/src/getting_started_with_cache.py" ``` === "getting_started_with_cache_payload.json" @@ -307,7 +308,7 @@ You can override `max_age` parameter when instantiating the store. === "getting_started_with_cache_features.json" - ```json hl_lines="2-3" + ```json hl_lines="2-4" --8<-- "examples/feature_flags/src/getting_started_with_cache_features.json" ``` @@ -322,7 +323,7 @@ You can access the configuration fetched from the store via `get_raw_configurati === "getting_stored_features.py" - ```python hl_lines="12-13" + ```python hl_lines="9" --8<-- "examples/feature_flags/src/getting_stored_features.py" ``` @@ -419,19 +420,19 @@ For this to work, you need to use a JMESPath expression via the `envelope` param === "extracting_envelope.py" - ```python hl_lines="9" + ```python hl_lines="7" --8<-- "examples/feature_flags/src/extracting_envelope.py" ``` === "extracting_envelope_payload.json" - ```json hl_lines="9-15" + ```json hl_lines="2-3" --8<-- "examples/feature_flags/src/extracting_envelope_payload.json" ``` === "extracting_envelope_features.json" - ```json hl_lines="9-15" + ```json hl_lines="6" --8<-- "examples/feature_flags/src/extracting_envelope_features.json" ``` @@ -446,25 +447,25 @@ Here are an example of implementing a custom store provider using Amazon S3, a p === "working_with_own_s3_store_provider.py" - ```python hl_lines="9" + ```python hl_lines="3 8 10" --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider.py" ``` === "custom_s3_store_provider.py" - ```python hl_lines="9" + ```python hl_lines="33 37" --8<-- "examples/feature_flags/src/custom_s3_store_provider.py" ``` === "working_with_own_s3_store_provider_payload.json" - ```json hl_lines="9-15" + ```json hl_lines="2 3" --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_payload.json" ``` === "working_with_own_s3_store_provider_features.json" - ```json hl_lines="9-15" + ```json hl_lines="2-4" --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_features.json" ``` @@ -487,19 +488,19 @@ These are the available options for further customization. === "appconfig_provider_options.py" - ```python hl_lines="9" + ```python hl_lines="9 13-17 20 28-30" --8<-- "examples/feature_flags/src/appconfig_provider_options.py" ``` === "appconfig_provider_options_payload.json" - ```json hl_lines="9-15" + ```json hl_lines="2 3" --8<-- "examples/feature_flags/src/appconfig_provider_options_payload.json" ``` === "appconfig_provider_options_features.json" - ```json hl_lines="9-15" + ```json hl_lines="6-9" --8<-- "examples/feature_flags/src/appconfig_provider_options_features.json" ``` @@ -514,7 +515,7 @@ You can unit test your feature flags locally and independently without setting u === "Testing your code" - ```python hl_lines="7-9" + ```python hl_lines="11-13" --8<-- "examples/feature_flags/src/getting_started_with_tests.py" ``` From 0095de7a502bcc4e0f06647ae9709168f4c85cdd Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Mon, 8 May 2023 16:02:43 +0100 Subject: [PATCH 15/17] docs: refactoring examples - fix menu --- docs/utilities/feature_flags.md | 66 +++++++++++++++++---------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 4bdeec87074..ffd16135555 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -436,38 +436,7 @@ For this to work, you need to use a JMESPath expression via the `envelope` param --8<-- "examples/feature_flags/src/extracting_envelope_features.json" ``` -### Create your own store provider - -You can create your own custom FeatureFlags store provider by inheriting the `StoreProvider` class, and implementing both `get_raw_configuration()` and `get_configuration()` methods to retrieve the configuration from your custom store. - -Here are an example of implementing a custom store provider using Amazon S3, a popular object storage. - -???+ note - This is just one example of how you can create your own store provider. Before creating a custom store provider, carefully evaluate your requirements and consider factors such as performance, scalability, and ease of maintenance. - -=== "working_with_own_s3_store_provider.py" - - ```python hl_lines="3 8 10" - --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider.py" - ``` - -=== "custom_s3_store_provider.py" - - ```python hl_lines="33 37" - --8<-- "examples/feature_flags/src/custom_s3_store_provider.py" - ``` - -=== "working_with_own_s3_store_provider_payload.json" - - ```json hl_lines="2 3" - --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_payload.json" - ``` - -=== "working_with_own_s3_store_provider_features.json" - - ```json hl_lines="2-4" - --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_features.json" - ``` +### Built-in store provider #### AppConfig @@ -504,6 +473,39 @@ These are the available options for further customization. --8<-- "examples/feature_flags/src/appconfig_provider_options_features.json" ``` +### Create your own store provider + +You can create your own custom FeatureFlags store provider by inheriting the `StoreProvider` class, and implementing both `get_raw_configuration()` and `get_configuration()` methods to retrieve the configuration from your custom store. + +Here are an example of implementing a custom store provider using Amazon S3, a popular object storage. + +???+ note + This is just one example of how you can create your own store provider. Before creating a custom store provider, carefully evaluate your requirements and consider factors such as performance, scalability, and ease of maintenance. + +=== "working_with_own_s3_store_provider.py" + + ```python hl_lines="3 8 10" + --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider.py" + ``` + +=== "custom_s3_store_provider.py" + + ```python hl_lines="33 37" + --8<-- "examples/feature_flags/src/custom_s3_store_provider.py" + ``` + +=== "working_with_own_s3_store_provider_payload.json" + + ```json hl_lines="2 3" + --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_payload.json" + ``` + +=== "working_with_own_s3_store_provider_features.json" + + ```json hl_lines="2-4" + --8<-- "examples/feature_flags/src/working_with_own_s3_store_provider_features.json" + ``` + ## Testing your code You can unit test your feature flags locally and independently without setting up AWS AppConfig. From 78ca4827bf637c8dc8c50359635032edec5b27ce Mon Sep 17 00:00:00 2001 From: Ruben Fonseca <fonseka@gmail.com> Date: Thu, 11 May 2023 15:14:14 +0200 Subject: [PATCH 16/17] fix: typos --- docs/utilities/feature_flags.md | 2 +- examples/feature_flags/src/beyond_boolean.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index ffd16135555..e333a27be2e 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -132,7 +132,7 @@ The `evaluate` method supports two optional parameters: === "getting_started_single_feature_flag.py" - ```python hl_lines="3 8 27 31" + ```python hl_lines="3 6 8 27 31" --8<-- "examples/feature_flags/src/getting_started_single_feature_flag.py" ``` diff --git a/examples/feature_flags/src/beyond_boolean.py b/examples/feature_flags/src/beyond_boolean.py index a7436079019..bd5ad021909 100644 --- a/examples/feature_flags/src/beyond_boolean.py +++ b/examples/feature_flags/src/beyond_boolean.py @@ -12,7 +12,7 @@ def lambda_handler(event: dict, context: LambdaContext): # Get customer's tier from incoming request ctx = {"tier": event.get("tier", "standard")} - # Evaluate `has_premium_features` base don customer's tier + # Evaluate `has_premium_features` based on customer's tier premium_features: Any = feature_flags.evaluate(name="premium_features", context=ctx, default=[]) return {"Premium features enabled": premium_features} From 8b2c266ef31a73f3e8f20758ba32d44417977de5 Mon Sep 17 00:00:00 2001 From: Leandro Damascena <leandro.damascena@gmail.com> Date: Thu, 11 May 2023 19:21:02 +0100 Subject: [PATCH 17/17] docs: Ruben's feedback --- docs/utilities/feature_flags.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index e333a27be2e..efe41c2f82f 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -37,7 +37,7 @@ If you want to learn more about feature flags, their variations and trade-offs, * Fetch one or all feature flags enabled for a given application context * Support for static feature flags to simply turn on/off a feature without rules * Support for time based feature flags -* Bring Your Own Feature Flags Store Provider +* Bring your own Feature Flags Store Provider ## Getting started @@ -477,6 +477,9 @@ These are the available options for further customization. You can create your own custom FeatureFlags store provider by inheriting the `StoreProvider` class, and implementing both `get_raw_configuration()` and `get_configuration()` methods to retrieve the configuration from your custom store. +* **`get_raw_configuration()`** – get the raw configuration from the store provider and return the parsed JSON dictionary +* **`get_configuration()`** – get the configuration from the store provider, parsing it as a JSON dictionary. If an envelope is set, extract the envelope data + Here are an example of implementing a custom store provider using Amazon S3, a popular object storage. ???+ note