Skip to content

Commit ef09c8b

Browse files
docs(feature_flags): snippets split, improved, and lint (#2222)
Co-authored-by: Ruben Fonseca <[email protected]>
1 parent 099487e commit ef09c8b

36 files changed

+860
-521
lines changed

Diff for: docs/utilities/feature_flags.md

+175-512
Large diffs are not rendered by default.

Diff for: examples/feature_flags/sam/template.yaml

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
AWSTemplateFormatVersion: "2010-09-09"
2+
Description: Lambda Powertools for Python Feature flags sample template
3+
Resources:
4+
FeatureStoreApp:
5+
Type: AWS::AppConfig::Application
6+
Properties:
7+
Description: "AppConfig Application for feature toggles"
8+
Name: product-catalogue
9+
10+
FeatureStoreDevEnv:
11+
Type: AWS::AppConfig::Environment
12+
Properties:
13+
ApplicationId: !Ref FeatureStoreApp
14+
Description: "Development Environment for the App Config Store"
15+
Name: dev
16+
17+
FeatureStoreConfigProfile:
18+
Type: AWS::AppConfig::ConfigurationProfile
19+
Properties:
20+
ApplicationId: !Ref FeatureStoreApp
21+
Name: features
22+
LocationUri: "hosted"
23+
24+
HostedConfigVersion:
25+
Type: AWS::AppConfig::HostedConfigurationVersion
26+
Properties:
27+
ApplicationId: !Ref FeatureStoreApp
28+
ConfigurationProfileId: !Ref FeatureStoreConfigProfile
29+
Description: 'A sample hosted configuration version'
30+
Content: |
31+
{
32+
"premium_features": {
33+
"default": false,
34+
"rules": {
35+
"customer tier equals premium": {
36+
"when_match": true,
37+
"conditions": [
38+
{
39+
"action": "EQUALS",
40+
"key": "tier",
41+
"value": "premium"
42+
}
43+
]
44+
}
45+
}
46+
},
47+
"ten_percent_off_campaign": {
48+
"default": false
49+
}
50+
}
51+
ContentType: 'application/json'
52+
53+
ConfigDeployment:
54+
Type: AWS::AppConfig::Deployment
55+
Properties:
56+
ApplicationId: !Ref FeatureStoreApp
57+
ConfigurationProfileId: !Ref FeatureStoreConfigProfile
58+
ConfigurationVersion: !Ref HostedConfigVersion
59+
DeploymentStrategyId: "AppConfig.AllAtOnce"
60+
EnvironmentId: !Ref FeatureStoreDevEnv
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import Any
2+
3+
from botocore.config import Config
4+
from jmespath.functions import Functions, signature
5+
6+
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
7+
from aws_lambda_powertools.utilities.typing import LambdaContext
8+
9+
boto_config = Config(read_timeout=10, retries={"total_max_attempts": 2})
10+
11+
12+
# Custom JMESPath functions
13+
class CustomFunctions(Functions):
14+
@signature({"types": ["object"]})
15+
def _func_special_decoder(self, features):
16+
# You can add some logic here
17+
return features
18+
19+
20+
custom_jmespath_options = {"custom_functions": CustomFunctions()}
21+
22+
23+
app_config = AppConfigStore(
24+
environment="dev",
25+
application="product-catalogue",
26+
name="features",
27+
max_age=120,
28+
envelope="special_decoder(features)", # using a custom function defined in CustomFunctions Class
29+
sdk_config=boto_config,
30+
jmespath_options=custom_jmespath_options,
31+
)
32+
33+
feature_flags = FeatureFlags(store=app_config)
34+
35+
36+
def lambda_handler(event: dict, context: LambdaContext):
37+
apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False)
38+
39+
price: Any = event.get("price")
40+
41+
if apply_discount:
42+
# apply 10% discount to product
43+
price = price * 0.9
44+
45+
return {"price": price}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"logging": {
3+
"level": "INFO",
4+
"sampling_rate": 0.1
5+
},
6+
"features": {
7+
"ten_percent_off_campaign": {
8+
"default": true
9+
}
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"product": "laptop",
3+
"price": 1000
4+
}

Diff for: examples/feature_flags/src/beyond_boolean.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Any
2+
3+
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
4+
from aws_lambda_powertools.utilities.typing import LambdaContext
5+
6+
app_config = AppConfigStore(environment="dev", application="comments", name="config")
7+
8+
feature_flags = FeatureFlags(store=app_config)
9+
10+
11+
def lambda_handler(event: dict, context: LambdaContext):
12+
# Get customer's tier from incoming request
13+
ctx = {"tier": event.get("tier", "standard")}
14+
15+
# Evaluate `has_premium_features` based on customer's tier
16+
premium_features: Any = feature_flags.evaluate(name="premium_features", context=ctx, default=[])
17+
18+
return {"Premium features enabled": premium_features}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"premium_features": {
3+
"boolean_type": false,
4+
"default": [],
5+
"rules": {
6+
"customer tier equals premium": {
7+
"when_match": [
8+
"no_ads",
9+
"no_limits",
10+
"chat"
11+
],
12+
"conditions": [
13+
{
14+
"action": "EQUALS",
15+
"key": "tier",
16+
"value": "premium"
17+
}
18+
]
19+
}
20+
}
21+
}
22+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"username": "lessa",
3+
"tier": "premium",
4+
"basked_id": "random_id"
5+
}

Diff for: examples/feature_flags/src/conditions.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"conditions": [
3+
{
4+
"action": "EQUALS",
5+
"key": "tier",
6+
"value": "premium"
7+
}
8+
]
9+
}
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import json
2+
from typing import Any, Dict
3+
4+
import boto3
5+
from botocore.exceptions import ClientError
6+
7+
from aws_lambda_powertools.utilities.feature_flags.base import StoreProvider
8+
from aws_lambda_powertools.utilities.feature_flags.exceptions import (
9+
ConfigurationStoreError,
10+
)
11+
12+
13+
class S3StoreProvider(StoreProvider):
14+
def __init__(self, bucket_name: str, object_key: str):
15+
# Initialize the client to your custom store provider
16+
17+
super().__init__()
18+
19+
self.bucket_name = bucket_name
20+
self.object_key = object_key
21+
self.client = boto3.client("s3")
22+
23+
def _get_s3_object(self) -> Dict[str, Any]:
24+
# Retrieve the object content
25+
parameters = {"Bucket": self.bucket_name, "Key": self.object_key}
26+
27+
try:
28+
response = self.client.get_object(**parameters)
29+
return json.loads(response["Body"].read().decode())
30+
except ClientError as exc:
31+
raise ConfigurationStoreError("Unable to get S3 Store Provider configuration file") from exc
32+
33+
def get_configuration(self) -> Dict[str, Any]:
34+
return self._get_s3_object()
35+
36+
@property
37+
def get_raw_configuration(self) -> Dict[str, Any]:
38+
return self._get_s3_object()

Diff for: examples/feature_flags/src/datetime_feature.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
2+
from aws_lambda_powertools.utilities.typing import LambdaContext
23

34
app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features")
45

56
feature_flags = FeatureFlags(store=app_config)
67

78

8-
def lambda_handler(event, context):
9-
# Get customer's tier from incoming request
9+
def lambda_handler(event: dict, context: LambdaContext):
10+
"""
11+
This feature flag is enabled under the following conditions:
12+
- Start date: December 25th, 2022 at 12:00:00 PM EST
13+
- End date: December 31st, 2022 at 11:59:59 PM EST
14+
- Timezone: America/New_York
15+
16+
Rule condition to be evaluated:
17+
"conditions": [
18+
{
19+
"action": "SCHEDULE_BETWEEN_DATETIME_RANGE",
20+
"key": "CURRENT_DATETIME",
21+
"value": {
22+
"START": "2022-12-25T12:00:00",
23+
"END": "2022-12-31T23:59:59",
24+
"TIMEZONE": "America/New_York"
25+
}
26+
}
27+
]
28+
"""
29+
30+
# Checking if the Christmas discount is enable
1031
xmas_discount = feature_flags.evaluate(name="christmas_discount", default=False)
1132

1233
if xmas_discount:
1334
# Enable special discount on christmas:
14-
pass
35+
return {"message": "The Christmas discount is enabled."}
36+
37+
return {"message": "The Christmas discount is not enabled."}

Diff for: examples/feature_flags/src/extracting_envelope.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Any
2+
3+
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
4+
from aws_lambda_powertools.utilities.typing import LambdaContext
5+
6+
app_config = AppConfigStore(
7+
environment="dev", application="product-catalogue", name="features", envelope="feature_flags"
8+
)
9+
10+
feature_flags = FeatureFlags(store=app_config)
11+
12+
13+
def lambda_handler(event: dict, context: LambdaContext):
14+
apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False)
15+
16+
price: Any = event.get("price")
17+
18+
if apply_discount:
19+
# apply 10% discount to product
20+
price = price * 0.9
21+
22+
return {"price": price}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"logging": {
3+
"level": "INFO",
4+
"sampling_rate": 0.1
5+
},
6+
"features": {
7+
"ten_percent_off_campaign": {
8+
"default": true
9+
}
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"product": "laptop",
3+
"price": 1000
4+
}

Diff for: examples/feature_flags/src/feature_with_rules.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"premium_feature": {
3+
"default": false,
4+
"rules": {
5+
"customer tier equals premium": {
6+
"when_match": true,
7+
"conditions": [
8+
{
9+
"action": "EQUALS",
10+
"key": "tier",
11+
"value": "premium"
12+
}
13+
]
14+
}
15+
}
16+
},
17+
"non_boolean_premium_feature": {
18+
"default": [],
19+
"rules": {
20+
"customer tier equals premium": {
21+
"when_match": ["remove_limits", "remove_ads"],
22+
"conditions": [
23+
{
24+
"action": "EQUALS",
25+
"key": "tier",
26+
"value": "premium"
27+
}
28+
]
29+
}
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
2+
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
5+
app = APIGatewayRestResolver()
6+
7+
app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features")
8+
9+
feature_flags = FeatureFlags(store=app_config)
10+
11+
12+
@app.get("/products")
13+
def list_products():
14+
# getting fields from request
15+
# https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/#accessing-request-details
16+
json_body = app.current_event.json_body
17+
headers = app.current_event.headers
18+
19+
ctx = {**headers, **json_body}
20+
21+
# getting price from payload
22+
price: float = float(json_body.get("price"))
23+
percent_discount: int = 0
24+
25+
# all_features is evaluated to ["premium_features", "geo_customer_campaign", "ten_percent_off_campaign"]
26+
all_features: list[str] = feature_flags.get_enabled_features(context=ctx)
27+
28+
if "geo_customer_campaign" in all_features:
29+
# apply 20% discounts for customers in NL
30+
percent_discount += 20
31+
32+
if "ten_percent_off_campaign" in all_features:
33+
# apply additional 10% for all customers
34+
percent_discount += 10
35+
36+
price = price * (100 - percent_discount) / 100
37+
38+
return {"price": price}
39+
40+
41+
def lambda_handler(event: dict, context: LambdaContext):
42+
return app.resolve(event, context)

0 commit comments

Comments
 (0)