posthoganalytics 6.7.5__tar.gz → 6.7.6__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 (62) hide show
  1. {posthoganalytics-6.7.5/posthoganalytics.egg-info → posthoganalytics-6.7.6}/PKG-INFO +1 -1
  2. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/client.py +42 -1
  3. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/feature_flags.py +1 -8
  4. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_client.py +43 -0
  5. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_feature_flags.py +23 -35
  6. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_module.py +0 -8
  7. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/version.py +1 -1
  8. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6/posthoganalytics.egg-info}/PKG-INFO +1 -1
  9. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/LICENSE +0 -0
  10. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/MANIFEST.in +0 -0
  11. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/README.md +0 -0
  12. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/__init__.py +0 -0
  13. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/__init__.py +0 -0
  14. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/anthropic/__init__.py +0 -0
  15. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/anthropic/anthropic.py +0 -0
  16. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
  17. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/anthropic/anthropic_converter.py +0 -0
  18. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
  19. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/gemini/__init__.py +0 -0
  20. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/gemini/gemini.py +0 -0
  21. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/gemini/gemini_converter.py +0 -0
  22. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/langchain/__init__.py +0 -0
  23. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/langchain/callbacks.py +0 -0
  24. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/openai/__init__.py +0 -0
  25. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/openai/openai.py +0 -0
  26. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/openai/openai_async.py +0 -0
  27. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/openai/openai_converter.py +0 -0
  28. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/openai/openai_providers.py +0 -0
  29. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/sanitization.py +0 -0
  30. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/types.py +0 -0
  31. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/ai/utils.py +0 -0
  32. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/args.py +0 -0
  33. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/consumer.py +0 -0
  34. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/contexts.py +0 -0
  35. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/exception_capture.py +0 -0
  36. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/exception_utils.py +0 -0
  37. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/integrations/__init__.py +0 -0
  38. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/integrations/django.py +0 -0
  39. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/poller.py +0 -0
  40. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/py.typed +0 -0
  41. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/request.py +0 -0
  42. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/__init__.py +0 -0
  43. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_before_send.py +0 -0
  44. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_consumer.py +0 -0
  45. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_contexts.py +0 -0
  46. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_exception_capture.py +0 -0
  47. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_feature_flag.py +0 -0
  48. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_feature_flag_result.py +0 -0
  49. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_request.py +0 -0
  50. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_size_limited_dict.py +0 -0
  51. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_types.py +0 -0
  52. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/test/test_utils.py +0 -0
  53. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/types.py +0 -0
  54. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics/utils.py +0 -0
  55. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics.egg-info/SOURCES.txt +0 -0
  56. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics.egg-info/dependency_links.txt +0 -0
  57. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics.egg-info/requires.txt +0 -0
  58. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/posthoganalytics.egg-info/top_level.txt +0 -0
  59. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/pyproject.toml +0 -0
  60. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/setup.cfg +0 -0
  61. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/setup.py +0 -0
  62. {posthoganalytics-6.7.5 → posthoganalytics-6.7.6}/setup_analytics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthoganalytics
3
- Version: 6.7.5
3
+ Version: 6.7.6
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -3,7 +3,7 @@ import logging
3
3
  import os
4
4
  import sys
5
5
  from datetime import datetime, timedelta
6
- from typing import Any, Dict, Optional, Union
6
+ from typing import Any, Callable, Dict, Optional, Union
7
7
  from typing_extensions import Unpack
8
8
  from uuid import uuid4
9
9
 
@@ -99,6 +99,34 @@ def add_context_tags(properties):
99
99
  return properties
100
100
 
101
101
 
102
+ def no_throw(default_return=None):
103
+ """
104
+ Decorator to prevent raising exceptions from public API methods.
105
+ Note that this doesn't prevent errors from propagating via `on_error`.
106
+ Exceptions will still be raised if the debug flag is enabled.
107
+
108
+ Args:
109
+ default_return: Value to return on exception (default: None)
110
+ """
111
+
112
+ def decorator(func):
113
+ from functools import wraps
114
+
115
+ @wraps(func)
116
+ def wrapper(self, *args, **kwargs):
117
+ try:
118
+ return func(self, *args, **kwargs)
119
+ except Exception as e:
120
+ if self.debug:
121
+ raise e
122
+ self.log.exception(f"Error in {func.__name__}: {e}")
123
+ return default_return
124
+
125
+ return wrapper
126
+
127
+ return decorator
128
+
129
+
102
130
  class Client(object):
103
131
  """
104
132
  This is the SDK reference for the PostHog Python SDK.
@@ -481,6 +509,7 @@ class Client(object):
481
509
 
482
510
  return normalize_flags_response(resp_data)
483
511
 
512
+ @no_throw()
484
513
  def capture(
485
514
  self, event: str, **kwargs: Unpack[OptionalCaptureArgs]
486
515
  ) -> Optional[str]:
@@ -657,6 +686,7 @@ class Client(object):
657
686
  f"Expected bool or dict."
658
687
  )
659
688
 
689
+ @no_throw()
660
690
  def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
661
691
  """
662
692
  Set properties on a person profile.
@@ -690,6 +720,8 @@ class Client(object):
690
720
 
691
721
  Category:
692
722
  Identification
723
+
724
+ Note: This method will not raise exceptions. Errors are logged.
693
725
  """
694
726
  distinct_id = kwargs.get("distinct_id", None)
695
727
  properties = kwargs.get("properties", None)
@@ -716,6 +748,7 @@ class Client(object):
716
748
 
717
749
  return self._enqueue(msg, disable_geoip)
718
750
 
751
+ @no_throw()
719
752
  def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
720
753
  """
721
754
  Set properties on a person profile only if they haven't been set before.
@@ -734,6 +767,8 @@ class Client(object):
734
767
 
735
768
  Category:
736
769
  Identification
770
+
771
+ Note: This method will not raise exceptions. Errors are logged.
737
772
  """
738
773
  distinct_id = kwargs.get("distinct_id", None)
739
774
  properties = kwargs.get("properties", None)
@@ -759,6 +794,7 @@ class Client(object):
759
794
 
760
795
  return self._enqueue(msg, disable_geoip)
761
796
 
797
+ @no_throw()
762
798
  def group_identify(
763
799
  self,
764
800
  group_type: str,
@@ -791,6 +827,8 @@ class Client(object):
791
827
 
792
828
  Category:
793
829
  Identification
830
+
831
+ Note: This method will not raise exceptions. Errors are logged.
794
832
  """
795
833
  properties = properties or {}
796
834
 
@@ -815,6 +853,7 @@ class Client(object):
815
853
 
816
854
  return self._enqueue(msg, disable_geoip)
817
855
 
856
+ @no_throw()
818
857
  def alias(
819
858
  self,
820
859
  previous_id: str,
@@ -840,6 +879,8 @@ class Client(object):
840
879
 
841
880
  Category:
842
881
  Identification
882
+
883
+ Note: This method will not raise exceptions. Errors are logged.
843
884
  """
844
885
  (distinct_id, personless) = get_identity_state(distinct_id)
845
886
 
@@ -220,14 +220,7 @@ def match_feature_flag_properties(
220
220
  ) or []
221
221
  valid_variant_keys = [variant["key"] for variant in flag_variants]
222
222
 
223
- # Stable sort conditions with variant overrides to the top. This ensures that if overrides are present, they are
224
- # evaluated first, and the variant override is applied to the first matching condition.
225
- sorted_flag_conditions = sorted(
226
- flag_conditions,
227
- key=lambda condition: 0 if condition.get("variant") else 1,
228
- )
229
-
230
- for condition in sorted_flag_conditions:
223
+ for condition in flag_conditions:
231
224
  try:
232
225
  # if any one condition resolves to True, we can shortcircuit and return
233
226
  # the matching variant
@@ -2423,3 +2423,46 @@ class TestClient(unittest.TestCase):
2423
2423
  batch_data = mock_post.call_args[1]["batch"]
2424
2424
  msg = batch_data[0]
2425
2425
  self.assertEqual(msg["properties"]["$context_tags"], ["random_tag"])
2426
+
2427
+ @mock.patch(
2428
+ "posthog.client.Client._enqueue", side_effect=Exception("Unexpected error")
2429
+ )
2430
+ def test_methods_handle_exceptions(self, mock_enqueue):
2431
+ """Test that all decorated methods handle exceptions gracefully."""
2432
+ client = Client("test-key")
2433
+
2434
+ test_cases = [
2435
+ ("capture", ["test_event"], {}),
2436
+ ("set", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2437
+ ("set_once", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2438
+ ("group_identify", ["group-type", "group-key"], {}),
2439
+ ("alias", ["some-id", "new-id"], {}),
2440
+ ]
2441
+
2442
+ for method_name, args, kwargs in test_cases:
2443
+ with self.subTest(method=method_name):
2444
+ method = getattr(client, method_name)
2445
+ result = method(*args, **kwargs)
2446
+ self.assertEqual(result, None)
2447
+
2448
+ @mock.patch(
2449
+ "posthog.client.Client._enqueue", side_effect=Exception("Expected error")
2450
+ )
2451
+ def test_debug_flag_re_raises_exceptions(self, mock_enqueue):
2452
+ """Test that methods re-raise exceptions when debug=True."""
2453
+ client = Client("test-key", debug=True)
2454
+
2455
+ test_cases = [
2456
+ ("capture", ["test_event"], {}),
2457
+ ("set", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2458
+ ("set_once", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2459
+ ("group_identify", ["group-type", "group-key"], {}),
2460
+ ("alias", ["some-id", "new-id"], {}),
2461
+ ]
2462
+
2463
+ for method_name, args, kwargs in test_cases:
2464
+ with self.subTest(method=method_name):
2465
+ method = getattr(client, method_name)
2466
+ with self.assertRaises(Exception) as cm:
2467
+ method(*args, **kwargs)
2468
+ self.assertEqual(str(cm.exception), "Expected error")
@@ -2804,73 +2804,61 @@ class TestLocalEvaluation(unittest.TestCase):
2804
2804
  self.assertEqual(patch_flags.call_count, 0)
2805
2805
 
2806
2806
  @mock.patch("posthog.client.flags")
2807
- def test_flag_with_multiple_variant_overrides(self, patch_flags):
2808
- patch_flags.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
2807
+ def test_conditions_evaluated_in_order(self, patch_flags):
2808
+ patch_flags.return_value = {"featureFlags": {"order-test": "server-variant"}}
2809
2809
  client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
2810
2810
  client.feature_flags = [
2811
2811
  {
2812
2812
  "id": 1,
2813
- "name": "Beta Feature",
2814
- "key": "beta-feature",
2813
+ "name": "Order Test Flag",
2814
+ "key": "order-test",
2815
2815
  "active": True,
2816
- "rollout_percentage": 100,
2817
2816
  "filters": {
2818
2817
  "groups": [
2819
2818
  {
2820
2819
  "rollout_percentage": 100,
2821
- # The override applies even if the first condition matches all and gives everyone their default group
2822
2820
  },
2823
2821
  {
2824
2822
  "properties": [
2825
2823
  {
2826
2824
  "key": "email",
2827
2825
  "type": "person",
2828
- "value": "test@posthog.com",
2829
- "operator": "exact",
2826
+ "value": "@vip.com",
2827
+ "operator": "icontains",
2830
2828
  }
2831
2829
  ],
2832
2830
  "rollout_percentage": 100,
2833
- "variant": "second-variant",
2831
+ "variant": "vip-variant",
2834
2832
  },
2835
- {"rollout_percentage": 50, "variant": "third-variant"},
2836
2833
  ],
2837
2834
  "multivariate": {
2838
2835
  "variants": [
2839
2836
  {
2840
- "key": "first-variant",
2841
- "name": "First Variant",
2842
- "rollout_percentage": 50,
2837
+ "key": "control",
2838
+ "name": "Control",
2839
+ "rollout_percentage": 100,
2843
2840
  },
2844
2841
  {
2845
- "key": "second-variant",
2846
- "name": "Second Variant",
2847
- "rollout_percentage": 25,
2848
- },
2849
- {
2850
- "key": "third-variant",
2851
- "name": "Third Variant",
2852
- "rollout_percentage": 25,
2842
+ "key": "vip-variant",
2843
+ "name": "VIP Variant",
2844
+ "rollout_percentage": 0,
2853
2845
  },
2854
2846
  ]
2855
2847
  },
2856
2848
  },
2857
2849
  }
2858
2850
  ]
2859
- self.assertEqual(
2860
- client.get_feature_flag(
2861
- "beta-feature",
2862
- "test_id",
2863
- person_properties={"email": "test@posthog.com"},
2864
- ),
2865
- "second-variant",
2866
- )
2867
- self.assertEqual(
2868
- client.get_feature_flag("beta-feature", "example_id"), "third-variant"
2869
- )
2870
- self.assertEqual(
2871
- client.get_feature_flag("beta-feature", "another_id"), "second-variant"
2851
+
2852
+ # Even though user@vip.com would match the second condition with variant override,
2853
+ # they should match the first condition and get control
2854
+ result = client.get_feature_flag(
2855
+ "order-test",
2856
+ "user123",
2857
+ person_properties={"email": "user@vip.com"},
2872
2858
  )
2873
- # decide not called because this can be evaluated locally
2859
+ self.assertEqual(result, "control")
2860
+
2861
+ # server not called because this can be evaluated locally
2874
2862
  self.assertEqual(patch_flags.call_count, 0)
2875
2863
 
2876
2864
  @mock.patch("posthog.client.flags")
@@ -18,14 +18,6 @@ class TestModule(unittest.TestCase):
18
18
  "testsecret", host="http://localhost:8000", on_error=self.failed
19
19
  )
20
20
 
21
- def test_no_api_key(self):
22
- self.posthog.api_key = None
23
- self.assertRaises(Exception, self.posthog.capture)
24
-
25
- def test_no_host(self):
26
- self.posthog.host = None
27
- self.assertRaises(Exception, self.posthog.capture)
28
-
29
21
  def test_track(self):
30
22
  res = self.posthog.capture("python module event", distinct_id="distinct_id")
31
23
  self._assert_enqueue_result(res)
@@ -1,4 +1,4 @@
1
- VERSION = "6.7.5"
1
+ VERSION = "6.7.6"
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.5
3
+ Version: 6.7.6
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog