From 821bfd15a7a23f63c3cdce0476487fde2d2e4c53 Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Fri, 14 Mar 2025 11:20:36 -0500 Subject: [PATCH 1/4] Make contexts with ConfigValue as the value work --- prefab_cloud_python/config_value_wrapper.py | 5 +++-- tests/test_context.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/prefab_cloud_python/config_value_wrapper.py b/prefab_cloud_python/config_value_wrapper.py index 22d0524..9d6d756 100644 --- a/prefab_cloud_python/config_value_wrapper.py +++ b/prefab_cloud_python/config_value_wrapper.py @@ -7,8 +7,9 @@ class ConfigValueWrapper: @staticmethod def wrap(value, confidential=None): value_type = type(value) - - if value_type == int: + if value_type == Prefab.ConfigValue: + return value + elif value_type == int: return Prefab.ConfigValue(int=value, confidential=confidential) elif value_type == float: return Prefab.ConfigValue(double=value, confidential=confidential) diff --git a/tests/test_context.py b/tests/test_context.py index 8577400..7b510da 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -169,6 +169,23 @@ def test_named_context_to_proto(self): ) assert proto_context == expected_proto_context + def test_named_context_to_proto_with_config_values(self): + proto_context = NamedContext( + "the-name", + {"a": ConfigValue(int=10), "b": ConfigValue(double=1.1), "c": "hello world", "d": ["hello", "world"], "e": True}, + ).to_proto() + expected_proto_context = ProtoContext( + type="the-name", + values={ + "a": ConfigValue(int=10), + "b": ConfigValue(double=1.1), + "c": ConfigValue(string="hello world"), + "d": ConfigValue(string_list=StringList(values=["hello", "world"])), + "e": ConfigValue(bool=True), + }, + ) + assert proto_context == expected_proto_context + def test_context_to_proto(self): proto_context_set = Context( { From 7d5de03ec0b1053a70662493a6ea8311cda8d055 Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Fri, 14 Mar 2025 11:36:18 -0500 Subject: [PATCH 2/4] Declare a new type for the Union of ContextDict or Context . export in init --- prefab_cloud_python/__init__.py | 1 + prefab_cloud_python/client.py | 8 ++++---- prefab_cloud_python/constants.py | 12 +++++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/prefab_cloud_python/__init__.py b/prefab_cloud_python/__init__.py index a81bd6c..eebcec2 100644 --- a/prefab_cloud_python/__init__.py +++ b/prefab_cloud_python/__init__.py @@ -30,6 +30,7 @@ from .context import Context, NamedContext from .feature_flag_client import FeatureFlagClient from .config_client import ConfigClient +from .constants import ConfigValueType, ContextDictType, ContextDictOrContext, NoDefaultProvided # Re-export Protocol Buffer types for easier access import prefab_pb2 diff --git a/prefab_cloud_python/client.py b/prefab_cloud_python/client.py index 8356ff9..fbfa8d0 100644 --- a/prefab_cloud_python/client.py +++ b/prefab_cloud_python/client.py @@ -22,7 +22,7 @@ import uuid import requests from urllib.parse import urljoin -from .constants import NoDefaultProvided, ConfigValueType, ContextDictType +from .constants import NoDefaultProvided, ConfigValueType, ContextDictType, ContextDictOrContext from ._internal_constants import LOG_LEVEL_BASE_KEY PostBodyType = Union[Prefab.Loggers, Prefab.ContextShapes, Prefab.TelemetryEvents] @@ -78,7 +78,7 @@ def get( self, key: str, default: ConfigValueType = NoDefaultProvided, - context: Optional[ContextDictType | Context] = None, + context: Optional[ContextDictOrContext] = None, ) -> ConfigValueType: if self.is_ff(key): return self.feature_flag_client().get(key, default=default, context=context) @@ -86,7 +86,7 @@ def get( return self.config_client().get(key, default=default, context=context) def enabled( - self, feature_name: str, context: Optional[ContextDictType | Context] = None + self, feature_name: str, context: Optional[ContextDictOrContext] = None ) -> bool: return self.feature_flag_client().feature_is_on_for( feature_name, context=context @@ -163,7 +163,7 @@ def is_ready(self) -> bool: return self.config_client().is_ready() def set_global_context( - self, global_context: Optional[ContextDictType | Context] = None + self, global_context: Optional[ContextDictOrContext] = None ) -> Client: self.global_context = Context.normalize_context_arg(global_context) return self diff --git a/prefab_cloud_python/constants.py b/prefab_cloud_python/constants.py index 598da28..1b4f567 100644 --- a/prefab_cloud_python/constants.py +++ b/prefab_cloud_python/constants.py @@ -1,6 +1,12 @@ -from typing import Optional, Union -from datetime import timedelta +from typing import Optional, Union, TYPE_CHECKING +from datetime import timedelta, date, datetime + +import prefab_pb2 as Prefab + +if TYPE_CHECKING: + from .context import Context NoDefaultProvided = object() ConfigValueType = Optional[Union[int, float, bool, str, list[str], timedelta, dict]] -ContextDictType = dict[str, dict[str, ConfigValueType]] +ContextDictType = dict[str, dict[str, Union[int, float, bool, str, date, datetime, list[str], Prefab.ConfigValue]]] +ContextDictOrContext = Union[ContextDictType, "Context"] From c4fb9aaa30af9e669b8302fc0c38e2c4e91dc68e Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Fri, 14 Mar 2025 11:42:03 -0500 Subject: [PATCH 3/4] Smaller logging in telemetry --- prefab_cloud_python/_telemetry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/prefab_cloud_python/_telemetry.py b/prefab_cloud_python/_telemetry.py index fb721bc..3fea9b0 100644 --- a/prefab_cloud_python/_telemetry.py +++ b/prefab_cloud_python/_telemetry.py @@ -304,8 +304,9 @@ def run(self): try: super().run() except Exception as e: - # ignore exception so thread keeps running - logger.exception(f"Exception in thread {self.name}: {e}") + # Log just the exception name and message without the full traceback + logger.warning(f"Exception in thread {self.name}: {e.__class__.__name__}: {e}") + # Using warning level instead of error+traceback to keep logs cleaner def __init__( self, From cf96eba08b6dd6639743789757d34d83a4d84786 Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Fri, 14 Mar 2025 11:45:38 -0500 Subject: [PATCH 4/4] styles --- prefab_cloud_python/__init__.py | 7 ++++++- prefab_cloud_python/_telemetry.py | 4 +++- prefab_cloud_python/client.py | 6 +++++- prefab_cloud_python/config_value_wrapper.py | 2 +- prefab_cloud_python/constants.py | 7 ++++++- tests/test_context.py | 8 +++++++- 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/prefab_cloud_python/__init__.py b/prefab_cloud_python/__init__.py index eebcec2..ecfdea6 100644 --- a/prefab_cloud_python/__init__.py +++ b/prefab_cloud_python/__init__.py @@ -30,7 +30,12 @@ from .context import Context, NamedContext from .feature_flag_client import FeatureFlagClient from .config_client import ConfigClient -from .constants import ConfigValueType, ContextDictType, ContextDictOrContext, NoDefaultProvided +from .constants import ( + ConfigValueType, + ContextDictType, + ContextDictOrContext, + NoDefaultProvided, +) # Re-export Protocol Buffer types for easier access import prefab_pb2 diff --git a/prefab_cloud_python/_telemetry.py b/prefab_cloud_python/_telemetry.py index 3fea9b0..fba2836 100644 --- a/prefab_cloud_python/_telemetry.py +++ b/prefab_cloud_python/_telemetry.py @@ -305,7 +305,9 @@ def run(self): super().run() except Exception as e: # Log just the exception name and message without the full traceback - logger.warning(f"Exception in thread {self.name}: {e.__class__.__name__}: {e}") + logger.warning( + f"Exception in thread {self.name}: {e.__class__.__name__}: {e}" + ) # Using warning level instead of error+traceback to keep logs cleaner def __init__( diff --git a/prefab_cloud_python/client.py b/prefab_cloud_python/client.py index fbfa8d0..9d6dfee 100644 --- a/prefab_cloud_python/client.py +++ b/prefab_cloud_python/client.py @@ -22,7 +22,11 @@ import uuid import requests from urllib.parse import urljoin -from .constants import NoDefaultProvided, ConfigValueType, ContextDictType, ContextDictOrContext +from .constants import ( + NoDefaultProvided, + ConfigValueType, + ContextDictOrContext, +) from ._internal_constants import LOG_LEVEL_BASE_KEY PostBodyType = Union[Prefab.Loggers, Prefab.ContextShapes, Prefab.TelemetryEvents] diff --git a/prefab_cloud_python/config_value_wrapper.py b/prefab_cloud_python/config_value_wrapper.py index 9d6d756..5205d8d 100644 --- a/prefab_cloud_python/config_value_wrapper.py +++ b/prefab_cloud_python/config_value_wrapper.py @@ -7,7 +7,7 @@ class ConfigValueWrapper: @staticmethod def wrap(value, confidential=None): value_type = type(value) - if value_type == Prefab.ConfigValue: + if value_type == Prefab.ConfigValue: return value elif value_type == int: return Prefab.ConfigValue(int=value, confidential=confidential) diff --git a/prefab_cloud_python/constants.py b/prefab_cloud_python/constants.py index 1b4f567..16ee334 100644 --- a/prefab_cloud_python/constants.py +++ b/prefab_cloud_python/constants.py @@ -8,5 +8,10 @@ NoDefaultProvided = object() ConfigValueType = Optional[Union[int, float, bool, str, list[str], timedelta, dict]] -ContextDictType = dict[str, dict[str, Union[int, float, bool, str, date, datetime, list[str], Prefab.ConfigValue]]] +ContextDictType = dict[ + str, + dict[ + str, Union[int, float, bool, str, date, datetime, list[str], Prefab.ConfigValue] + ], +] ContextDictOrContext = Union[ContextDictType, "Context"] diff --git a/tests/test_context.py b/tests/test_context.py index 7b510da..39b58e5 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -172,7 +172,13 @@ def test_named_context_to_proto(self): def test_named_context_to_proto_with_config_values(self): proto_context = NamedContext( "the-name", - {"a": ConfigValue(int=10), "b": ConfigValue(double=1.1), "c": "hello world", "d": ["hello", "world"], "e": True}, + { + "a": ConfigValue(int=10), + "b": ConfigValue(double=1.1), + "c": "hello world", + "d": ["hello", "world"], + "e": True, + }, ).to_proto() expected_proto_context = ProtoContext( type="the-name",