posthog 7.5.0__py3-none-any.whl → 7.5.1__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.
posthog/consumer.py CHANGED
@@ -84,12 +84,16 @@ class Consumer(Thread):
84
84
  self.log.error("error uploading: %s", e)
85
85
  success = False
86
86
  if self.on_error:
87
- self.on_error(e, batch)
87
+ try:
88
+ self.on_error(e, batch)
89
+ except Exception as e:
90
+ self.log.error("on_error handler failed: %s", e)
88
91
  finally:
89
92
  # mark items as acknowledged from queue
90
93
  for item in batch:
91
94
  self.queue.task_done()
92
- return success
95
+
96
+ return success
93
97
 
94
98
  def next(self):
95
99
  """Return the next batch of items to upload."""
@@ -1,8 +1,10 @@
1
1
  import json
2
2
  import time
3
3
  import unittest
4
+ from typing import Any
4
5
 
5
6
  import mock
7
+ from parameterized import parameterized
6
8
 
7
9
  try:
8
10
  from queue import Queue
@@ -14,15 +16,19 @@ from posthog.request import APIError
14
16
  from posthog.test.test_utils import TEST_API_KEY
15
17
 
16
18
 
19
+ def _track_event(event_name: str = "python event") -> dict[str, str]:
20
+ return {"type": "track", "event": event_name, "distinct_id": "distinct_id"}
21
+
22
+
17
23
  class TestConsumer(unittest.TestCase):
18
- def test_next(self):
24
+ def test_next(self) -> None:
19
25
  q = Queue()
20
26
  consumer = Consumer(q, "")
21
27
  q.put(1)
22
28
  next = consumer.next()
23
29
  self.assertEqual(next, [1])
24
30
 
25
- def test_next_limit(self):
31
+ def test_next_limit(self) -> None:
26
32
  q = Queue()
27
33
  flush_at = 50
28
34
  consumer = Consumer(q, "", flush_at)
@@ -31,7 +37,7 @@ class TestConsumer(unittest.TestCase):
31
37
  next = consumer.next()
32
38
  self.assertEqual(next, list(range(flush_at)))
33
39
 
34
- def test_dropping_oversize_msg(self):
40
+ def test_dropping_oversize_msg(self) -> None:
35
41
  q = Queue()
36
42
  consumer = Consumer(q, "")
37
43
  oversize_msg = {"m": "x" * MAX_MSG_SIZE}
@@ -40,15 +46,14 @@ class TestConsumer(unittest.TestCase):
40
46
  self.assertEqual(next, [])
41
47
  self.assertTrue(q.empty())
42
48
 
43
- def test_upload(self):
49
+ def test_upload(self) -> None:
44
50
  q = Queue()
45
51
  consumer = Consumer(q, TEST_API_KEY)
46
- track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
47
- q.put(track)
52
+ q.put(_track_event())
48
53
  success = consumer.upload()
49
54
  self.assertTrue(success)
50
55
 
51
- def test_flush_interval(self):
56
+ def test_flush_interval(self) -> None:
52
57
  # Put _n_ items in the queue, pausing a little bit more than
53
58
  # _flush_interval_ after each one.
54
59
  # The consumer should upload _n_ times.
@@ -57,17 +62,12 @@ class TestConsumer(unittest.TestCase):
57
62
  consumer = Consumer(q, TEST_API_KEY, flush_at=10, flush_interval=flush_interval)
58
63
  with mock.patch("posthog.consumer.batch_post") as mock_post:
59
64
  consumer.start()
60
- for i in range(0, 3):
61
- track = {
62
- "type": "track",
63
- "event": "python event %d" % i,
64
- "distinct_id": "distinct_id",
65
- }
66
- q.put(track)
65
+ for i in range(3):
66
+ q.put(_track_event("python event %d" % i))
67
67
  time.sleep(flush_interval * 1.1)
68
68
  self.assertEqual(mock_post.call_count, 3)
69
69
 
70
- def test_multiple_uploads_per_interval(self):
70
+ def test_multiple_uploads_per_interval(self) -> None:
71
71
  # Put _flush_at*2_ items in the queue at once, then pause for
72
72
  # _flush_interval_. The consumer should upload 2 times.
73
73
  q = Queue()
@@ -78,88 +78,60 @@ class TestConsumer(unittest.TestCase):
78
78
  )
79
79
  with mock.patch("posthog.consumer.batch_post") as mock_post:
80
80
  consumer.start()
81
- for i in range(0, flush_at * 2):
82
- track = {
83
- "type": "track",
84
- "event": "python event %d" % i,
85
- "distinct_id": "distinct_id",
86
- }
87
- q.put(track)
81
+ for i in range(flush_at * 2):
82
+ q.put(_track_event("python event %d" % i))
88
83
  time.sleep(flush_interval * 1.1)
89
84
  self.assertEqual(mock_post.call_count, 2)
90
85
 
91
- def test_request(self):
86
+ def test_request(self) -> None:
92
87
  consumer = Consumer(None, TEST_API_KEY)
93
- track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
94
- consumer.request([track])
88
+ consumer.request([_track_event()])
95
89
 
96
- def _test_request_retry(self, consumer, expected_exception, exception_count):
97
- def mock_post(*args, **kwargs):
98
- mock_post.call_count += 1
99
- if mock_post.call_count <= exception_count:
100
- raise expected_exception
90
+ def _run_retry_test(
91
+ self, exception: Exception, exception_count: int, retries: int = 10
92
+ ) -> None:
93
+ call_count = [0]
101
94
 
102
- mock_post.call_count = 0
95
+ def mock_post(*args: Any, **kwargs: Any) -> None:
96
+ call_count[0] += 1
97
+ if call_count[0] <= exception_count:
98
+ raise exception
103
99
 
100
+ consumer = Consumer(None, TEST_API_KEY, retries=retries)
104
101
  with mock.patch(
105
102
  "posthog.consumer.batch_post", mock.Mock(side_effect=mock_post)
106
103
  ):
107
- track = {
108
- "type": "track",
109
- "event": "python event",
110
- "distinct_id": "distinct_id",
111
- }
112
- # request() should succeed if the number of exceptions raised is
113
- # less than the retries paramater.
114
- if exception_count <= consumer.retries:
115
- consumer.request([track])
104
+ if exception_count <= retries:
105
+ consumer.request([_track_event()])
116
106
  else:
117
- # if exceptions are raised more times than the retries
118
- # parameter, we expect the exception to be returned to
119
- # the caller.
120
- try:
121
- consumer.request([track])
122
- except type(expected_exception) as exc:
123
- self.assertEqual(exc, expected_exception)
124
- else:
125
- self.fail(
126
- "request() should raise an exception if still failing after %d retries"
127
- % consumer.retries
128
- )
129
-
130
- def test_request_retry(self):
131
- # we should retry on general errors
132
- consumer = Consumer(None, TEST_API_KEY)
133
- self._test_request_retry(consumer, Exception("generic exception"), 2)
134
-
135
- # we should retry on server errors
136
- consumer = Consumer(None, TEST_API_KEY)
137
- self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 2)
138
-
139
- # we should retry on HTTP 429 errors
140
- consumer = Consumer(None, TEST_API_KEY)
141
- self._test_request_retry(consumer, APIError(429, "Too Many Requests"), 2)
142
-
143
- # we should NOT retry on other client errors
144
- consumer = Consumer(None, TEST_API_KEY)
145
- api_error = APIError(400, "Client Errors")
146
- try:
147
- self._test_request_retry(consumer, api_error, 1)
148
- except APIError:
149
- pass
150
- else:
151
- self.fail("request() should not retry on client errors")
152
-
153
- # test for number of exceptions raise > retries value
154
- consumer = Consumer(None, TEST_API_KEY, retries=3)
155
- self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 3)
156
-
157
- def test_pause(self):
107
+ with self.assertRaises(type(exception)):
108
+ consumer.request([_track_event()])
109
+
110
+ @parameterized.expand(
111
+ [
112
+ ("general_errors", Exception("generic exception"), 2),
113
+ ("server_errors", APIError(500, "Internal Server Error"), 2),
114
+ ("rate_limit_errors", APIError(429, "Too Many Requests"), 2),
115
+ ]
116
+ )
117
+ def test_request_retries_on_retriable_errors(
118
+ self, _name: str, exception: Exception, exception_count: int
119
+ ) -> None:
120
+ self._run_retry_test(exception, exception_count)
121
+
122
+ def test_request_does_not_retry_client_errors(self) -> None:
123
+ with self.assertRaises(APIError):
124
+ self._run_retry_test(APIError(400, "Client Errors"), 1)
125
+
126
+ def test_request_fails_when_exceptions_exceed_retries(self) -> None:
127
+ self._run_retry_test(APIError(500, "Internal Server Error"), 4, retries=3)
128
+
129
+ def test_pause(self) -> None:
158
130
  consumer = Consumer(None, TEST_API_KEY)
159
131
  consumer.pause()
160
132
  self.assertFalse(consumer.running)
161
133
 
162
- def test_max_batch_size(self):
134
+ def test_max_batch_size(self) -> None:
163
135
  q = Queue()
164
136
  consumer = Consumer(q, TEST_API_KEY, flush_at=100000, flush_interval=3)
165
137
  properties = {}
@@ -175,7 +147,7 @@ class TestConsumer(unittest.TestCase):
175
147
  # Let's capture 8MB of data to trigger two batches
176
148
  n_msgs = int(8_000_000 / msg_size)
177
149
 
178
- def mock_post_fn(_, data, **kwargs):
150
+ def mock_post_fn(_: str, data: str, **kwargs: Any) -> mock.Mock:
179
151
  res = mock.Mock()
180
152
  res.status_code = 200
181
153
  request_size = len(data.encode())
@@ -194,3 +166,34 @@ class TestConsumer(unittest.TestCase):
194
166
  q.put(track)
195
167
  q.join()
196
168
  self.assertEqual(mock_post.call_count, 2)
169
+
170
+ @parameterized.expand(
171
+ [
172
+ ("on_error_succeeds", False),
173
+ ("on_error_raises", True),
174
+ ]
175
+ )
176
+ def test_upload_exception_calls_on_error_and_does_not_raise(
177
+ self, _name: str, on_error_raises: bool
178
+ ) -> None:
179
+ on_error_called: list[tuple[Exception, list[dict[str, str]]]] = []
180
+
181
+ def on_error(e: Exception, batch: list[dict[str, str]]) -> None:
182
+ on_error_called.append((e, batch))
183
+ if on_error_raises:
184
+ raise Exception("on_error failed")
185
+
186
+ q = Queue()
187
+ consumer = Consumer(q, TEST_API_KEY, on_error=on_error)
188
+ track = _track_event()
189
+ q.put(track)
190
+
191
+ with mock.patch.object(
192
+ consumer, "request", side_effect=Exception("request failed")
193
+ ):
194
+ result = consumer.upload()
195
+
196
+ self.assertFalse(result)
197
+ self.assertEqual(len(on_error_called), 1)
198
+ self.assertEqual(str(on_error_called[0][0]), "request failed")
199
+ self.assertEqual(on_error_called[0][1], [track])
posthog/version.py CHANGED
@@ -1,4 +1,4 @@
1
- VERSION = "7.5.0"
1
+ VERSION = "7.5.1"
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: posthog
3
- Version: 7.5.0
3
+ Version: 7.5.1
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -87,6 +87,14 @@ Dynamic: maintainer
87
87
 
88
88
  Please see the [Python integration docs](https://posthog.com/docs/integrations/python-integration) for details.
89
89
 
90
+ ## Python Version Support
91
+
92
+ | SDK Version | Python Versions Supported | Notes |
93
+ |-------------|---------------------------|-------|
94
+ | 7.3.1+ | 3.10, 3.11, 3.12, 3.13, 3.14 | Added Python 3.14 support |
95
+ | 7.0.0 - 7.0.1 | 3.10, 3.11, 3.12, 3.13 | Dropped Python 3.9 support |
96
+ | 4.0.1 - 6.x | 3.9, 3.10, 3.11, 3.12, 3.13 | Python 3.9+ required |
97
+
90
98
  ## Development
91
99
 
92
100
  ### Testing Locally
@@ -1,7 +1,7 @@
1
1
  posthog/__init__.py,sha256=LWMZ06Ys1vzDv1b0bAIdYRivKkPuKHDkn8W8SgRCyG8,28718
2
2
  posthog/args.py,sha256=JUt0vbtF33IzLt3ARgsxMEYYnZo3RNS_LcK4-CjWaco,3298
3
3
  posthog/client.py,sha256=u8wpxTN_Vc6InJUK-GI_620aCQ9LgiWZ6j76rqjfZH4,82233
4
- posthog/consumer.py,sha256=fdteMZ-deJGMpaQmHyznw_cwQG2Vvld1tmN9LUkZPrY,4608
4
+ posthog/consumer.py,sha256=eMS2Q81aaScjDaLEjsBYx5DsrBNdiWEXD-aGKdY2NyE,4738
5
5
  posthog/contexts.py,sha256=-_XWC4OBmuVl3kQGCZcdj9I7A72Y2ccFFXO7_2FZT4Q,12534
6
6
  posthog/exception_capture.py,sha256=pmKtjQ6QY6zs4u_-ZA4H1gCyR3iI4sfqCQG_jwe_bKo,1774
7
7
  posthog/exception_utils.py,sha256=AsFNhFQdeJG1sNJMsCQ4hy6Hjhvq_fOGSAGHVBGpXig,33738
@@ -12,7 +12,7 @@ posthog/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  posthog/request.py,sha256=_WdffuI4BgRL9UvbEEi-4uxpFW3P4h7PRDpYM0lawVU,11817
13
13
  posthog/types.py,sha256=OxGHSmmhVYwA7ecmJXUznDCZ1c4gAGtERzSLSYlyQFM,11540
14
14
  posthog/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
15
- posthog/version.py,sha256=BSerKR6ueQqDBV7jQ8PZbMeuApV-brq4Om1VRMcsA8E,87
15
+ posthog/version.py,sha256=LMvATKqkjlQkAH72PmLuWIk_lEtgTE3LQljPg7fcOLo,87
16
16
  posthog/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  posthog/ai/sanitization.py,sha256=Dpx_5gKZfDS38KjmK1C0lvvjm9N8Pp_oIxusac888-g,6057
18
18
  posthog/ai/types.py,sha256=arX98hR1PIPeJ3vFikxTlACIh1xPp6aEUw1gBLcKoB0,3273
@@ -38,7 +38,7 @@ posthog/integrations/django.py,sha256=9X37yCF-T-MXUsxqkqjBWG3kdgOCyQYYNJQG_ZlwbR
38
38
  posthog/test/__init__.py,sha256=VYgM6xPbJbvS-xhIcDiBRs0MFC9V_jT65uNeerCz_rM,299
39
39
  posthog/test/test_before_send.py,sha256=3546WKlk8rF6bhvqhwcxAsjJovDw0Hf8yTvcYGbrhyI,7912
40
40
  posthog/test/test_client.py,sha256=n7v0Wn9d55CKhBgiU5eBdQi1WP6qdeihUbOCBkc0R5Q,102841
41
- posthog/test/test_consumer.py,sha256=HRDXSH0IPpCfo5yHs23n-0VzFyGSjWBKLEa8XNtU3_Y,7080
41
+ posthog/test/test_consumer.py,sha256=3xBl7isCZANJJ_616ZMqnzUNwS6QrhqHvTJVxmRfyv0,6991
42
42
  posthog/test/test_contexts.py,sha256=AiTClE306dRVg4gLE_g2ZSLAlAECvQQNr8m6U_E01WY,8095
43
43
  posthog/test/test_exception_capture.py,sha256=nqG33mnxpMrSfPxXGoK6hz3yrQu3Yr-pV7r5_yYBZ68,13305
44
44
  posthog/test/test_feature_flag.py,sha256=yIMJkoRtdJr91Y6Rb0PPlpZWBIR394TgWhccnlf-vYE,6815
@@ -50,8 +50,8 @@ posthog/test/test_request.py,sha256=vrZSayStAAcTr05h2ci3Y8qmKDIV2umBdiJqhr3NPp0,
50
50
  posthog/test/test_size_limited_dict.py,sha256=Wom7BkzpHmusHilZy0SV3PNzhw7ucuQgqrx86jf8euo,765
51
51
  posthog/test/test_types.py,sha256=csLuBiz6RMV36cpg9LVIor4Khq6MfjjGxYXodx5VttY,7586
52
52
  posthog/test/test_utils.py,sha256=YqAnXaMHxzEV_D3AHhs-RXnZYzdEN7kdIlpOT6Ti6t0,9714
53
- posthog-7.5.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
54
- posthog-7.5.0.dist-info/METADATA,sha256=2UDK-C3a08KsTZZW0e7SD3CacS-zy9MRr3sYLiWiSp8,6010
55
- posthog-7.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
- posthog-7.5.0.dist-info/top_level.txt,sha256=7FBLsRjIUHVKQsXIhozuI3k-mun1tapp8iZO9EmUPEw,8
57
- posthog-7.5.0.dist-info/RECORD,,
53
+ posthog-7.5.1.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
54
+ posthog-7.5.1.dist-info/METADATA,sha256=zeyO9-gopm8VdpYOk9t0koQVd7WQaU5WwBYJuDQYF4A,6359
55
+ posthog-7.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
+ posthog-7.5.1.dist-info/top_level.txt,sha256=7FBLsRjIUHVKQsXIhozuI3k-mun1tapp8iZO9EmUPEw,8
57
+ posthog-7.5.1.dist-info/RECORD,,