posthoganalytics 6.7.7__py3-none-any.whl → 6.7.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@ from posthoganalytics.contexts import (
11
11
  set_context_session as inner_set_context_session,
12
12
  identify_context as inner_identify_context,
13
13
  )
14
+ from posthoganalytics.feature_flags import InconclusiveMatchError, RequiresServerEvaluation
14
15
  from posthoganalytics.types import FeatureFlag, FlagsAndPayloads, FeatureFlagResult
15
16
  from posthoganalytics.version import VERSION
16
17
 
@@ -128,7 +128,7 @@ class WrappedResponses:
128
128
  start_time = time.time()
129
129
  usage_stats: TokenUsage = TokenUsage()
130
130
  final_content = []
131
- response = self._original.create(**kwargs)
131
+ response = await self._original.create(**kwargs)
132
132
 
133
133
  async def async_generator():
134
134
  nonlocal usage_stats
@@ -345,7 +345,7 @@ class WrappedCompletions:
345
345
  if "stream_options" not in kwargs:
346
346
  kwargs["stream_options"] = {}
347
347
  kwargs["stream_options"]["include_usage"] = True
348
- response = self._original.create(**kwargs)
348
+ response = await self._original.create(**kwargs)
349
349
 
350
350
  async def async_generator():
351
351
  nonlocal usage_stats
@@ -499,7 +499,7 @@ class WrappedEmbeddings:
499
499
  posthog_trace_id = str(uuid.uuid4())
500
500
 
501
501
  start_time = time.time()
502
- response = self._original.create(**kwargs)
502
+ response = await self._original.create(**kwargs)
503
503
  end_time = time.time()
504
504
 
505
505
  # Extract usage statistics if available
@@ -20,7 +20,11 @@ from posthoganalytics.exception_utils import (
20
20
  exception_is_already_captured,
21
21
  mark_exception_as_captured,
22
22
  )
23
- from posthoganalytics.feature_flags import InconclusiveMatchError, match_feature_flag_properties
23
+ from posthoganalytics.feature_flags import (
24
+ InconclusiveMatchError,
25
+ RequiresServerEvaluation,
26
+ match_feature_flag_properties,
27
+ )
24
28
  from posthoganalytics.poller import Poller
25
29
  from posthoganalytics.request import (
26
30
  DEFAULT_HOST,
@@ -1583,7 +1587,7 @@ class Client(object):
1583
1587
  self.log.debug(
1584
1588
  f"Successfully computed flag locally: {key} -> {response}"
1585
1589
  )
1586
- except InconclusiveMatchError as e:
1590
+ except (RequiresServerEvaluation, InconclusiveMatchError) as e:
1587
1591
  self.log.debug(f"Failed to compute flag {key} locally: {e}")
1588
1592
  except Exception as e:
1589
1593
  self.log.exception(
@@ -22,6 +22,18 @@ class InconclusiveMatchError(Exception):
22
22
  pass
23
23
 
24
24
 
25
+ class RequiresServerEvaluation(Exception):
26
+ """
27
+ Raised when feature flag evaluation requires server-side data that is not
28
+ available locally (e.g., static cohorts, experience continuity).
29
+
30
+ This error should propagate immediately to trigger API fallback, unlike
31
+ InconclusiveMatchError which allows trying other conditions.
32
+ """
33
+
34
+ pass
35
+
36
+
25
37
  # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
26
38
  # Given the same distinct_id and key, it'll always return the same float. These floats are
27
39
  # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
@@ -239,7 +251,12 @@ def match_feature_flag_properties(
239
251
  else:
240
252
  variant = get_matching_variant(flag, distinct_id)
241
253
  return variant or True
254
+ except RequiresServerEvaluation:
255
+ # Static cohort or other missing server-side data - must fallback to API
256
+ raise
242
257
  except InconclusiveMatchError:
258
+ # Evaluation error (bad regex, invalid date, missing property, etc.)
259
+ # Track that we had an inconclusive match, but try other conditions
243
260
  is_inconclusive = True
244
261
 
245
262
  if is_inconclusive:
@@ -449,8 +466,8 @@ def match_cohort(
449
466
  # }
450
467
  cohort_id = str(property.get("value"))
451
468
  if cohort_id not in cohort_properties:
452
- raise InconclusiveMatchError(
453
- "can't match cohort without a given cohort property value"
469
+ raise RequiresServerEvaluation(
470
+ f"cohort {cohort_id} not found in local cohorts - likely a static cohort that requires server evaluation"
454
471
  )
455
472
 
456
473
  property_group = cohort_properties[cohort_id]
@@ -503,6 +520,9 @@ def match_property_group(
503
520
  # OR group
504
521
  if matches:
505
522
  return True
523
+ except RequiresServerEvaluation:
524
+ # Immediately propagate - this condition requires server-side data
525
+ raise
506
526
  except InconclusiveMatchError as e:
507
527
  log.debug(f"Failed to compute property {prop} locally: {e}")
508
528
  error_matching_locally = True
@@ -552,6 +572,9 @@ def match_property_group(
552
572
  return True
553
573
  if not matches and negation:
554
574
  return True
575
+ except RequiresServerEvaluation:
576
+ # Immediately propagate - this condition requires server-side data
577
+ raise
555
578
  except InconclusiveMatchError as e:
556
579
  log.debug(f"Failed to compute property {prop} locally: {e}")
557
580
  error_matching_locally = True
@@ -3013,6 +3013,75 @@ class TestLocalEvaluation(unittest.TestCase):
3013
3013
  )
3014
3014
  self.assertEqual(patch_flags.call_count, 0)
3015
3015
 
3016
+ @mock.patch("posthog.client.flags")
3017
+ @mock.patch("posthog.client.get")
3018
+ def test_fallback_to_api_when_flag_has_static_cohort_in_multi_condition(
3019
+ self, patch_get, patch_flags
3020
+ ):
3021
+ """
3022
+ When a flag has multiple conditions and one contains a static cohort,
3023
+ the SDK should fallback to API for the entire flag, not just skip that
3024
+ condition and evaluate the next one locally.
3025
+
3026
+ This prevents returning wrong variants when later conditions could match
3027
+ locally but the user is actually in the static cohort.
3028
+ """
3029
+ client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
3030
+
3031
+ # Mock the local flags response - cohort 999 is NOT in cohorts map (static cohort)
3032
+ client.feature_flags = [
3033
+ {
3034
+ "id": 1,
3035
+ "key": "multi-condition-flag",
3036
+ "active": True,
3037
+ "filters": {
3038
+ "groups": [
3039
+ {
3040
+ "properties": [
3041
+ {"key": "id", "value": 999, "type": "cohort"}
3042
+ ],
3043
+ "rollout_percentage": 100,
3044
+ "variant": "set-1",
3045
+ },
3046
+ {
3047
+ "properties": [
3048
+ {
3049
+ "key": "$geoip_country_code",
3050
+ "operator": "exact",
3051
+ "value": ["DE"],
3052
+ "type": "person",
3053
+ }
3054
+ ],
3055
+ "rollout_percentage": 100,
3056
+ "variant": "set-8",
3057
+ },
3058
+ ],
3059
+ "multivariate": {
3060
+ "variants": [
3061
+ {"key": "set-1", "rollout_percentage": 50},
3062
+ {"key": "set-8", "rollout_percentage": 50},
3063
+ ]
3064
+ },
3065
+ },
3066
+ }
3067
+ ]
3068
+ client.cohorts = {} # Note: cohort 999 is NOT here - it's a static cohort
3069
+
3070
+ # Mock the API response - user is in the static cohort
3071
+ patch_flags.return_value = {"featureFlags": {"multi-condition-flag": "set-1"}}
3072
+
3073
+ result = client.get_feature_flag(
3074
+ "multi-condition-flag",
3075
+ "test-distinct-id",
3076
+ person_properties={"$geoip_country_code": "DE"},
3077
+ )
3078
+
3079
+ # Should return the API result (set-1), not local evaluation (set-8)
3080
+ self.assertEqual(result, "set-1")
3081
+
3082
+ # Verify API was called (fallback occurred)
3083
+ self.assertEqual(patch_flags.call_count, 1)
3084
+
3016
3085
 
3017
3086
  class TestMatchProperties(unittest.TestCase):
3018
3087
  def property(self, key, value, operator=None):
@@ -4006,6 +4075,60 @@ class TestCaptureCalls(unittest.TestCase):
4006
4075
 
4007
4076
  patch_capture.reset_mock()
4008
4077
 
4078
+ @mock.patch("posthog.client.flags")
4079
+ def test_fallback_to_api_in_get_feature_flag_payload_when_flag_has_static_cohort(
4080
+ self, patch_flags
4081
+ ):
4082
+ """
4083
+ Test that get_feature_flag_payload falls back to API when evaluating
4084
+ a flag with static cohorts, similar to get_feature_flag behavior.
4085
+ """
4086
+ client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
4087
+
4088
+ # Mock the local flags response - cohort 999 is NOT in cohorts map (static cohort)
4089
+ client.feature_flags = [
4090
+ {
4091
+ "id": 1,
4092
+ "name": "Multi-condition Flag",
4093
+ "key": "multi-condition-flag",
4094
+ "active": True,
4095
+ "filters": {
4096
+ "groups": [
4097
+ {
4098
+ "properties": [
4099
+ {"key": "id", "value": 999, "type": "cohort"}
4100
+ ],
4101
+ "rollout_percentage": 100,
4102
+ "variant": "variant-1",
4103
+ }
4104
+ ],
4105
+ "multivariate": {
4106
+ "variants": [{"key": "variant-1", "rollout_percentage": 100}]
4107
+ },
4108
+ "payloads": {"variant-1": '{"message": "local-payload"}'},
4109
+ },
4110
+ }
4111
+ ]
4112
+ client.cohorts = {} # Note: cohort 999 is NOT here - it's a static cohort
4113
+
4114
+ # Mock the API response - user is in the static cohort
4115
+ patch_flags.return_value = {
4116
+ "featureFlags": {"multi-condition-flag": "variant-1"},
4117
+ "featureFlagPayloads": {"multi-condition-flag": '{"message": "from-api"}'},
4118
+ }
4119
+
4120
+ # Call get_feature_flag_payload without match_value to trigger evaluation
4121
+ result = client.get_feature_flag_payload(
4122
+ "multi-condition-flag",
4123
+ "test-distinct-id",
4124
+ )
4125
+
4126
+ # Should return the API payload, not local payload
4127
+ self.assertEqual(result, {"message": "from-api"})
4128
+
4129
+ # Verify API was called (fallback occurred)
4130
+ self.assertEqual(patch_flags.call_count, 1)
4131
+
4009
4132
  @mock.patch.object(Client, "capture")
4010
4133
  @mock.patch("posthog.client.flags")
4011
4134
  def test_disable_geoip_get_flag_capture_call(self, patch_flags, patch_capture):
@@ -1,4 +1,4 @@
1
- VERSION = "6.7.7"
1
+ VERSION = "6.7.9"
2
2
 
3
3
  if __name__ == "__main__":
4
4
  print(VERSION, end="") # noqa: T201
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthoganalytics
3
- Version: 6.7.7
3
+ Version: 6.7.9
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -1,17 +1,17 @@
1
- posthoganalytics/__init__.py,sha256=66HkeJ1fkzbKC2ggl3F164oajFeiGm8v84kJR0Yf5BI,25987
1
+ posthoganalytics/__init__.py,sha256=vYBBQuWxyCdN2mkFuJgHqGGh0ZcO7WriFy7tEILtpSI,26079
2
2
  posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
3
- posthoganalytics/client.py,sha256=zbBzmDYcHsMvXnqumjwxpU_X0l5Qt8LpCFdVm3NdpKc,72688
3
+ posthoganalytics/client.py,sha256=43gscUKk6GFqBXRrlodxR9ZnTuNlChAPxYe3UW7wqps,72759
4
4
  posthoganalytics/consumer.py,sha256=CiNbJBdyW9jER3ZYCKbX-JFmEDXlE1lbDy1MSl43-a0,4617
5
5
  posthoganalytics/contexts.py,sha256=LFSFIYpUFWKTBnGMjV9n1aYHWbAzz5zLJGr2qG34PoE,9405
6
6
  posthoganalytics/exception_capture.py,sha256=1VHBfffrXXrkK0PT8iVgKPpj_R1pGAzG5f3Qw0WF79w,1783
7
7
  posthoganalytics/exception_utils.py,sha256=P_75873Y2jayqlLiIkbxCNE7Bc8cM6J9kfrdZ5ZSnA0,26696
8
- posthoganalytics/feature_flags.py,sha256=e2DOE0LOj6yk3ySf5oGkdNe5wXLoGtxvsFemFo8SK7A,21530
8
+ posthoganalytics/feature_flags.py,sha256=yHjiH6LSvhQgurbsPCHUdGakZKvkzOLdqB8vL3iyhmw,22544
9
9
  posthoganalytics/poller.py,sha256=jBz5rfH_kn_bBz7wCB46Fpvso4ttx4uzqIZWvXBCFmQ,595
10
10
  posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  posthoganalytics/request.py,sha256=Bsl2c5WwONKPQzwWMmKPX5VgOlwSiIcSNfhXgoz62Y8,6186
12
12
  posthoganalytics/types.py,sha256=Dl3aFGX9XUR0wMmK12r2s5Hjan9jL4HpQ9GHpVcEq5U,10207
13
13
  posthoganalytics/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
14
- posthoganalytics/version.py,sha256=gqdJquZGn8LXrtKf1DWRhAbLaaQ0R5vPBb1hWtoQANw,87
14
+ posthoganalytics/version.py,sha256=DV1G6YcxO_uwUg4WtwP5ybEnwJ_IQ_GSfj5EZRGd8fo,87
15
15
  posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  posthoganalytics/ai/sanitization.py,sha256=owipZ4eJYtd4JTI-CM_klatclXaeaIec3XJBOUfsOnQ,5770
17
17
  posthoganalytics/ai/types.py,sha256=ceubs4K9xf8vQx7wokq1NL9hPtxyS7D7sUOuT7Lx1lM,3237
@@ -28,7 +28,7 @@ posthoganalytics/ai/langchain/__init__.py,sha256=9CqAwLynTGj3ASAR80C3PmdTdrYGmu9
28
28
  posthoganalytics/ai/langchain/callbacks.py,sha256=Otha0a6YLBwETfKjDDbdLzNi-RHRgKFJB69GwWCv9lg,29527
29
29
  posthoganalytics/ai/openai/__init__.py,sha256=u4OuUT7k1NgFj0TrxjuyegOg7a_UA8nAU6a-Hszr0OM,490
30
30
  posthoganalytics/ai/openai/openai.py,sha256=I05NruE9grWezM_EgOZBiG5Ej_gABsDcYKN0pRQWvzU,20235
31
- posthoganalytics/ai/openai/openai_async.py,sha256=k6bo3LfJ_CAPBZCxAzyM2uLz4BpW2YWEFhNuzVcpJlM,21811
31
+ posthoganalytics/ai/openai/openai_async.py,sha256=YAaj8Q-X3bExx-BXLWUOtdTMdj3RKe8bUkTyjamNURo,21829
32
32
  posthoganalytics/ai/openai/openai_converter.py,sha256=VBaAGdXPSVNgfvCnSAojslWkTRO2luUxpjafR-WMEbs,20469
33
33
  posthoganalytics/ai/openai/openai_providers.py,sha256=RPVmj2V0_lAdno_ax5Ul2kwhBA9_rRgAdl_sCqrQc6M,4004
34
34
  posthoganalytics/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -41,14 +41,14 @@ posthoganalytics/test/test_contexts.py,sha256=c--hNUIEf6SHQ7H9vdPhU1oLCN0SnD4wDb
41
41
  posthoganalytics/test/test_exception_capture.py,sha256=al37Kg6wjzL_IBCFUUXRvkP6nVrqS6IZRCOKSo29Nh8,1063
42
42
  posthoganalytics/test/test_feature_flag.py,sha256=9RQwB5eCvVAGrrO7UnR3Z1OidP_YoL4iBl3A83fuAig,6824
43
43
  posthoganalytics/test/test_feature_flag_result.py,sha256=z2OgD97r85LKMqCnoCqAs74WjUMucayAtC3qWaITGCA,15898
44
- posthoganalytics/test/test_feature_flags.py,sha256=H7DmRSs0ggl7E6DzBGy1aXfCQwf2nDQ0I_xh-kQyXQ0,212423
44
+ posthoganalytics/test/test_feature_flags.py,sha256=b0CcW2JyBRhOxlWX9KOBncqL7OG_VHFX3Z4J6VOlPNs,217352
45
45
  posthoganalytics/test/test_module.py,sha256=M772XKYO30XluqBTumZFFnYGqVxDmKKly4eUjhLIjZU,822
46
46
  posthoganalytics/test/test_request.py,sha256=Zc0VbkjpVmj8mKokQm9rzdgTr0b1U44vvMYSkB_IQLs,4467
47
47
  posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
48
48
  posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
49
49
  posthoganalytics/test/test_utils.py,sha256=sqUTbfweVcxxFRd3WDMFXqPMyU6DvzOBeAOc68Py9aw,9620
50
- posthoganalytics-6.7.7.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
51
- posthoganalytics-6.7.7.dist-info/METADATA,sha256=HOVbTs7PQJ0BsqHHTfnhJ4GYseCkqz6QZCPDComgJeU,6024
52
- posthoganalytics-6.7.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- posthoganalytics-6.7.7.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
54
- posthoganalytics-6.7.7.dist-info/RECORD,,
50
+ posthoganalytics-6.7.9.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
51
+ posthoganalytics-6.7.9.dist-info/METADATA,sha256=-DlivZXLGC6A9HWbaruqrTzLOjI6d4w28TI2GjPfzTE,6024
52
+ posthoganalytics-6.7.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ posthoganalytics-6.7.9.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
54
+ posthoganalytics-6.7.9.dist-info/RECORD,,