Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions devcycle_python_sdk/models/user.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ruff: noqa: N815
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Dict, Optional, Any
from typing import Dict, Optional, Any, cast
from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import TargetingKeyMissingError, InvalidContextError

Expand Down Expand Up @@ -114,10 +114,10 @@ def create_user_from_context(
user_id = context.targeting_key
user_id_source = "targeting_key"
elif context.attributes and "user_id" in context.attributes.keys():
user_id = context.attributes["user_id"]
user_id = cast(str, context.attributes["user_id"])
user_id_source = "user_id"
elif context.attributes and "userId" in context.attributes.keys():
user_id = context.attributes["userId"]
user_id = cast(str, context.attributes["userId"])
user_id_source = "userId"

if not user_id:
Expand Down
12 changes: 7 additions & 5 deletions devcycle_python_sdk/open_feature_provider/provider.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import logging
import time

from typing import Any, Optional, Union, List
from typing import Any, Optional, Union, List, Mapping, Sequence

from devcycle_python_sdk import AbstractDevCycleClient
from devcycle_python_sdk.models.user import DevCycleUser

from openfeature.provider import AbstractProvider
from openfeature.provider.metadata import Metadata
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.flag_evaluation import FlagResolutionDetails, Reason, FlagValueType
from openfeature.exception import (
ErrorCode,
InvalidContextError,
Expand Down Expand Up @@ -138,10 +138,12 @@ def resolve_float_details(
def resolve_object_details(
self,
flag_key: str,
default_value: Union[dict, list],
default_value: Union[Mapping[str, FlagValueType], Sequence[FlagValueType]],
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[Union[dict, list]]:
if not isinstance(default_value, dict):
) -> FlagResolutionDetails[
Union[Mapping[str, FlagValueType], Sequence[FlagValueType]]
]:
if not isinstance(default_value, Mapping):
raise TypeMismatchError("Default value must be a flat dictionary")

if default_value:
Expand Down
6 changes: 0 additions & 6 deletions example/django-app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

Expand All @@ -34,7 +33,6 @@
}
}


# Application definition

INSTALLED_APPS = [
Expand Down Expand Up @@ -78,7 +76,6 @@

WSGI_APPLICATION = "config.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

Expand All @@ -89,7 +86,6 @@
}
}


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

Expand All @@ -108,7 +104,6 @@
},
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

Expand All @@ -120,7 +115,6 @@

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

Expand Down
Binary file added example/django-app/db.sqlite3
Binary file not shown.
195 changes: 181 additions & 14 deletions example/openfeature_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,32 @@
from openfeature import api
from openfeature.evaluation_context import EvaluationContext

FLAG_KEY = "test-boolean-variable"

logger = logging.getLogger(__name__)


def main():
"""
Sample usage of the DevCycle OpenFeature Provider along with the Python Server SDK using Local Bucketing.

This example demonstrates how to use all variable types supported by DevCycle through OpenFeature:
- Boolean variables
- String variables
- Number variables (integer and float)
- JSON object variables

See DEVCYCLE_SETUP.md for instructions on creating the required variables in DevCycle.
"""
logging.basicConfig(level="INFO", format="%(levelname)s: %(message)s")

# create an instance of the DevCycle Client object
server_sdk_key = os.environ["DEVCYCLE_SERVER_SDK_KEY"]
server_sdk_key = os.environ.get("DEVCYCLE_SERVER_SDK_KEY")
if not server_sdk_key:
logger.error("DEVCYCLE_SERVER_SDK_KEY environment variable is not set")
logger.error(
"Please set it with: export DEVCYCLE_SERVER_SDK_KEY='your-sdk-key'"
)
exit(1)

devcycle_client = DevCycleLocalClient(server_sdk_key, DevCycleLocalOptions())

# Wait for DevCycle to initialize and load the configuration
Expand All @@ -32,6 +45,8 @@ def main():
logger.error("DevCycle failed to initialize")
exit(1)

logger.info("DevCycle initialized successfully!\n")

# set the provider for OpenFeature
api.set_provider(devcycle_client.get_openfeature_provider())

Expand All @@ -53,22 +68,174 @@ def main():
},
)

# Look up the value of the flag
if open_feature_client.get_boolean_value(FLAG_KEY, False, context):
logger.info(f"Variable {FLAG_KEY} is enabled")
logger.info("=" * 60)
logger.info("Testing Boolean Variable")
logger.info("=" * 60)

# Test Boolean Variable
boolean_details = open_feature_client.get_boolean_details(
"test-boolean-variable", False, context
)
logger.info("Variable Key: test-boolean-variable")
logger.info("Value: {boolean_details.value}")
logger.info("Reason: {boolean_details.reason}")
if boolean_details.value:
logger.info("✓ Boolean variable is ENABLED")
else:
logger.info(f"Variable {FLAG_KEY} is not enabled")
logger.info("✗ Boolean variable is DISABLED")

logger.info("\n" + "=" * 60)
logger.info("Testing String Variable")
logger.info("=" * 60)

# Fetch a JSON object variable
json_object = open_feature_client.get_object_value(
# Test String Variable
string_details = open_feature_client.get_string_details(
"test-string-variable", "default string", context
)
logger.info("Variable Key: test-string-variable")
logger.info("Value: {string_details.value}")
logger.info("Reason: {string_details.reason}")

logger.info("\n" + "=" * 60)
logger.info("Testing Number Variable (Integer)")
logger.info("=" * 60)

# Test Number Variable (Integer)
integer_details = open_feature_client.get_integer_details(
"test-number-variable", 0, context
)
logger.info("Variable Key: test-number-variable")
logger.info("Value: {integer_details.value}")
logger.info("Reason: {integer_details.reason}")

logger.info("\n" + "=" * 60)
logger.info("Testing Number Variable (Float)")
logger.info("=" * 60)

# Test Number Variable as Float
# Note: If the DevCycle variable is an integer, it will be cast to float
float_value = open_feature_client.get_float_value(
"test-number-variable", 0.0, context
)
logger.info("Variable Key: test-number-variable (as float)")
logger.info("Value: {float_value}")

logger.info("\n" + "=" * 60)
logger.info("Testing JSON Object Variable")
logger.info("=" * 60)

# Test JSON Object Variable
json_details = open_feature_client.get_object_details(
"test-json-variable", {"default": "value"}, context
)
logger.info(f"JSON Object Value: {json_object}")
logger.info("Variable Key: test-json-variable")
logger.info("Value: {json_details.value}")
logger.info("Reason: {json_details.reason}")

logger.info("\n" + "=" * 60)
logger.info("Testing Object Variable - Empty Dictionary")
logger.info("=" * 60)

# Test with empty dictionary default (valid per OpenFeature spec)
empty_dict_result = open_feature_client.get_object_value(
"test-json-variable", {}, context
)
logger.info("Variable Key: test-json-variable (with empty default)")
logger.info("Value: {empty_dict_result}")

logger.info("\n" + "=" * 60)
logger.info("Testing Object Variable - Mixed Types")
logger.info("=" * 60)

# Test with flat dictionary containing mixed primitive types
# OpenFeature allows string, int, float, bool, and None in flat dictionaries
mixed_default = {
"string_key": "hello",
"int_key": 42,
"float_key": 3.14,
"bool_key": True,
"none_key": None,
}
mixed_result = open_feature_client.get_object_value(
"test-json-variable", mixed_default, context
)
logger.info("Variable Key: test-json-variable (with mixed types)")
logger.info("Value: {mixed_result}")
logger.info(
f"Value types: {[(k, type(v).__name__) for k, v in mixed_result.items()]}"
)

logger.info("\n" + "=" * 60)
logger.info("Testing Object Variable - All String Values")
logger.info("=" * 60)

# Test with all string values
string_dict_default = {
"name": "John Doe",
"email": "[email protected]",
"status": "active",
}
string_dict_result = open_feature_client.get_object_value(
"test-json-variable", string_dict_default, context
)
logger.info("Variable Key: test-json-variable (all strings)")
logger.info("Value: {string_dict_result}")

logger.info("\n" + "=" * 60)
logger.info("Testing Object Variable - Numeric Values")
logger.info("=" * 60)

# Test with numeric values (integers and floats)
numeric_dict_default = {"count": 100, "percentage": 85.5, "threshold": 0}
numeric_dict_result = open_feature_client.get_object_value(
"test-json-variable", numeric_dict_default, context
)
logger.info("Variable Key: test-json-variable (numeric)")
logger.info("Value: {numeric_dict_result}")

logger.info("\n" + "=" * 60)
logger.info("Testing Object Variable - Boolean Flags")
logger.info("=" * 60)

# Test with boolean values
bool_dict_default = {"feature_a": True, "feature_b": False, "feature_c": True}
bool_dict_result = open_feature_client.get_object_value(
"test-json-variable", bool_dict_default, context
)
logger.info("Variable Key: test-json-variable (booleans)")
logger.info("Value: {bool_dict_result}")

logger.info("\n" + "=" * 60)
logger.info("Testing Object Variable - With None Values")
logger.info("=" * 60)

# Test with None values (valid per OpenFeature spec for flat dictionaries)
none_dict_default = {
"optional_field": None,
"required_field": "value",
"nullable_count": None,
}
none_dict_result = open_feature_client.get_object_value(
"test-json-variable", none_dict_default, context
)
logger.info("Variable Key: test-json-variable (with None)")
logger.info("Value: {none_dict_result}")

logger.info("\n" + "=" * 60)
logger.info("Testing Non-Existent Variable (Should Return Default)")
logger.info("=" * 60)

# Test non-existent variable to demonstrate default handling
nonexistent_details = open_feature_client.get_string_details(
"doesnt-exist", "default fallback value", context
)
logger.info("Variable Key: doesnt-exist")
logger.info("Value: {nonexistent_details.value}")
logger.info("Reason: {nonexistent_details.reason}")

# Retrieve a string variable along with resolution details
details = open_feature_client.get_string_details("doesnt-exist", "default", context)
logger.info(f"String Value: {details.value}")
logger.info(f"Eval Reason: {details.reason}")
logger.info("\n" + "=" * 60)
logger.info("All tests completed!")
logger.info("=" * 60)

devcycle_client.close()

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ urllib3 >= 1.15.1
requests >= 2.32
wasmtime ~= 30.0.0
protobuf >= 4.23.3
openfeature-sdk == 0.8.0
openfeature-sdk ~= 0.8.0
launchdarkly-eventsource >= 1.2.1
responses >= 0.23.1
Loading