posthoganalytics 6.6.1__py3-none-any.whl → 6.7.0__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.
@@ -8,7 +8,7 @@ except ImportError:
8
8
 
9
9
  import time
10
10
  import uuid
11
- from typing import Any, Dict, Optional, cast
11
+ from typing import Any, Dict, Optional
12
12
 
13
13
  from posthoganalytics.ai.utils import (
14
14
  call_llm_and_track_usage,
@@ -1,6 +1,6 @@
1
1
  import time
2
2
  import uuid
3
- from typing import Any, Dict, List, Optional, cast
3
+ from typing import Any, Dict, List, Optional
4
4
 
5
5
  try:
6
6
  import openai
@@ -329,7 +329,7 @@ class Client(object):
329
329
  only these flags will be evaluated, improving performance.
330
330
 
331
331
  Category:
332
- Feature Flags
332
+ Feature flags
333
333
  """
334
334
  resp_data = self.get_flags_decision(
335
335
  distinct_id,
@@ -368,7 +368,7 @@ class Client(object):
368
368
  ```
369
369
 
370
370
  Category:
371
- Feature Flags
371
+ Feature flags
372
372
  """
373
373
  resp_data = self.get_flags_decision(
374
374
  distinct_id,
@@ -407,7 +407,7 @@ class Client(object):
407
407
  ```
408
408
 
409
409
  Category:
410
- Feature Flags
410
+ Feature flags
411
411
  """
412
412
  resp = self.get_flags_decision(
413
413
  distinct_id,
@@ -446,7 +446,7 @@ class Client(object):
446
446
  ```
447
447
 
448
448
  Category:
449
- Feature Flags
449
+ Feature flags
450
450
  """
451
451
  groups = groups or {}
452
452
  person_properties = person_properties or {}
@@ -1169,7 +1169,7 @@ class Client(object):
1169
1169
  ```
1170
1170
 
1171
1171
  Category:
1172
- Feature Flags
1172
+ Feature flags
1173
1173
  """
1174
1174
  if not self.personal_api_key:
1175
1175
  self.log.warning(
@@ -1291,7 +1291,7 @@ class Client(object):
1291
1291
  ```
1292
1292
 
1293
1293
  Category:
1294
- Feature Flags
1294
+ Feature flags
1295
1295
  """
1296
1296
  response = self.get_feature_flag(
1297
1297
  key,
@@ -1499,7 +1499,7 @@ class Client(object):
1499
1499
  ```
1500
1500
 
1501
1501
  Category:
1502
- Feature Flags
1502
+ Feature flags
1503
1503
  """
1504
1504
  feature_flag_result = self.get_feature_flag_result(
1505
1505
  key,
@@ -1589,7 +1589,7 @@ class Client(object):
1589
1589
  ```
1590
1590
 
1591
1591
  Category:
1592
- Feature Flags
1592
+ Feature flags
1593
1593
  """
1594
1594
  feature_flag_result = self._get_feature_flag_result(
1595
1595
  key,
@@ -1759,7 +1759,7 @@ class Client(object):
1759
1759
  ```
1760
1760
 
1761
1761
  Category:
1762
- Feature Flags
1762
+ Feature flags
1763
1763
  """
1764
1764
  response = self.get_all_flags_and_payloads(
1765
1765
  distinct_id,
@@ -1803,7 +1803,7 @@ class Client(object):
1803
1803
  ```
1804
1804
 
1805
1805
  Category:
1806
- Feature Flags
1806
+ Feature flags
1807
1807
  """
1808
1808
  if self.disabled:
1809
1809
  return {"featureFlags": None, "featureFlagPayloads": None}
@@ -139,9 +139,70 @@ def evaluate_flag_dependency(
139
139
  # Definitive False result - dependency failed
140
140
  return False
141
141
 
142
+ # All dependencies in the chain have been evaluated successfully
143
+ # Now check if the final flag value matches the expected value in the property
144
+ flag_key = property.get("key")
145
+ expected_value = property.get("value")
146
+ operator = property.get("operator", "exact")
147
+
148
+ if flag_key and expected_value is not None:
149
+ # Get the actual value of the flag we're checking
150
+ actual_value = evaluation_cache.get(flag_key)
151
+
152
+ if actual_value is None:
153
+ # Flag wasn't evaluated - this shouldn't happen if dependency chain is correct
154
+ raise InconclusiveMatchError(
155
+ f"Flag '{flag_key}' was not evaluated despite being in dependency chain"
156
+ )
157
+
158
+ # For flag dependencies, we need to compare the actual flag result with expected value
159
+ # using the flag_evaluates_to operator logic
160
+ if operator == "flag_evaluates_to":
161
+ return matches_dependency_value(expected_value, actual_value)
162
+ else:
163
+ # This should never happen, but just to be defensive.
164
+ raise InconclusiveMatchError(
165
+ f"Flag dependency property for '{property.get('key', 'unknown')}' has invalid operator '{operator}'"
166
+ )
167
+
168
+ # If no value check needed, return True (all dependencies passed)
142
169
  return True
143
170
 
144
171
 
172
+ def matches_dependency_value(expected_value, actual_value):
173
+ """
174
+ Check if the actual flag value matches the expected dependency value.
175
+
176
+ This follows the same logic as the C# MatchesDependencyValue function:
177
+ - String variant case: check for exact match or boolean true
178
+ - Boolean case: must match expected boolean value
179
+
180
+ Args:
181
+ expected_value: The expected value from the property
182
+ actual_value: The actual value returned by the flag evaluation
183
+
184
+ Returns:
185
+ bool: True if the values match according to flag dependency rules
186
+ """
187
+ # String variant case - check for exact match or boolean true
188
+ if isinstance(actual_value, str) and len(actual_value) > 0:
189
+ if isinstance(expected_value, bool):
190
+ # Any variant matches boolean true
191
+ return expected_value
192
+ elif isinstance(expected_value, str):
193
+ # variants are case-sensitive, hence our comparison is too
194
+ return actual_value == expected_value
195
+ else:
196
+ return False
197
+
198
+ # Boolean case - must match expected boolean value
199
+ elif isinstance(actual_value, bool) and isinstance(expected_value, bool):
200
+ return actual_value == expected_value
201
+
202
+ # Default case
203
+ return False
204
+
205
+
145
206
  def match_feature_flag_properties(
146
207
  flag,
147
208
  distinct_id,
@@ -1376,7 +1376,7 @@ class TestLocalEvaluation(unittest.TestCase):
1376
1376
  "properties": [
1377
1377
  {
1378
1378
  "key": "beta-feature",
1379
- "operator": "exact",
1379
+ "operator": "flag_evaluates_to",
1380
1380
  "value": True,
1381
1381
  "type": "flag",
1382
1382
  "dependency_chain": ["beta-feature"],
@@ -1456,7 +1456,7 @@ class TestLocalEvaluation(unittest.TestCase):
1456
1456
  "properties": [
1457
1457
  {
1458
1458
  "key": "flag-a",
1459
- "operator": "exact",
1459
+ "operator": "flag_evaluates_to",
1460
1460
  "value": True,
1461
1461
  "type": "flag",
1462
1462
  "dependency_chain": ["flag-a"],
@@ -1504,7 +1504,7 @@ class TestLocalEvaluation(unittest.TestCase):
1504
1504
  "properties": [
1505
1505
  {
1506
1506
  "key": "flag-b",
1507
- "operator": "exact",
1507
+ "operator": "flag_evaluates_to",
1508
1508
  "value": True,
1509
1509
  "type": "flag",
1510
1510
  "dependency_chain": [], # Empty chain indicates circular dependency
@@ -1526,7 +1526,7 @@ class TestLocalEvaluation(unittest.TestCase):
1526
1526
  "properties": [
1527
1527
  {
1528
1528
  "key": "flag-a",
1529
- "operator": "exact",
1529
+ "operator": "flag_evaluates_to",
1530
1530
  "value": True,
1531
1531
  "type": "flag",
1532
1532
  "dependency_chain": [], # Empty chain indicates circular dependency
@@ -1566,7 +1566,7 @@ class TestLocalEvaluation(unittest.TestCase):
1566
1566
  "properties": [
1567
1567
  {
1568
1568
  "key": "non-existent-flag",
1569
- "operator": "exact",
1569
+ "operator": "flag_evaluates_to",
1570
1570
  "value": True,
1571
1571
  "type": "flag",
1572
1572
  "dependency_chain": ["non-existent-flag"],
@@ -1629,14 +1629,14 @@ class TestLocalEvaluation(unittest.TestCase):
1629
1629
  "properties": [
1630
1630
  {
1631
1631
  "key": "flag-a",
1632
- "operator": "exact",
1632
+ "operator": "flag_evaluates_to",
1633
1633
  "value": True,
1634
1634
  "type": "flag",
1635
1635
  "dependency_chain": ["flag-a"],
1636
1636
  },
1637
1637
  {
1638
1638
  "key": "flag-b",
1639
- "operator": "exact",
1639
+ "operator": "flag_evaluates_to",
1640
1640
  "value": True,
1641
1641
  "type": "flag",
1642
1642
  "dependency_chain": ["flag-b"],
@@ -1658,7 +1658,7 @@ class TestLocalEvaluation(unittest.TestCase):
1658
1658
  "properties": [
1659
1659
  {
1660
1660
  "key": "flag-c",
1661
- "operator": "exact",
1661
+ "operator": "flag_evaluates_to",
1662
1662
  "value": True,
1663
1663
  "type": "flag",
1664
1664
  "dependency_chain": ["flag-a", "flag-b", "flag-c"],
@@ -1711,7 +1711,7 @@ class TestLocalEvaluation(unittest.TestCase):
1711
1711
  "properties": [
1712
1712
  {
1713
1713
  "key": "base-flag",
1714
- "operator": "exact",
1714
+ "operator": "flag_evaluates_to",
1715
1715
  "value": True,
1716
1716
  "type": "flag",
1717
1717
  "dependency_chain": ["base-flag"],
@@ -1788,10 +1788,10 @@ class TestLocalEvaluation(unittest.TestCase):
1788
1788
  "properties": [
1789
1789
  {
1790
1790
  "key": "base-flag",
1791
- "operator": "exact",
1791
+ "operator": "flag_evaluates_to",
1792
1792
  "value": True,
1793
1793
  "type": "flag",
1794
- # No dependency_chain property - should handle gracefully
1794
+ # No dependency_chain property - should evaluate as inconclusive
1795
1795
  }
1796
1796
  ],
1797
1797
  "rollout_percentage": 100,
@@ -1815,7 +1815,7 @@ class TestLocalEvaluation(unittest.TestCase):
1815
1815
 
1816
1816
  property_with_flag_dep = {
1817
1817
  "key": "some-flag",
1818
- "operator": "exact",
1818
+ "operator": "flag_evaluates_to",
1819
1819
  "value": True,
1820
1820
  "type": "flag",
1821
1821
  "dependency_chain": ["some-flag"],
@@ -1835,6 +1835,516 @@ class TestLocalEvaluation(unittest.TestCase):
1835
1835
  self.assertIn("Cannot evaluate flag dependency", str(cm.exception))
1836
1836
  self.assertIn("some-flag", str(cm.exception))
1837
1837
 
1838
+ @mock.patch("posthog.client.flags")
1839
+ @mock.patch("posthog.client.get")
1840
+ def test_multi_level_multivariate_dependency_chain(self, patch_get, patch_flags):
1841
+ """Test multi-level multivariate dependency chain: dependent-flag -> intermediate-flag -> leaf-flag"""
1842
+ client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
1843
+ client.feature_flags = [
1844
+ # Leaf flag: multivariate with "control" and "test" variants using person property overrides
1845
+ {
1846
+ "id": 1,
1847
+ "name": "Leaf Flag",
1848
+ "key": "leaf-flag",
1849
+ "active": True,
1850
+ "rollout_percentage": 100,
1851
+ "filters": {
1852
+ "groups": [
1853
+ {
1854
+ "properties": [
1855
+ {
1856
+ "key": "email",
1857
+ "type": "person",
1858
+ "value": "control@example.com",
1859
+ "operator": "exact",
1860
+ }
1861
+ ],
1862
+ "rollout_percentage": 100,
1863
+ "variant": "control",
1864
+ },
1865
+ {
1866
+ "properties": [
1867
+ {
1868
+ "key": "email",
1869
+ "type": "person",
1870
+ "value": "test@example.com",
1871
+ "operator": "exact",
1872
+ }
1873
+ ],
1874
+ "rollout_percentage": 100,
1875
+ "variant": "test",
1876
+ },
1877
+ {
1878
+ "rollout_percentage": 50,
1879
+ "variant": "control",
1880
+ }, # Default fallback
1881
+ ],
1882
+ "multivariate": {
1883
+ "variants": [
1884
+ {
1885
+ "key": "control",
1886
+ "name": "Control",
1887
+ "rollout_percentage": 50,
1888
+ },
1889
+ {"key": "test", "name": "Test", "rollout_percentage": 50},
1890
+ ]
1891
+ },
1892
+ },
1893
+ },
1894
+ # Intermediate flag: multivariate with "blue" and "green" variants, depends on leaf-flag="control"
1895
+ {
1896
+ "id": 2,
1897
+ "name": "Intermediate Flag",
1898
+ "key": "intermediate-flag",
1899
+ "active": True,
1900
+ "rollout_percentage": 100,
1901
+ "filters": {
1902
+ "groups": [
1903
+ {
1904
+ "properties": [
1905
+ {
1906
+ "key": "leaf-flag",
1907
+ "operator": "flag_evaluates_to",
1908
+ "value": "control",
1909
+ "type": "flag",
1910
+ "dependency_chain": ["leaf-flag"],
1911
+ },
1912
+ {
1913
+ "key": "variant_type",
1914
+ "type": "person",
1915
+ "value": "blue",
1916
+ "operator": "exact",
1917
+ },
1918
+ ],
1919
+ "rollout_percentage": 100,
1920
+ "variant": "blue",
1921
+ },
1922
+ {
1923
+ "properties": [
1924
+ {
1925
+ "key": "leaf-flag",
1926
+ "operator": "flag_evaluates_to",
1927
+ "value": "control",
1928
+ "type": "flag",
1929
+ "dependency_chain": ["leaf-flag"],
1930
+ },
1931
+ {
1932
+ "key": "variant_type",
1933
+ "type": "person",
1934
+ "value": "green",
1935
+ "operator": "exact",
1936
+ },
1937
+ ],
1938
+ "rollout_percentage": 100,
1939
+ "variant": "green",
1940
+ },
1941
+ ],
1942
+ "multivariate": {
1943
+ "variants": [
1944
+ {"key": "blue", "name": "Blue", "rollout_percentage": 50},
1945
+ {"key": "green", "name": "Green", "rollout_percentage": 50},
1946
+ ]
1947
+ },
1948
+ },
1949
+ },
1950
+ # Dependent flag: boolean flag that depends on intermediate-flag="blue"
1951
+ {
1952
+ "id": 3,
1953
+ "name": "Dependent Flag",
1954
+ "key": "dependent-flag",
1955
+ "active": True,
1956
+ "rollout_percentage": 100,
1957
+ "filters": {
1958
+ "groups": [
1959
+ {
1960
+ "properties": [
1961
+ {
1962
+ "key": "intermediate-flag",
1963
+ "operator": "flag_evaluates_to",
1964
+ "value": "blue",
1965
+ "type": "flag",
1966
+ "dependency_chain": [
1967
+ "leaf-flag",
1968
+ "intermediate-flag",
1969
+ ],
1970
+ }
1971
+ ],
1972
+ "rollout_percentage": 100,
1973
+ }
1974
+ ],
1975
+ },
1976
+ },
1977
+ ]
1978
+
1979
+ # Test using person properties and variant overrides to ensure predictable variants
1980
+
1981
+ # Test 1: Make sure the leaf flag evaluates to the variant we expect using email overrides
1982
+ self.assertEqual(
1983
+ "control",
1984
+ client.get_feature_flag(
1985
+ "leaf-flag",
1986
+ "any-user",
1987
+ person_properties={"email": "control@example.com"},
1988
+ ),
1989
+ )
1990
+ self.assertEqual(
1991
+ "test",
1992
+ client.get_feature_flag(
1993
+ "leaf-flag",
1994
+ "any-user",
1995
+ person_properties={"email": "test@example.com"},
1996
+ ),
1997
+ )
1998
+
1999
+ # Test 2: Make sure the intermediate flag evaluates to the expected variants when dependency is satisfied
2000
+ self.assertEqual(
2001
+ "blue",
2002
+ client.get_feature_flag(
2003
+ "intermediate-flag",
2004
+ "any-user",
2005
+ person_properties={
2006
+ "email": "control@example.com",
2007
+ "variant_type": "blue",
2008
+ },
2009
+ ),
2010
+ )
2011
+
2012
+ self.assertEqual(
2013
+ "green",
2014
+ client.get_feature_flag(
2015
+ "intermediate-flag",
2016
+ "any-user",
2017
+ person_properties={
2018
+ "email": "control@example.com",
2019
+ "variant_type": "green",
2020
+ },
2021
+ ),
2022
+ )
2023
+
2024
+ # Test 3: Make sure the intermediate flag evaluates to false when leaf dependency fails
2025
+ self.assertEqual(
2026
+ False,
2027
+ client.get_feature_flag(
2028
+ "intermediate-flag",
2029
+ "any-user",
2030
+ person_properties={
2031
+ "email": "test@example.com", # This makes leaf-flag="test", breaking dependency
2032
+ "variant_type": "blue",
2033
+ },
2034
+ ),
2035
+ )
2036
+
2037
+ # Test 4: When leaf-flag="control", intermediate="blue", dependent should be true
2038
+ self.assertEqual(
2039
+ True,
2040
+ client.get_feature_flag(
2041
+ "dependent-flag",
2042
+ "any-user",
2043
+ person_properties={
2044
+ "email": "control@example.com",
2045
+ "variant_type": "blue",
2046
+ },
2047
+ ),
2048
+ )
2049
+
2050
+ # Test 5: When leaf-flag="control", intermediate="green", dependent should be false
2051
+ self.assertEqual(
2052
+ False,
2053
+ client.get_feature_flag(
2054
+ "dependent-flag",
2055
+ "any-user",
2056
+ person_properties={
2057
+ "email": "control@example.com",
2058
+ "variant_type": "green",
2059
+ },
2060
+ ),
2061
+ )
2062
+
2063
+ # Test 6: When leaf-flag="test", intermediate is False, dependent should be false
2064
+ self.assertEqual(
2065
+ False,
2066
+ client.get_feature_flag(
2067
+ "dependent-flag",
2068
+ "any-user",
2069
+ person_properties={"email": "test@example.com", "variant_type": "blue"},
2070
+ ),
2071
+ )
2072
+
2073
+ def test_matches_dependency_value(self):
2074
+ """Test the matches_dependency_value function logic"""
2075
+ from posthoganalytics.feature_flags import matches_dependency_value
2076
+
2077
+ # String variant matches string exactly (case-sensitive)
2078
+ self.assertTrue(matches_dependency_value("control", "control"))
2079
+ self.assertTrue(matches_dependency_value("Control", "Control"))
2080
+ self.assertFalse(matches_dependency_value("control", "Control"))
2081
+ self.assertFalse(matches_dependency_value("Control", "CONTROL"))
2082
+ self.assertFalse(matches_dependency_value("control", "test"))
2083
+
2084
+ # String variant matches boolean true (any variant is truthy)
2085
+ self.assertTrue(matches_dependency_value(True, "control"))
2086
+ self.assertTrue(matches_dependency_value(True, "test"))
2087
+ self.assertFalse(matches_dependency_value(False, "control"))
2088
+
2089
+ # Boolean matches boolean exactly
2090
+ self.assertTrue(matches_dependency_value(True, True))
2091
+ self.assertTrue(matches_dependency_value(False, False))
2092
+ self.assertFalse(matches_dependency_value(False, True))
2093
+ self.assertFalse(matches_dependency_value(True, False))
2094
+
2095
+ # Empty string doesn't match
2096
+ self.assertFalse(matches_dependency_value(True, ""))
2097
+ self.assertFalse(matches_dependency_value("control", ""))
2098
+
2099
+ # Type mismatches
2100
+ self.assertFalse(matches_dependency_value(123, "control"))
2101
+ self.assertFalse(matches_dependency_value("control", True))
2102
+
2103
+ @mock.patch("posthog.client.flags")
2104
+ @mock.patch("posthog.client.get")
2105
+ def test_production_style_multivariate_dependency_chain(
2106
+ self, patch_get, patch_flags
2107
+ ):
2108
+ """Test production-style multivariate dependency chain: multivariate-root-flag -> multivariate-intermediate-flag -> multivariate-leaf-flag"""
2109
+ client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
2110
+ client.feature_flags = [
2111
+ # Leaf flag: multivariate with fruit variants
2112
+ {
2113
+ "id": 451,
2114
+ "name": "Multivariate Leaf Flag (Base)",
2115
+ "key": "multivariate-leaf-flag",
2116
+ "active": True,
2117
+ "rollout_percentage": 100,
2118
+ "filters": {
2119
+ "groups": [
2120
+ {
2121
+ "properties": [
2122
+ {
2123
+ "key": "email",
2124
+ "type": "person",
2125
+ "value": ["pineapple@example.com"],
2126
+ "operator": "exact",
2127
+ }
2128
+ ],
2129
+ "rollout_percentage": 100,
2130
+ "variant": "pineapple",
2131
+ },
2132
+ {
2133
+ "properties": [
2134
+ {
2135
+ "key": "email",
2136
+ "type": "person",
2137
+ "value": ["mango@example.com"],
2138
+ "operator": "exact",
2139
+ }
2140
+ ],
2141
+ "rollout_percentage": 100,
2142
+ "variant": "mango",
2143
+ },
2144
+ {
2145
+ "properties": [
2146
+ {
2147
+ "key": "email",
2148
+ "type": "person",
2149
+ "value": ["papaya@example.com"],
2150
+ "operator": "exact",
2151
+ }
2152
+ ],
2153
+ "rollout_percentage": 100,
2154
+ "variant": "papaya",
2155
+ },
2156
+ {
2157
+ "properties": [
2158
+ {
2159
+ "key": "email",
2160
+ "type": "person",
2161
+ "value": ["kiwi@example.com"],
2162
+ "operator": "exact",
2163
+ }
2164
+ ],
2165
+ "rollout_percentage": 100,
2166
+ "variant": "kiwi",
2167
+ },
2168
+ {
2169
+ "properties": [],
2170
+ "rollout_percentage": 0, # Force default to false for unknown emails
2171
+ },
2172
+ ],
2173
+ "multivariate": {
2174
+ "variants": [
2175
+ {"key": "pineapple", "rollout_percentage": 25},
2176
+ {"key": "mango", "rollout_percentage": 25},
2177
+ {"key": "papaya", "rollout_percentage": 25},
2178
+ {"key": "kiwi", "rollout_percentage": 25},
2179
+ ]
2180
+ },
2181
+ },
2182
+ },
2183
+ # Intermediate flag: multivariate with color variants, depends on fruit
2184
+ {
2185
+ "id": 467,
2186
+ "name": "Multivariate Intermediate Flag (Depends on fruit)",
2187
+ "key": "multivariate-intermediate-flag",
2188
+ "active": True,
2189
+ "rollout_percentage": 100,
2190
+ "filters": {
2191
+ "groups": [
2192
+ {
2193
+ "properties": [
2194
+ {
2195
+ "key": "multivariate-leaf-flag",
2196
+ "type": "flag",
2197
+ "value": "pineapple",
2198
+ "operator": "flag_evaluates_to",
2199
+ "dependency_chain": ["multivariate-leaf-flag"],
2200
+ }
2201
+ ],
2202
+ "rollout_percentage": 100,
2203
+ "variant": "blue",
2204
+ },
2205
+ {
2206
+ "properties": [
2207
+ {
2208
+ "key": "multivariate-leaf-flag",
2209
+ "type": "flag",
2210
+ "value": "mango",
2211
+ "operator": "flag_evaluates_to",
2212
+ "dependency_chain": ["multivariate-leaf-flag"],
2213
+ }
2214
+ ],
2215
+ "rollout_percentage": 100,
2216
+ "variant": "red",
2217
+ },
2218
+ ],
2219
+ "multivariate": {
2220
+ "variants": [
2221
+ {"key": "blue", "rollout_percentage": 100},
2222
+ {"key": "red", "rollout_percentage": 0},
2223
+ {"key": "green", "rollout_percentage": 0},
2224
+ {"key": "black", "rollout_percentage": 0},
2225
+ ]
2226
+ },
2227
+ },
2228
+ },
2229
+ # Root flag: multivariate with show variants, depends on color
2230
+ {
2231
+ "id": 468,
2232
+ "name": "Multivariate Root Flag (Depends on color)",
2233
+ "key": "multivariate-root-flag",
2234
+ "active": True,
2235
+ "rollout_percentage": 100,
2236
+ "filters": {
2237
+ "groups": [
2238
+ {
2239
+ "properties": [
2240
+ {
2241
+ "key": "multivariate-intermediate-flag",
2242
+ "type": "flag",
2243
+ "value": "blue",
2244
+ "operator": "flag_evaluates_to",
2245
+ "dependency_chain": [
2246
+ "multivariate-leaf-flag",
2247
+ "multivariate-intermediate-flag",
2248
+ ],
2249
+ }
2250
+ ],
2251
+ "rollout_percentage": 100,
2252
+ "variant": "breaking-bad",
2253
+ },
2254
+ {
2255
+ "properties": [
2256
+ {
2257
+ "key": "multivariate-intermediate-flag",
2258
+ "type": "flag",
2259
+ "value": "red",
2260
+ "operator": "flag_evaluates_to",
2261
+ "dependency_chain": [
2262
+ "multivariate-leaf-flag",
2263
+ "multivariate-intermediate-flag",
2264
+ ],
2265
+ }
2266
+ ],
2267
+ "rollout_percentage": 100,
2268
+ "variant": "the-wire",
2269
+ },
2270
+ ],
2271
+ "multivariate": {
2272
+ "variants": [
2273
+ {"key": "breaking-bad", "rollout_percentage": 100},
2274
+ {"key": "the-wire", "rollout_percentage": 0},
2275
+ {"key": "game-of-thrones", "rollout_percentage": 0},
2276
+ {"key": "the-expanse", "rollout_percentage": 0},
2277
+ ]
2278
+ },
2279
+ },
2280
+ },
2281
+ ]
2282
+
2283
+ # Test successful pineapple -> blue -> breaking-bad chain
2284
+ leaf_result = client.get_feature_flag(
2285
+ "multivariate-leaf-flag",
2286
+ "test-user",
2287
+ person_properties={"email": "pineapple@example.com"},
2288
+ )
2289
+ intermediate_result = client.get_feature_flag(
2290
+ "multivariate-intermediate-flag",
2291
+ "test-user",
2292
+ person_properties={"email": "pineapple@example.com"},
2293
+ )
2294
+ root_result = client.get_feature_flag(
2295
+ "multivariate-root-flag",
2296
+ "test-user",
2297
+ person_properties={"email": "pineapple@example.com"},
2298
+ )
2299
+
2300
+ self.assertEqual(leaf_result, "pineapple")
2301
+ self.assertEqual(intermediate_result, "blue")
2302
+ self.assertEqual(root_result, "breaking-bad")
2303
+
2304
+ # Test successful mango -> red -> the-wire chain
2305
+ mango_leaf_result = client.get_feature_flag(
2306
+ "multivariate-leaf-flag",
2307
+ "test-user",
2308
+ person_properties={"email": "mango@example.com"},
2309
+ )
2310
+ mango_intermediate_result = client.get_feature_flag(
2311
+ "multivariate-intermediate-flag",
2312
+ "test-user",
2313
+ person_properties={"email": "mango@example.com"},
2314
+ )
2315
+ mango_root_result = client.get_feature_flag(
2316
+ "multivariate-root-flag",
2317
+ "test-user",
2318
+ person_properties={"email": "mango@example.com"},
2319
+ )
2320
+
2321
+ self.assertEqual(mango_leaf_result, "mango")
2322
+ self.assertEqual(mango_intermediate_result, "red")
2323
+ self.assertEqual(mango_root_result, "the-wire")
2324
+
2325
+ # Test broken chain - user without matching email gets default/false results
2326
+ unknown_leaf_result = client.get_feature_flag(
2327
+ "multivariate-leaf-flag",
2328
+ "test-user",
2329
+ person_properties={"email": "unknown@example.com"},
2330
+ )
2331
+ unknown_intermediate_result = client.get_feature_flag(
2332
+ "multivariate-intermediate-flag",
2333
+ "test-user",
2334
+ person_properties={"email": "unknown@example.com"},
2335
+ )
2336
+ unknown_root_result = client.get_feature_flag(
2337
+ "multivariate-root-flag",
2338
+ "test-user",
2339
+ person_properties={"email": "unknown@example.com"},
2340
+ )
2341
+
2342
+ self.assertEqual(
2343
+ unknown_leaf_result, False
2344
+ ) # No matching email -> null variant -> false
2345
+ self.assertEqual(unknown_intermediate_result, False) # Dependency not satisfied
2346
+ self.assertEqual(unknown_root_result, False) # Chain broken
2347
+
1838
2348
  @mock.patch("posthog.client.Poller")
1839
2349
  @mock.patch("posthog.client.get")
1840
2350
  def test_load_feature_flags(self, patch_get, patch_poll):
@@ -1,4 +1,4 @@
1
- VERSION = "6.6.1"
1
+ VERSION = "6.7.0"
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.6.1
3
+ Version: 6.7.0
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -1,21 +1,21 @@
1
1
  posthoganalytics/__init__.py,sha256=66HkeJ1fkzbKC2ggl3F164oajFeiGm8v84kJR0Yf5BI,25987
2
2
  posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
3
- posthoganalytics/client.py,sha256=UXIUoe0gRp1SYFKiy9vLKIajBwY2KyB9Etv5-0ZlEr8,71654
3
+ posthoganalytics/client.py,sha256=1K38fDKzm9BVGoEEVj01rno9HjhsL-73uFiErOgUom8,71654
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=lWeBhjHYfk338ZN6Ib-yT7-PscLy7kqZIGFyhLdTIYY,19325
8
+ posthoganalytics/feature_flags.py,sha256=uI7nDXOu1aPDwVaF_SJGDwhQ9YzJxotTiyafALpO6Rs,21883
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=LB6qjMDtZV5cJSTWNfGLOSZbTVne-YozmEDDYmRfjJY,87
14
+ posthoganalytics/version.py,sha256=Yc8jr2XmXvCz8R0gBQdAIMWigUaL3wTCTkpS9VJLxWw,87
15
15
  posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  posthoganalytics/ai/utils.py,sha256=92RlL395wjL5V9FstS8BeebwMtaz6DP6zS9miCNla9M,21106
17
17
  posthoganalytics/ai/anthropic/__init__.py,sha256=fFhDOiRzTXzGQlgnrRDL-4yKC8EYIl8NW4a2QNR6xRU,368
18
- posthoganalytics/ai/anthropic/anthropic.py,sha256=P8o-pZ2rbJXDiHO73OWjO7OgboGiEm_wVY4pbvHnUEs,7397
18
+ posthoganalytics/ai/anthropic/anthropic.py,sha256=y2vKegtXVgxBCH1bmkLOxZ-KQ4CXWO4eDh34XWbrk_I,7391
19
19
  posthoganalytics/ai/anthropic/anthropic_async.py,sha256=iAwVlAY6VeW0dGZdMkdfniBTBFUdZZrDMZi-O9vdiuo,7511
20
20
  posthoganalytics/ai/anthropic/anthropic_providers.py,sha256=y1_qc8Lbip-YDmpimPGg3DfTm5g-WZk5FrRCXzwF_Ow,2139
21
21
  posthoganalytics/ai/gemini/__init__.py,sha256=bMNBnJ6NO_PCQCwmxKIiw4adFuEQ06hFFBALt-aDW-0,174
@@ -24,7 +24,7 @@ posthoganalytics/ai/langchain/__init__.py,sha256=9CqAwLynTGj3ASAR80C3PmdTdrYGmu9
24
24
  posthoganalytics/ai/langchain/callbacks.py,sha256=imnhz4u5uPsqUCqtRymdD-PWNzSuWHiace7g_iHyqTU,29423
25
25
  posthoganalytics/ai/openai/__init__.py,sha256=_flZxkyaDZme9hxJsY31sMlq4nP1dtc75HmNgj-21Kg,197
26
26
  posthoganalytics/ai/openai/openai.py,sha256=Ii0kU8S8hBSRxjTRcnAJAVGGmMDYFy03wtM75MrCtlA,22176
27
- posthoganalytics/ai/openai/openai_async.py,sha256=F0VYOGVvLYr7TevXENy9vULqiknsCErKbaprswR3aSc,22583
27
+ posthoganalytics/ai/openai/openai_async.py,sha256=mzCsNA_oeyrWgtCXO-uk6UFHNsqLCCN5DotVwzljq3c,22577
28
28
  posthoganalytics/ai/openai/openai_providers.py,sha256=RPVmj2V0_lAdno_ax5Ul2kwhBA9_rRgAdl_sCqrQc6M,4004
29
29
  posthoganalytics/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  posthoganalytics/integrations/django.py,sha256=KYtBr7CkiZQynRc2TCWWYHe-J3ie8iSUa42WPshYZdc,6795
@@ -36,14 +36,14 @@ posthoganalytics/test/test_contexts.py,sha256=c--hNUIEf6SHQ7H9vdPhU1oLCN0SnD4wDb
36
36
  posthoganalytics/test/test_exception_capture.py,sha256=al37Kg6wjzL_IBCFUUXRvkP6nVrqS6IZRCOKSo29Nh8,1063
37
37
  posthoganalytics/test/test_feature_flag.py,sha256=9RQwB5eCvVAGrrO7UnR3Z1OidP_YoL4iBl3A83fuAig,6824
38
38
  posthoganalytics/test/test_feature_flag_result.py,sha256=z2OgD97r85LKMqCnoCqAs74WjUMucayAtC3qWaITGCA,15898
39
- posthoganalytics/test/test_feature_flags.py,sha256=gFILn07WykFtJE_QpkJ_c9u_JeoGnegixxs6BffCR0s,191526
39
+ posthoganalytics/test/test_feature_flags.py,sha256=GH03a8eTFuSCUI-Sfwgo8J1HY5HvrlDC7RmKF-Qtkkg,213060
40
40
  posthoganalytics/test/test_module.py,sha256=viqaAWA_uHt8r20fHIeME6IQkeXmQ8ZyrJTtPGQAb1E,1070
41
41
  posthoganalytics/test/test_request.py,sha256=Zc0VbkjpVmj8mKokQm9rzdgTr0b1U44vvMYSkB_IQLs,4467
42
42
  posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
43
43
  posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
44
44
  posthoganalytics/test/test_utils.py,sha256=sqUTbfweVcxxFRd3WDMFXqPMyU6DvzOBeAOc68Py9aw,9620
45
- posthoganalytics-6.6.1.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
46
- posthoganalytics-6.6.1.dist-info/METADATA,sha256=80BPZRBZWY8hPhIi62ql0HZ7l1gajIBUNYxvdnwxYic,6024
47
- posthoganalytics-6.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
- posthoganalytics-6.6.1.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
49
- posthoganalytics-6.6.1.dist-info/RECORD,,
45
+ posthoganalytics-6.7.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
46
+ posthoganalytics-6.7.0.dist-info/METADATA,sha256=9jN-73KOEWNUEf4T8zFaTixyouws5cNOX_rhNEIOEY0,6024
47
+ posthoganalytics-6.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ posthoganalytics-6.7.0.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
49
+ posthoganalytics-6.7.0.dist-info/RECORD,,