From 61542b3ed3cf6f08f72aa1944fea70505b0ca734 Mon Sep 17 00:00:00 2001 From: Jason Salaber Date: Wed, 22 Oct 2025 11:08:42 -0400 Subject: [PATCH 01/10] chore: change openfeature requirement to compatible patch versions --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3645631..1054d22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file From 29a2513624d302518cccbb2e435ed7d87fe16978 Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Tue, 28 Oct 2025 11:55:10 -0400 Subject: [PATCH 02/10] update the typing to match openfeature --- devcycle_python_sdk/models/user.py | 6 +++--- .../open_feature_provider/provider.py | 8 ++++---- example/django-app/db.sqlite3 | Bin 0 -> 131072 bytes 3 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 example/django-app/db.sqlite3 diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index 3110ead..2afda96 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -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 @@ -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: diff --git a/devcycle_python_sdk/open_feature_provider/provider.py b/devcycle_python_sdk/open_feature_provider/provider.py index aad750c..107e0ba 100644 --- a/devcycle_python_sdk/open_feature_provider/provider.py +++ b/devcycle_python_sdk/open_feature_provider/provider.py @@ -1,7 +1,7 @@ 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 @@ -9,7 +9,7 @@ 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, @@ -138,9 +138,9 @@ 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]]: + ) -> FlagResolutionDetails[Union[Mapping[str, FlagValueType], Sequence[FlagValueType]]]: if not isinstance(default_value, dict): raise TypeMismatchError("Default value must be a flat dictionary") diff --git a/example/django-app/db.sqlite3 b/example/django-app/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..a1afe5e01bf09b04ed1d4c80048619523876c211 GIT binary patch literal 131072 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 Dk~g6+ literal 0 HcmV?d00001 From cb8eb5dc3aed776c10693185ea8ab93ae155dcb5 Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Tue, 28 Oct 2025 11:58:34 -0400 Subject: [PATCH 03/10] lint --- devcycle_python_sdk/open_feature_provider/provider.py | 4 +++- example/django-app/config/settings.py | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/devcycle_python_sdk/open_feature_provider/provider.py b/devcycle_python_sdk/open_feature_provider/provider.py index 107e0ba..aebcb9c 100644 --- a/devcycle_python_sdk/open_feature_provider/provider.py +++ b/devcycle_python_sdk/open_feature_provider/provider.py @@ -140,7 +140,9 @@ def resolve_object_details( flag_key: str, default_value: Union[Mapping[str, FlagValueType], Sequence[FlagValueType]], evaluation_context: Optional[EvaluationContext] = None, - ) -> FlagResolutionDetails[Union[Mapping[str, FlagValueType], Sequence[FlagValueType]]]: + ) -> FlagResolutionDetails[ + Union[Mapping[str, FlagValueType], Sequence[FlagValueType]] + ]: if not isinstance(default_value, dict): raise TypeMismatchError("Default value must be a flat dictionary") diff --git a/example/django-app/config/settings.py b/example/django-app/config/settings.py index efd931b..8ae76cd 100644 --- a/example/django-app/config/settings.py +++ b/example/django-app/config/settings.py @@ -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/ @@ -34,7 +33,6 @@ } } - # Application definition INSTALLED_APPS = [ @@ -78,7 +76,6 @@ WSGI_APPLICATION = "config.wsgi.application" - # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases @@ -89,7 +86,6 @@ } } - # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators @@ -108,7 +104,6 @@ }, ] - # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ @@ -120,7 +115,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ From d25d6acfdc3904059d9f974de35433fb0b8cd9d9 Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Tue, 28 Oct 2025 12:26:28 -0400 Subject: [PATCH 04/10] fix type checking --- devcycle_python_sdk/open_feature_provider/provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devcycle_python_sdk/open_feature_provider/provider.py b/devcycle_python_sdk/open_feature_provider/provider.py index aebcb9c..7284b24 100644 --- a/devcycle_python_sdk/open_feature_provider/provider.py +++ b/devcycle_python_sdk/open_feature_provider/provider.py @@ -143,12 +143,12 @@ def resolve_object_details( ) -> FlagResolutionDetails[ Union[Mapping[str, FlagValueType], Sequence[FlagValueType]] ]: - if not isinstance(default_value, dict): + if not isinstance(default_value, Mapping): raise TypeMismatchError("Default value must be a flat dictionary") if default_value: for k, v in default_value.items(): - if not isinstance(v, (str, int, float, bool)) or v is None: + if not isinstance(v, (str, int, float, bool)) and v is not None: raise TypeMismatchError( "Default value must be a flat dictionary containing only strings, numbers, booleans or None values" ) From c43afbcc39b26b45e9a444afb224bb736be7d3e1 Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Tue, 28 Oct 2025 13:06:50 -0400 Subject: [PATCH 05/10] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- devcycle_python_sdk/open_feature_provider/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devcycle_python_sdk/open_feature_provider/provider.py b/devcycle_python_sdk/open_feature_provider/provider.py index 7284b24..6ce417c 100644 --- a/devcycle_python_sdk/open_feature_provider/provider.py +++ b/devcycle_python_sdk/open_feature_provider/provider.py @@ -148,7 +148,7 @@ def resolve_object_details( if default_value: for k, v in default_value.items(): - if not isinstance(v, (str, int, float, bool)) and v is not None: + if not isinstance(v, (str, int, float, bool)) or v is None: raise TypeMismatchError( "Default value must be a flat dictionary containing only strings, numbers, booleans or None values" ) From 6a5f0d7926bde77aa8c73796a574e3e945ac5042 Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Wed, 29 Oct 2025 09:43:13 -0400 Subject: [PATCH 06/10] update openfeature test --- example/openfeature_example.py | 203 ++++++++++++++++++++++++++++++--- 1 file changed, 187 insertions(+), 16 deletions(-) diff --git a/example/openfeature_example.py b/example/openfeature_example.py index 358bfd1..9668188 100644 --- a/example/openfeature_example.py +++ b/example/openfeature_example.py @@ -7,19 +7,30 @@ 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 @@ -32,6 +43,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()) @@ -53,22 +66,180 @@ 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(f"Variable Key: test-boolean-variable") + logger.info(f"Value: {boolean_details.value}") + logger.info(f"Reason: {boolean_details.reason}") + if boolean_details.value: + logger.info("✓ Boolean variable is ENABLED") else: - logger.info(f"Variable {FLAG_KEY} is not enabled") - - # Fetch a JSON object variable - json_object = open_feature_client.get_object_value( + logger.info("✗ Boolean variable is DISABLED") + + logger.info("\n" + "=" * 60) + logger.info("Testing String Variable") + logger.info("=" * 60) + + # Test String Variable + string_details = open_feature_client.get_string_details( + "test-string-variable", "default string", context + ) + logger.info(f"Variable Key: test-string-variable") + logger.info(f"Value: {string_details.value}") + logger.info(f"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(f"Variable Key: test-number-variable") + logger.info(f"Value: {integer_details.value}") + logger.info(f"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(f"Variable Key: test-number-variable (as float)") + logger.info(f"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}") - - # 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(f"Variable Key: test-json-variable") + logger.info(f"Value: {json_details.value}") + logger.info(f"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(f"Variable Key: test-json-variable (with empty default)") + logger.info(f"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(f"Variable Key: test-json-variable (with mixed types)") + logger.info(f"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": "john@example.com", + "status": "active" + } + string_dict_result = open_feature_client.get_object_value( + "test-json-variable", string_dict_default, context + ) + logger.info(f"Variable Key: test-json-variable (all strings)") + logger.info(f"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(f"Variable Key: test-json-variable (numeric)") + logger.info(f"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(f"Variable Key: test-json-variable (booleans)") + logger.info(f"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(f"Variable Key: test-json-variable (with None)") + logger.info(f"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(f"Variable Key: doesnt-exist") + logger.info(f"Value: {nonexistent_details.value}") + logger.info(f"Reason: {nonexistent_details.reason}") + + logger.info("\n" + "=" * 60) + logger.info("All tests completed!") + logger.info("=" * 60) devcycle_client.close() From af1098677b2549e5f236eba46a81eaef3c6fbb7e Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Wed, 29 Oct 2025 10:22:27 -0400 Subject: [PATCH 07/10] lint --- example/openfeature_example.py | 80 ++++++++++++++++------------------ 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/example/openfeature_example.py b/example/openfeature_example.py index 9668188..a639b22 100644 --- a/example/openfeature_example.py +++ b/example/openfeature_example.py @@ -13,13 +13,13 @@ 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") @@ -28,9 +28,11 @@ def main(): 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'") + 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 @@ -69,7 +71,7 @@ def main(): 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 @@ -81,11 +83,11 @@ def main(): logger.info("✓ Boolean variable is ENABLED") else: logger.info("✗ Boolean variable is DISABLED") - + logger.info("\n" + "=" * 60) logger.info("Testing String Variable") logger.info("=" * 60) - + # Test String Variable string_details = open_feature_client.get_string_details( "test-string-variable", "default string", context @@ -93,11 +95,11 @@ def main(): logger.info(f"Variable Key: test-string-variable") logger.info(f"Value: {string_details.value}") logger.info(f"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 @@ -105,11 +107,11 @@ def main(): logger.info(f"Variable Key: test-number-variable") logger.info(f"Value: {integer_details.value}") logger.info(f"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( @@ -117,11 +119,11 @@ def main(): ) logger.info(f"Variable Key: test-number-variable (as float)") logger.info(f"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 @@ -129,22 +131,22 @@ def main(): logger.info(f"Variable Key: test-json-variable") logger.info(f"Value: {json_details.value}") logger.info(f"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(f"Variable Key: test-json-variable (with empty default)") logger.info(f"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 = { @@ -152,83 +154,77 @@ def main(): "int_key": 42, "float_key": 3.14, "bool_key": True, - "none_key": None + "none_key": None, } mixed_result = open_feature_client.get_object_value( "test-json-variable", mixed_default, context ) logger.info(f"Variable Key: test-json-variable (with mixed types)") logger.info(f"Value: {mixed_result}") - logger.info(f"Value types: {[(k, type(v).__name__) for k, v in mixed_result.items()]}") - + 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": "john@example.com", - "status": "active" + "status": "active", } string_dict_result = open_feature_client.get_object_value( "test-json-variable", string_dict_default, context ) logger.info(f"Variable Key: test-json-variable (all strings)") logger.info(f"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_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(f"Variable Key: test-json-variable (numeric)") logger.info(f"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_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(f"Variable Key: test-json-variable (booleans)") logger.info(f"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 + "nullable_count": None, } none_dict_result = open_feature_client.get_object_value( "test-json-variable", none_dict_default, context ) logger.info(f"Variable Key: test-json-variable (with None)") logger.info(f"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 @@ -236,7 +232,7 @@ def main(): logger.info(f"Variable Key: doesnt-exist") logger.info(f"Value: {nonexistent_details.value}") logger.info(f"Reason: {nonexistent_details.reason}") - + logger.info("\n" + "=" * 60) logger.info("All tests completed!") logger.info("=" * 60) From d72dfc98f376190279c445538b5198a5903cc2d1 Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Wed, 29 Oct 2025 10:23:45 -0400 Subject: [PATCH 08/10] lint --- example/openfeature_example.py | 58 +++++++++++++++++----------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/example/openfeature_example.py b/example/openfeature_example.py index a639b22..47f1ab3 100644 --- a/example/openfeature_example.py +++ b/example/openfeature_example.py @@ -76,9 +76,9 @@ def main(): boolean_details = open_feature_client.get_boolean_details( "test-boolean-variable", False, context ) - logger.info(f"Variable Key: test-boolean-variable") - logger.info(f"Value: {boolean_details.value}") - logger.info(f"Reason: {boolean_details.reason}") + 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: @@ -92,9 +92,9 @@ def main(): string_details = open_feature_client.get_string_details( "test-string-variable", "default string", context ) - logger.info(f"Variable Key: test-string-variable") - logger.info(f"Value: {string_details.value}") - logger.info(f"Reason: {string_details.reason}") + 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)") @@ -104,9 +104,9 @@ def main(): integer_details = open_feature_client.get_integer_details( "test-number-variable", 0, context ) - logger.info(f"Variable Key: test-number-variable") - logger.info(f"Value: {integer_details.value}") - logger.info(f"Reason: {integer_details.reason}") + 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)") @@ -117,8 +117,8 @@ def main(): float_value = open_feature_client.get_float_value( "test-number-variable", 0.0, context ) - logger.info(f"Variable Key: test-number-variable (as float)") - logger.info(f"Value: {float_value}") + logger.info("Variable Key: test-number-variable (as float)") + logger.info("Value: {float_value}") logger.info("\n" + "=" * 60) logger.info("Testing JSON Object Variable") @@ -128,9 +128,9 @@ def main(): json_details = open_feature_client.get_object_details( "test-json-variable", {"default": "value"}, context ) - logger.info(f"Variable Key: test-json-variable") - logger.info(f"Value: {json_details.value}") - logger.info(f"Reason: {json_details.reason}") + 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") @@ -140,8 +140,8 @@ def main(): empty_dict_result = open_feature_client.get_object_value( "test-json-variable", {}, context ) - logger.info(f"Variable Key: test-json-variable (with empty default)") - logger.info(f"Value: {empty_dict_result}") + 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") @@ -159,8 +159,8 @@ def main(): mixed_result = open_feature_client.get_object_value( "test-json-variable", mixed_default, context ) - logger.info(f"Variable Key: test-json-variable (with mixed types)") - logger.info(f"Value: {mixed_result}") + 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()]}" ) @@ -178,8 +178,8 @@ def main(): string_dict_result = open_feature_client.get_object_value( "test-json-variable", string_dict_default, context ) - logger.info(f"Variable Key: test-json-variable (all strings)") - logger.info(f"Value: {string_dict_result}") + 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") @@ -190,8 +190,8 @@ def main(): numeric_dict_result = open_feature_client.get_object_value( "test-json-variable", numeric_dict_default, context ) - logger.info(f"Variable Key: test-json-variable (numeric)") - logger.info(f"Value: {numeric_dict_result}") + 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") @@ -202,8 +202,8 @@ def main(): bool_dict_result = open_feature_client.get_object_value( "test-json-variable", bool_dict_default, context ) - logger.info(f"Variable Key: test-json-variable (booleans)") - logger.info(f"Value: {bool_dict_result}") + 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") @@ -218,8 +218,8 @@ def main(): none_dict_result = open_feature_client.get_object_value( "test-json-variable", none_dict_default, context ) - logger.info(f"Variable Key: test-json-variable (with None)") - logger.info(f"Value: {none_dict_result}") + 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)") @@ -229,9 +229,9 @@ def main(): nonexistent_details = open_feature_client.get_string_details( "doesnt-exist", "default fallback value", context ) - logger.info(f"Variable Key: doesnt-exist") - logger.info(f"Value: {nonexistent_details.value}") - logger.info(f"Reason: {nonexistent_details.reason}") + logger.info("Variable Key: doesnt-exist") + logger.info("Value: {nonexistent_details.value}") + logger.info("Reason: {nonexistent_details.reason}") logger.info("\n" + "=" * 60) logger.info("All tests completed!") From 35307fd9510fbbcdb880003226e5793841db83a2 Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Wed, 29 Oct 2025 10:27:17 -0400 Subject: [PATCH 09/10] lint --- example/openfeature_example.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/example/openfeature_example.py b/example/openfeature_example.py index 47f1ab3..21d4bb0 100644 --- a/example/openfeature_example.py +++ b/example/openfeature_example.py @@ -89,7 +89,7 @@ def main(): logger.info("=" * 60) # Test String Variable - string_details = open_feature_client.get_string_details( + open_feature_client.get_string_details( "test-string-variable", "default string", context ) logger.info("Variable Key: test-string-variable") @@ -101,7 +101,7 @@ def main(): logger.info("=" * 60) # Test Number Variable (Integer) - integer_details = open_feature_client.get_integer_details( + open_feature_client.get_integer_details( "test-number-variable", 0, context ) logger.info("Variable Key: test-number-variable") @@ -114,7 +114,7 @@ def main(): # 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( + open_feature_client.get_float_value( "test-number-variable", 0.0, context ) logger.info("Variable Key: test-number-variable (as float)") @@ -125,7 +125,7 @@ def main(): logger.info("=" * 60) # Test JSON Object Variable - json_details = open_feature_client.get_object_details( + open_feature_client.get_object_details( "test-json-variable", {"default": "value"}, context ) logger.info("Variable Key: test-json-variable") @@ -137,7 +137,7 @@ def main(): logger.info("=" * 60) # Test with empty dictionary default (valid per OpenFeature spec) - empty_dict_result = open_feature_client.get_object_value( + open_feature_client.get_object_value( "test-json-variable", {}, context ) logger.info("Variable Key: test-json-variable (with empty default)") @@ -175,7 +175,7 @@ def main(): "email": "john@example.com", "status": "active", } - string_dict_result = open_feature_client.get_object_value( + open_feature_client.get_object_value( "test-json-variable", string_dict_default, context ) logger.info("Variable Key: test-json-variable (all strings)") @@ -187,7 +187,7 @@ def main(): # 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( + open_feature_client.get_object_value( "test-json-variable", numeric_dict_default, context ) logger.info("Variable Key: test-json-variable (numeric)") @@ -199,7 +199,7 @@ def main(): # 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( + open_feature_client.get_object_value( "test-json-variable", bool_dict_default, context ) logger.info("Variable Key: test-json-variable (booleans)") @@ -215,7 +215,7 @@ def main(): "required_field": "value", "nullable_count": None, } - none_dict_result = open_feature_client.get_object_value( + open_feature_client.get_object_value( "test-json-variable", none_dict_default, context ) logger.info("Variable Key: test-json-variable (with None)") @@ -226,7 +226,7 @@ def main(): logger.info("=" * 60) # Test non-existent variable to demonstrate default handling - nonexistent_details = open_feature_client.get_string_details( + open_feature_client.get_string_details( "doesnt-exist", "default fallback value", context ) logger.info("Variable Key: doesnt-exist") From 2028f918dd7775d8ab2f6a1552262805031d4f8d Mon Sep 17 00:00:00 2001 From: Parth Suthar Date: Wed, 29 Oct 2025 10:40:20 -0400 Subject: [PATCH 10/10] lint --- example/openfeature_example.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/example/openfeature_example.py b/example/openfeature_example.py index 21d4bb0..0cc2b2f 100644 --- a/example/openfeature_example.py +++ b/example/openfeature_example.py @@ -101,9 +101,7 @@ def main(): logger.info("=" * 60) # Test Number Variable (Integer) - open_feature_client.get_integer_details( - "test-number-variable", 0, context - ) + 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}") @@ -114,9 +112,7 @@ def main(): # 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 - ) + 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}") @@ -137,9 +133,7 @@ def main(): logger.info("=" * 60) # Test with empty dictionary default (valid per OpenFeature spec) - open_feature_client.get_object_value( - "test-json-variable", {}, context - ) + 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}")