Skip to content

Commit

Permalink
docs(feature_flags): snippets split, improved, and lint (#2222)
Browse files Browse the repository at this point in the history
Co-authored-by: Ruben Fonseca <fonseka@gmail.com>
  • Loading branch information
leandrodamascena and rubenfonseca authored May 16, 2023
1 parent 099487e commit ef09c8b
Show file tree
Hide file tree
Showing 36 changed files with 860 additions and 521 deletions.
687 changes: 175 additions & 512 deletions docs/utilities/feature_flags.md

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions examples/feature_flags/sam/template.yaml
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions examples/feature_flags/src/appconfig_provider_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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="product-catalogue",
name="features",
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):
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}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"logging": {
"level": "INFO",
"sampling_rate": 0.1
},
"features": {
"ten_percent_off_campaign": {
"default": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"product": "laptop",
"price": 1000
}
18 changes: 18 additions & 0 deletions examples/feature_flags/src/beyond_boolean.py
Original file line number Diff line number Diff line change
@@ -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` based on customer's tier
premium_features: Any = feature_flags.evaluate(name="premium_features", context=ctx, default=[])

return {"Premium features enabled": premium_features}
22 changes: 22 additions & 0 deletions examples/feature_flags/src/beyond_boolean_features.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
}
}
5 changes: 5 additions & 0 deletions examples/feature_flags/src/beyond_boolean_payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"username": "lessa",
"tier": "premium",
"basked_id": "random_id"
}
9 changes: 9 additions & 0 deletions examples/feature_flags/src/conditions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"conditions": [
{
"action": "EQUALS",
"key": "tier",
"value": "premium"
}
]
}
38 changes: 38 additions & 0 deletions examples/feature_flags/src/custom_s3_store_provider.py
Original file line number Diff line number Diff line change
@@ -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()
29 changes: 26 additions & 3 deletions examples/feature_flags/src/datetime_feature.py
Original file line number Diff line number Diff line change
@@ -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."}
22 changes: 22 additions & 0 deletions examples/feature_flags/src/extracting_envelope.py
Original file line number Diff line number Diff line change
@@ -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}
11 changes: 11 additions & 0 deletions examples/feature_flags/src/extracting_envelope_features.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"logging": {
"level": "INFO",
"sampling_rate": 0.1
},
"features": {
"ten_percent_off_campaign": {
"default": true
}
}
}
4 changes: 4 additions & 0 deletions examples/feature_flags/src/extracting_envelope_payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"product": "laptop",
"price": 1000
}
32 changes: 32 additions & 0 deletions examples/feature_flags/src/feature_with_rules.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
}
}
42 changes: 42 additions & 0 deletions examples/feature_flags/src/getting_all_enabled_features.py
Original file line number Diff line number Diff line change
@@ -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="product-catalogue", name="features")

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)
Loading

0 comments on commit ef09c8b

Please sign in to comment.