posthoganalytics 6.4.1__tar.gz → 6.6.0__tar.gz

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.
Files changed (57) hide show
  1. {posthoganalytics-6.4.1/posthoganalytics.egg-info → posthoganalytics-6.6.0}/PKG-INFO +1 -1
  2. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/client.py +73 -14
  3. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_client.py +69 -0
  4. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_feature_flags.py +118 -7
  5. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/types.py +3 -0
  6. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/version.py +1 -1
  7. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0/posthoganalytics.egg-info}/PKG-INFO +1 -1
  8. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/LICENSE +0 -0
  9. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/MANIFEST.in +0 -0
  10. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/README.md +0 -0
  11. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/__init__.py +0 -0
  12. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/__init__.py +0 -0
  13. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/anthropic/__init__.py +0 -0
  14. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/anthropic/anthropic.py +0 -0
  15. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
  16. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
  17. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/gemini/__init__.py +0 -0
  18. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/gemini/gemini.py +0 -0
  19. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/langchain/__init__.py +0 -0
  20. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/langchain/callbacks.py +0 -0
  21. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/openai/__init__.py +0 -0
  22. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/openai/openai.py +0 -0
  23. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/openai/openai_async.py +0 -0
  24. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/openai/openai_providers.py +0 -0
  25. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/ai/utils.py +0 -0
  26. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/args.py +0 -0
  27. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/consumer.py +0 -0
  28. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/contexts.py +0 -0
  29. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/exception_capture.py +0 -0
  30. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/exception_utils.py +0 -0
  31. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/feature_flags.py +0 -0
  32. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/integrations/__init__.py +0 -0
  33. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/integrations/django.py +0 -0
  34. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/poller.py +0 -0
  35. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/py.typed +0 -0
  36. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/request.py +0 -0
  37. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/__init__.py +0 -0
  38. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_before_send.py +0 -0
  39. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_consumer.py +0 -0
  40. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_contexts.py +0 -0
  41. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_exception_capture.py +0 -0
  42. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_feature_flag.py +0 -0
  43. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_feature_flag_result.py +0 -0
  44. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_module.py +0 -0
  45. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_request.py +0 -0
  46. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_size_limited_dict.py +0 -0
  47. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_types.py +0 -0
  48. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/test/test_utils.py +0 -0
  49. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics/utils.py +0 -0
  50. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics.egg-info/SOURCES.txt +0 -0
  51. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics.egg-info/dependency_links.txt +0 -0
  52. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics.egg-info/requires.txt +0 -0
  53. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/posthoganalytics.egg-info/top_level.txt +0 -0
  54. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/pyproject.toml +0 -0
  55. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/setup.cfg +0 -0
  56. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/setup.py +0 -0
  57. {posthoganalytics-6.4.1 → posthoganalytics-6.6.0}/setup_analytics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthoganalytics
3
- Version: 6.4.1
3
+ Version: 6.6.0
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -44,6 +44,7 @@ from posthoganalytics.types import (
44
44
  FlagsAndPayloads,
45
45
  FlagsResponse,
46
46
  FlagValue,
47
+ SendFeatureFlagsOptions,
47
48
  normalize_flags_response,
48
49
  to_flags_and_payloads,
49
50
  to_payloads,
@@ -87,6 +88,7 @@ def add_context_tags(properties):
87
88
  current_context = _get_current_context()
88
89
  if current_context:
89
90
  context_tags = current_context.collect_tags()
91
+ properties["$context_tags"] = set(context_tags.keys())
90
92
  # We want explicitly passed properties to override context tags
91
93
  context_tags.update(properties)
92
94
  properties = context_tags
@@ -312,6 +314,7 @@ class Client(object):
312
314
  person_properties=None,
313
315
  group_properties=None,
314
316
  disable_geoip=None,
317
+ flag_keys_to_evaluate: Optional[list[str]] = None,
315
318
  ) -> dict[str, Union[bool, str]]:
316
319
  """
317
320
  Get feature flag variants for a user by calling decide.
@@ -322,12 +325,19 @@ class Client(object):
322
325
  person_properties: A dictionary of person properties.
323
326
  group_properties: A dictionary of group properties.
324
327
  disable_geoip: Whether to disable GeoIP for this request.
328
+ flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
329
+ only these flags will be evaluated, improving performance.
325
330
 
326
331
  Category:
327
332
  Feature Flags
328
333
  """
329
334
  resp_data = self.get_flags_decision(
330
- distinct_id, groups, person_properties, group_properties, disable_geoip
335
+ distinct_id,
336
+ groups,
337
+ person_properties,
338
+ group_properties,
339
+ disable_geoip,
340
+ flag_keys_to_evaluate,
331
341
  )
332
342
  return to_values(resp_data) or {}
333
343
 
@@ -338,6 +348,7 @@ class Client(object):
338
348
  person_properties=None,
339
349
  group_properties=None,
340
350
  disable_geoip=None,
351
+ flag_keys_to_evaluate: Optional[list[str]] = None,
341
352
  ) -> dict[str, str]:
342
353
  """
343
354
  Get feature flag payloads for a user by calling decide.
@@ -348,6 +359,8 @@ class Client(object):
348
359
  person_properties: A dictionary of person properties.
349
360
  group_properties: A dictionary of group properties.
350
361
  disable_geoip: Whether to disable GeoIP for this request.
362
+ flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
363
+ only these flags will be evaluated, improving performance.
351
364
 
352
365
  Examples:
353
366
  ```python
@@ -358,7 +371,12 @@ class Client(object):
358
371
  Feature Flags
359
372
  """
360
373
  resp_data = self.get_flags_decision(
361
- distinct_id, groups, person_properties, group_properties, disable_geoip
374
+ distinct_id,
375
+ groups,
376
+ person_properties,
377
+ group_properties,
378
+ disable_geoip,
379
+ flag_keys_to_evaluate,
362
380
  )
363
381
  return to_payloads(resp_data) or {}
364
382
 
@@ -369,6 +387,7 @@ class Client(object):
369
387
  person_properties=None,
370
388
  group_properties=None,
371
389
  disable_geoip=None,
390
+ flag_keys_to_evaluate: Optional[list[str]] = None,
372
391
  ) -> FlagsAndPayloads:
373
392
  """
374
393
  Get feature flags and payloads for a user by calling decide.
@@ -379,6 +398,8 @@ class Client(object):
379
398
  person_properties: A dictionary of person properties.
380
399
  group_properties: A dictionary of group properties.
381
400
  disable_geoip: Whether to disable GeoIP for this request.
401
+ flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
402
+ only these flags will be evaluated, improving performance.
382
403
 
383
404
  Examples:
384
405
  ```python
@@ -389,7 +410,12 @@ class Client(object):
389
410
  Feature Flags
390
411
  """
391
412
  resp = self.get_flags_decision(
392
- distinct_id, groups, person_properties, group_properties, disable_geoip
413
+ distinct_id,
414
+ groups,
415
+ person_properties,
416
+ group_properties,
417
+ disable_geoip,
418
+ flag_keys_to_evaluate,
393
419
  )
394
420
  return to_flags_and_payloads(resp)
395
421
 
@@ -400,6 +426,7 @@ class Client(object):
400
426
  person_properties=None,
401
427
  group_properties=None,
402
428
  disable_geoip=None,
429
+ flag_keys_to_evaluate: Optional[list[str]] = None,
403
430
  ) -> FlagsResponse:
404
431
  """
405
432
  Get feature flags decision.
@@ -410,6 +437,8 @@ class Client(object):
410
437
  person_properties: A dictionary of person properties.
411
438
  group_properties: A dictionary of group properties.
412
439
  disable_geoip: Whether to disable GeoIP for this request.
440
+ flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
441
+ only these flags will be evaluated, improving performance.
413
442
 
414
443
  Examples:
415
444
  ```python
@@ -440,6 +469,9 @@ class Client(object):
440
469
  "geoip_disable": disable_geoip,
441
470
  }
442
471
 
472
+ if flag_keys_to_evaluate:
473
+ request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
474
+
443
475
  resp_data = flags(
444
476
  self.api_key,
445
477
  self.host,
@@ -544,6 +576,7 @@ class Client(object):
544
576
  group_properties=flag_options["group_properties"],
545
577
  disable_geoip=disable_geoip,
546
578
  only_evaluate_locally=True,
579
+ flag_keys_to_evaluate=flag_options["flag_keys_filter"],
547
580
  )
548
581
  else:
549
582
  # Default behavior - use remote evaluation
@@ -553,6 +586,7 @@ class Client(object):
553
586
  person_properties=flag_options["person_properties"],
554
587
  group_properties=flag_options["group_properties"],
555
588
  disable_geoip=disable_geoip,
589
+ flag_keys_to_evaluate=flag_options["flag_keys_filter"],
556
590
  )
557
591
  except Exception as e:
558
592
  self.log.exception(
@@ -585,7 +619,7 @@ class Client(object):
585
619
 
586
620
  return self._enqueue(msg, disable_geoip)
587
621
 
588
- def _parse_send_feature_flags(self, send_feature_flags) -> dict:
622
+ def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
589
623
  """
590
624
  Parse and normalize send_feature_flags parameter into a standard format.
591
625
 
@@ -593,8 +627,8 @@ class Client(object):
593
627
  send_feature_flags: Either bool or SendFeatureFlagsOptions dict
594
628
 
595
629
  Returns:
596
- dict: Normalized options with keys: should_send, only_evaluate_locally,
597
- person_properties, group_properties
630
+ SendFeatureFlagsOptions: Normalized options with keys: should_send, only_evaluate_locally,
631
+ person_properties, group_properties, flag_keys_filter
598
632
 
599
633
  Raises:
600
634
  TypeError: If send_feature_flags is not bool or dict
@@ -607,6 +641,7 @@ class Client(object):
607
641
  ),
608
642
  "person_properties": send_feature_flags.get("person_properties"),
609
643
  "group_properties": send_feature_flags.get("group_properties"),
644
+ "flag_keys_filter": send_feature_flags.get("flag_keys_filter"),
610
645
  }
611
646
  elif isinstance(send_feature_flags, bool):
612
647
  return {
@@ -614,6 +649,7 @@ class Client(object):
614
649
  "only_evaluate_locally": None,
615
650
  "person_properties": None,
616
651
  "group_properties": None,
652
+ "flag_keys_filter": None,
617
653
  }
618
654
  else:
619
655
  raise TypeError(
@@ -1183,12 +1219,12 @@ class Client(object):
1183
1219
  self.log.warning(
1184
1220
  f"[FEATURE FLAGS] Unknown group type index {aggregation_group_type_index} for feature flag {feature_flag['key']}"
1185
1221
  )
1186
- # failover to `/decide/`
1222
+ # failover to `/flags`
1187
1223
  raise InconclusiveMatchError("Flag has unknown group type index")
1188
1224
 
1189
1225
  if group_name not in groups:
1190
1226
  # Group flags are never enabled in `groups` aren't passed in
1191
- # don't failover to `/decide/`, since response will be the same
1227
+ # don't failover to `/flags`, since response will be the same
1192
1228
  if warn_on_unknown_groups:
1193
1229
  self.log.warning(
1194
1230
  f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
@@ -1316,7 +1352,7 @@ class Client(object):
1316
1352
  )
1317
1353
  elif not only_evaluate_locally:
1318
1354
  try:
1319
- flag_details, request_id = self._get_feature_flag_details_from_decide(
1355
+ flag_details, request_id = self._get_feature_flag_details_from_server(
1320
1356
  key,
1321
1357
  distinct_id,
1322
1358
  groups,
@@ -1556,7 +1592,7 @@ class Client(object):
1556
1592
  )
1557
1593
  return feature_flag_result.payload if feature_flag_result else None
1558
1594
 
1559
- def _get_feature_flag_details_from_decide(
1595
+ def _get_feature_flag_details_from_server(
1560
1596
  self,
1561
1597
  key: str,
1562
1598
  distinct_id: ID_TYPES,
@@ -1566,10 +1602,15 @@ class Client(object):
1566
1602
  disable_geoip: Optional[bool],
1567
1603
  ) -> tuple[Optional[FeatureFlag], Optional[str]]:
1568
1604
  """
1569
- Calls /decide and returns the flag details and request id
1605
+ Calls /flags and returns the flag details and request id
1570
1606
  """
1571
1607
  resp_data = self.get_flags_decision(
1572
- distinct_id, groups, person_properties, group_properties, disable_geoip
1608
+ distinct_id,
1609
+ groups,
1610
+ person_properties,
1611
+ group_properties,
1612
+ disable_geoip,
1613
+ flag_keys_to_evaluate=[key],
1573
1614
  )
1574
1615
  request_id = resp_data.get("requestId")
1575
1616
  flags = resp_data.get("flags")
@@ -1685,6 +1726,7 @@ class Client(object):
1685
1726
  group_properties=None,
1686
1727
  only_evaluate_locally=False,
1687
1728
  disable_geoip=None,
1729
+ flag_keys_to_evaluate: Optional[list[str]] = None,
1688
1730
  ) -> Optional[dict[str, Union[bool, str]]]:
1689
1731
  """
1690
1732
  Get all feature flags for a user.
@@ -1696,6 +1738,8 @@ class Client(object):
1696
1738
  group_properties: A dictionary of group properties.
1697
1739
  only_evaluate_locally: Whether to only evaluate locally.
1698
1740
  disable_geoip: Whether to disable GeoIP for this request.
1741
+ flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
1742
+ only these flags will be evaluated, improving performance.
1699
1743
 
1700
1744
  Examples:
1701
1745
  ```python
@@ -1712,6 +1756,7 @@ class Client(object):
1712
1756
  group_properties=group_properties,
1713
1757
  only_evaluate_locally=only_evaluate_locally,
1714
1758
  disable_geoip=disable_geoip,
1759
+ flag_keys_to_evaluate=flag_keys_to_evaluate,
1715
1760
  )
1716
1761
 
1717
1762
  return response["featureFlags"]
@@ -1725,6 +1770,7 @@ class Client(object):
1725
1770
  group_properties=None,
1726
1771
  only_evaluate_locally=False,
1727
1772
  disable_geoip=None,
1773
+ flag_keys_to_evaluate: Optional[list[str]] = None,
1728
1774
  ) -> FlagsAndPayloads:
1729
1775
  """
1730
1776
  Get all feature flags and their payloads for a user.
@@ -1736,6 +1782,8 @@ class Client(object):
1736
1782
  group_properties: A dictionary of group properties.
1737
1783
  only_evaluate_locally: Whether to only evaluate locally.
1738
1784
  disable_geoip: Whether to disable GeoIP for this request.
1785
+ flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
1786
+ only these flags will be evaluated, improving performance.
1739
1787
 
1740
1788
  Examples:
1741
1789
  ```python
@@ -1759,6 +1807,7 @@ class Client(object):
1759
1807
  groups=groups,
1760
1808
  person_properties=person_properties,
1761
1809
  group_properties=group_properties,
1810
+ flag_keys_to_evaluate=flag_keys_to_evaluate,
1762
1811
  )
1763
1812
 
1764
1813
  if fallback_to_decide and not only_evaluate_locally:
@@ -1769,6 +1818,7 @@ class Client(object):
1769
1818
  person_properties=person_properties,
1770
1819
  group_properties=group_properties,
1771
1820
  disable_geoip=disable_geoip,
1821
+ flag_keys_to_evaluate=flag_keys_to_evaluate,
1772
1822
  )
1773
1823
  return to_flags_and_payloads(decide_response)
1774
1824
  except Exception as e:
@@ -1786,6 +1836,7 @@ class Client(object):
1786
1836
  person_properties=None,
1787
1837
  group_properties=None,
1788
1838
  warn_on_unknown_groups=False,
1839
+ flag_keys_to_evaluate: Optional[list[str]] = None,
1789
1840
  ) -> tuple[FlagsAndPayloads, bool]:
1790
1841
  person_properties = person_properties or {}
1791
1842
  group_properties = group_properties or {}
@@ -1798,7 +1849,15 @@ class Client(object):
1798
1849
  fallback_to_decide = False
1799
1850
  # If loading in previous line failed
1800
1851
  if self.feature_flags:
1801
- for flag in self.feature_flags:
1852
+ # Filter flags based on flag_keys_to_evaluate if provided
1853
+ flags_to_process = self.feature_flags
1854
+ if flag_keys_to_evaluate:
1855
+ flag_keys_set = set(flag_keys_to_evaluate)
1856
+ flags_to_process = [
1857
+ flag for flag in self.feature_flags if flag["key"] in flag_keys_set
1858
+ ]
1859
+
1860
+ for flag in flags_to_process:
1802
1861
  try:
1803
1862
  flags[flag["key"]] = self._compute_flag_locally(
1804
1863
  flag,
@@ -1814,7 +1873,7 @@ class Client(object):
1814
1873
  if matched_payload is not None:
1815
1874
  payloads[flag["key"]] = matched_payload
1816
1875
  except InconclusiveMatchError:
1817
- # No need to log this, since it's just telling us to fall back to `/decide`
1876
+ # No need to log this, since it's just telling us to fall back to `/flags`
1818
1877
  fallback_to_decide = True
1819
1878
  except Exception as e:
1820
1879
  self.log.exception(
@@ -13,6 +13,7 @@ from posthoganalytics.request import APIError
13
13
  from posthoganalytics.test.test_utils import FAKE_TEST_API_KEY
14
14
  from posthoganalytics.types import FeatureFlag, LegacyFlagMetadata
15
15
  from posthoganalytics.version import VERSION
16
+ from posthoganalytics.contexts import tag
16
17
 
17
18
 
18
19
  class TestClient(unittest.TestCase):
@@ -1741,6 +1742,7 @@ class TestClient(unittest.TestCase):
1741
1742
  person_properties={"distinct_id": "some_id"},
1742
1743
  group_properties={},
1743
1744
  geoip_disable=True,
1745
+ flag_keys_to_evaluate=["random_key"],
1744
1746
  )
1745
1747
  patch_flags.reset_mock()
1746
1748
  client.feature_enabled(
@@ -1755,6 +1757,7 @@ class TestClient(unittest.TestCase):
1755
1757
  person_properties={"distinct_id": "feature_enabled_distinct_id"},
1756
1758
  group_properties={},
1757
1759
  geoip_disable=True,
1760
+ flag_keys_to_evaluate=["random_key"],
1758
1761
  )
1759
1762
  patch_flags.reset_mock()
1760
1763
  client.get_all_flags_and_payloads("all_flags_payloads_id")
@@ -1815,6 +1818,7 @@ class TestClient(unittest.TestCase):
1815
1818
  "instance": {"$group_key": "app.posthog.com"},
1816
1819
  },
1817
1820
  geoip_disable=False,
1821
+ flag_keys_to_evaluate=["random_key"],
1818
1822
  )
1819
1823
 
1820
1824
  patch_flags.reset_mock()
@@ -1841,6 +1845,7 @@ class TestClient(unittest.TestCase):
1841
1845
  "instance": {"$group_key": "app.posthog.com"},
1842
1846
  },
1843
1847
  geoip_disable=False,
1848
+ flag_keys_to_evaluate=["random_key"],
1844
1849
  )
1845
1850
 
1846
1851
  patch_flags.reset_mock()
@@ -2186,6 +2191,7 @@ class TestClient(unittest.TestCase):
2186
2191
  "only_evaluate_locally": None,
2187
2192
  "person_properties": None,
2188
2193
  "group_properties": None,
2194
+ "flag_keys_filter": None,
2189
2195
  }
2190
2196
  self.assertEqual(result, expected)
2191
2197
 
@@ -2196,6 +2202,7 @@ class TestClient(unittest.TestCase):
2196
2202
  "only_evaluate_locally": None,
2197
2203
  "person_properties": None,
2198
2204
  "group_properties": None,
2205
+ "flag_keys_filter": None,
2199
2206
  }
2200
2207
  self.assertEqual(result, expected)
2201
2208
 
@@ -2211,6 +2218,7 @@ class TestClient(unittest.TestCase):
2211
2218
  "only_evaluate_locally": True,
2212
2219
  "person_properties": {"plan": "premium"},
2213
2220
  "group_properties": {"company": {"type": "enterprise"}},
2221
+ "flag_keys_filter": None,
2214
2222
  }
2215
2223
  self.assertEqual(result, expected)
2216
2224
 
@@ -2222,6 +2230,7 @@ class TestClient(unittest.TestCase):
2222
2230
  "only_evaluate_locally": None,
2223
2231
  "person_properties": {"user_id": "123"},
2224
2232
  "group_properties": None,
2233
+ "flag_keys_filter": None,
2225
2234
  }
2226
2235
  self.assertEqual(result, expected)
2227
2236
 
@@ -2232,6 +2241,7 @@ class TestClient(unittest.TestCase):
2232
2241
  "only_evaluate_locally": None,
2233
2242
  "person_properties": None,
2234
2243
  "group_properties": None,
2244
+ "flag_keys_filter": None,
2235
2245
  }
2236
2246
  self.assertEqual(result, expected)
2237
2247
 
@@ -2248,6 +2258,53 @@ class TestClient(unittest.TestCase):
2248
2258
  client._parse_send_feature_flags(None)
2249
2259
  self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
2250
2260
 
2261
+ @mock.patch("posthog.client.flags")
2262
+ def test_capture_with_send_feature_flags_flag_keys_filter(self, patch_flags):
2263
+ """Test that SendFeatureFlagsOptions with flag_keys_filter only evaluates specified flags"""
2264
+ # When flag_keys_to_evaluate is provided, the API should only return the requested flags
2265
+ patch_flags.return_value = {
2266
+ "featureFlags": {
2267
+ "flag1": "value1",
2268
+ "flag3": "value3",
2269
+ }
2270
+ }
2271
+
2272
+ with mock.patch("posthog.client.batch_post") as mock_post:
2273
+ client = Client(
2274
+ FAKE_TEST_API_KEY,
2275
+ on_error=self.set_fail,
2276
+ personal_api_key=FAKE_TEST_API_KEY,
2277
+ sync_mode=True,
2278
+ )
2279
+
2280
+ send_options = {
2281
+ "flag_keys_filter": ["flag1", "flag3"],
2282
+ "person_properties": {"subscription": "pro"},
2283
+ }
2284
+
2285
+ msg_uuid = client.capture(
2286
+ "test event", distinct_id="distinct_id", send_feature_flags=send_options
2287
+ )
2288
+
2289
+ self.assertIsNotNone(msg_uuid)
2290
+ self.assertFalse(self.failed)
2291
+
2292
+ # Verify flags() was called with flag_keys_to_evaluate
2293
+ patch_flags.assert_called_once()
2294
+ call_args = patch_flags.call_args[1]
2295
+ self.assertEqual(call_args["flag_keys_to_evaluate"], ["flag1", "flag3"])
2296
+ self.assertEqual(call_args["person_properties"], {"subscription": "pro"})
2297
+
2298
+ # Check the message includes only the filtered flags
2299
+ mock_post.assert_called_once()
2300
+ batch_data = mock_post.call_args[1]["batch"]
2301
+ msg = batch_data[0]
2302
+
2303
+ self.assertEqual(msg["properties"]["$feature/flag1"], "value1")
2304
+ self.assertEqual(msg["properties"]["$feature/flag3"], "value3")
2305
+ # flag2 should not be included since it wasn't requested
2306
+ self.assertNotIn("$feature/flag2", msg["properties"])
2307
+
2251
2308
  @mock.patch("posthog.client.batch_post")
2252
2309
  def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_post):
2253
2310
  """Test that get_feature_flag_result returns a FeatureFlagResult when payload is empty string"""
@@ -2354,3 +2411,15 @@ class TestClient(unittest.TestCase):
2354
2411
  self.assertEqual(
2355
2412
  result["featureFlagPayloads"]["normal-payload-flag"], "normal payload"
2356
2413
  )
2414
+
2415
+ def test_context_tags_added(self):
2416
+ with mock.patch("posthog.client.batch_post") as mock_post:
2417
+ client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
2418
+
2419
+ with new_context():
2420
+ tag("random_tag", 12345)
2421
+ client.capture("python test event", distinct_id="distinct_id")
2422
+
2423
+ batch_data = mock_post.call_args[1]["batch"]
2424
+ msg = batch_data[0]
2425
+ self.assertEqual(msg["properties"]["$context_tags"], ["random_tag"])
@@ -215,7 +215,7 @@ class TestLocalEvaluation(unittest.TestCase):
215
215
  )
216
216
  self.assertEqual(patch_flags.call_count, 0)
217
217
 
218
- # Now group type mappings are gone, so fall back to /decide/
218
+ # Now group type mappings are gone, so fall back to /flags/
219
219
  patch_flags.return_value = {
220
220
  "featureFlags": {"group-flag": "decide-fallback-value"}
221
221
  }
@@ -311,7 +311,7 @@ class TestLocalEvaluation(unittest.TestCase):
311
311
  )
312
312
  self.assertEqual(patch_flags.call_count, 0)
313
313
 
314
- # will fall back on `/decide`, as all properties present for second group, but that group resolves to false
314
+ # will fall back on `/flags`, as all properties present for second group, but that group resolves to false
315
315
  self.assertEqual(
316
316
  client.get_feature_flag(
317
317
  "complex-flag",
@@ -651,7 +651,7 @@ class TestLocalEvaluation(unittest.TestCase):
651
651
  },
652
652
  },
653
653
  ]
654
- # beta-feature value overridden by /decide
654
+ # beta-feature value overridden by /flags
655
655
  self.assertEqual(
656
656
  client.get_all_flags("distinct_id"),
657
657
  {
@@ -725,7 +725,7 @@ class TestLocalEvaluation(unittest.TestCase):
725
725
  },
726
726
  },
727
727
  ]
728
- # beta-feature value overridden by /decide
728
+ # beta-feature value overridden by /flags
729
729
  self.assertEqual(
730
730
  client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
731
731
  {
@@ -746,7 +746,7 @@ class TestLocalEvaluation(unittest.TestCase):
746
746
  }
747
747
  client = self.client
748
748
  client.feature_flags = []
749
- # beta-feature value overridden by /decide
749
+ # beta-feature value overridden by /flags
750
750
  self.assertEqual(
751
751
  client.get_all_flags("distinct_id"),
752
752
  {"beta-feature": "variant-1", "beta-feature2": "variant-2"},
@@ -765,7 +765,7 @@ class TestLocalEvaluation(unittest.TestCase):
765
765
  }
766
766
  client = self.client
767
767
  client.feature_flags = []
768
- # beta-feature value overridden by /decide
768
+ # beta-feature value overridden by /flags
769
769
  self.assertEqual(
770
770
  client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
771
771
  {"beta-feature": 100, "beta-feature2": 300},
@@ -5387,4 +5387,115 @@ class TestConsistency(unittest.TestCase):
5387
5387
  test_cases = ["beta-feature", "BETA-FEATURE", "bEtA-FeAtUrE"]
5388
5388
  for case in test_cases:
5389
5389
  self.assertFalse(client.feature_enabled(case, "user1"))
5390
- self.assertIsNone(client.get_feature_flag_payload(case, "user1"))
5390
+
5391
+ @mock.patch("posthog.client.flags")
5392
+ def test_get_all_flags_with_flag_keys_to_evaluate(self, mock_flags):
5393
+ """Test that get_all_flags with flag_keys_to_evaluate only evaluates specified flags"""
5394
+ mock_flags.return_value = {
5395
+ "featureFlags": {
5396
+ "flag1": "value1",
5397
+ "flag2": True,
5398
+ }
5399
+ }
5400
+
5401
+ client = Client(
5402
+ project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
5403
+ )
5404
+
5405
+ # Call get_all_flags with flag_keys_to_evaluate
5406
+ result = client.get_all_flags(
5407
+ "user123",
5408
+ flag_keys_to_evaluate=["flag1", "flag2"],
5409
+ person_properties={"region": "USA"},
5410
+ )
5411
+
5412
+ # Verify flags() was called with flag_keys_to_evaluate
5413
+ mock_flags.assert_called_once()
5414
+ call_args = mock_flags.call_args[1]
5415
+ self.assertEqual(call_args["flag_keys_to_evaluate"], ["flag1", "flag2"])
5416
+ self.assertEqual(
5417
+ call_args["person_properties"], {"distinct_id": "user123", "region": "USA"}
5418
+ )
5419
+
5420
+ # Check the result
5421
+ self.assertEqual(result, {"flag1": "value1", "flag2": True})
5422
+
5423
+ @mock.patch("posthog.client.flags")
5424
+ def test_get_all_flags_and_payloads_with_flag_keys_to_evaluate(self, mock_flags):
5425
+ """Test that get_all_flags_and_payloads with flag_keys_to_evaluate only evaluates specified flags"""
5426
+ mock_flags.return_value = {
5427
+ "featureFlags": {
5428
+ "flag1": "variant1",
5429
+ "flag3": True,
5430
+ },
5431
+ "featureFlagPayloads": {
5432
+ "flag1": {"data": "payload1"},
5433
+ "flag3": {"data": "payload3"},
5434
+ },
5435
+ }
5436
+
5437
+ client = Client(
5438
+ project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
5439
+ )
5440
+
5441
+ # Call get_all_flags_and_payloads with flag_keys_to_evaluate
5442
+ result = client.get_all_flags_and_payloads(
5443
+ "user123",
5444
+ flag_keys_to_evaluate=["flag1", "flag3"],
5445
+ person_properties={"subscription": "pro"},
5446
+ )
5447
+
5448
+ # Verify flags() was called with flag_keys_to_evaluate
5449
+ mock_flags.assert_called_once()
5450
+ call_args = mock_flags.call_args[1]
5451
+ self.assertEqual(call_args["flag_keys_to_evaluate"], ["flag1", "flag3"])
5452
+ self.assertEqual(
5453
+ call_args["person_properties"],
5454
+ {"distinct_id": "user123", "subscription": "pro"},
5455
+ )
5456
+
5457
+ # Check the result
5458
+ self.assertEqual(result["featureFlags"], {"flag1": "variant1", "flag3": True})
5459
+ self.assertEqual(
5460
+ result["featureFlagPayloads"],
5461
+ {"flag1": {"data": "payload1"}, "flag3": {"data": "payload3"}},
5462
+ )
5463
+
5464
+ def test_get_all_flags_locally_with_flag_keys_to_evaluate(self):
5465
+ """Test that local evaluation with flag_keys_to_evaluate only evaluates specified flags"""
5466
+ client = Client(
5467
+ project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
5468
+ )
5469
+
5470
+ # Set up multiple flags
5471
+ client.feature_flags = [
5472
+ {
5473
+ "id": 1,
5474
+ "key": "flag1",
5475
+ "active": True,
5476
+ "filters": {"groups": [{"properties": [], "rollout_percentage": 100}]},
5477
+ },
5478
+ {
5479
+ "id": 2,
5480
+ "key": "flag2",
5481
+ "active": True,
5482
+ "filters": {"groups": [{"properties": [], "rollout_percentage": 100}]},
5483
+ },
5484
+ {
5485
+ "id": 3,
5486
+ "key": "flag3",
5487
+ "active": True,
5488
+ "filters": {"groups": [{"properties": [], "rollout_percentage": 100}]},
5489
+ },
5490
+ ]
5491
+
5492
+ # Call get_all_flags with flag_keys_to_evaluate
5493
+ result = client.get_all_flags(
5494
+ "user123",
5495
+ flag_keys_to_evaluate=["flag1", "flag3"],
5496
+ only_evaluate_locally=True,
5497
+ )
5498
+
5499
+ # Should only return flag1 and flag3
5500
+ self.assertEqual(result, {"flag1": True, "flag3": True})
5501
+ self.assertNotIn("flag2", result)
@@ -9,6 +9,7 @@ FlagValue = Union[bool, str]
9
9
  BeforeSendCallback = Callable[[dict[str, Any]], Optional[dict[str, Any]]]
10
10
 
11
11
 
12
+ # Type alias for the send_feature_flags parameter
12
13
  class SendFeatureFlagsOptions(TypedDict, total=False):
13
14
  """Options for sending feature flags with capture events.
14
15
 
@@ -22,9 +23,11 @@ class SendFeatureFlagsOptions(TypedDict, total=False):
22
23
  Format: { group_type_name: { group_properties } }
23
24
  """
24
25
 
26
+ should_send: bool
25
27
  only_evaluate_locally: Optional[bool]
26
28
  person_properties: Optional[dict[str, Any]]
27
29
  group_properties: Optional[dict[str, dict[str, Any]]]
30
+ flag_keys_filter: Optional[list[str]]
28
31
 
29
32
 
30
33
  @dataclass(frozen=True)
@@ -1,4 +1,4 @@
1
- VERSION = "6.4.1"
1
+ VERSION = "6.6.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.4.1
3
+ Version: 6.6.0
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog