Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
189 changes: 175 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,168 @@ 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
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)
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
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
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)
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",
}
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}
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}
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,
}
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
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