posthoganalytics 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.
- posthoganalytics/consumer.py +6 -2
- posthoganalytics/test/test_consumer.py +86 -83
- posthoganalytics/version.py +1 -1
- {posthoganalytics-7.5.0.dist-info → posthoganalytics-7.5.1.dist-info}/METADATA +9 -1
- {posthoganalytics-7.5.0.dist-info → posthoganalytics-7.5.1.dist-info}/RECORD +8 -8
- {posthoganalytics-7.5.0.dist-info → posthoganalytics-7.5.1.dist-info}/WHEEL +0 -0
- {posthoganalytics-7.5.0.dist-info → posthoganalytics-7.5.1.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-7.5.0.dist-info → posthoganalytics-7.5.1.dist-info}/top_level.txt +0 -0
posthoganalytics/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
|
-
|
|
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
|
-
|
|
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 posthoganalytics.request import APIError
|
|
|
14
16
|
from posthoganalytics.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
|
-
|
|
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(
|
|
61
|
-
|
|
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(
|
|
82
|
-
|
|
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
|
-
|
|
94
|
-
consumer.request([track])
|
|
88
|
+
consumer.request([_track_event()])
|
|
95
89
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
self.
|
|
138
|
-
|
|
139
|
-
|
|
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])
|
posthoganalytics/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: posthoganalytics
|
|
3
|
-
Version: 7.5.
|
|
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
|
posthoganalytics/__init__.py,sha256=SARrZDVM5I5v1ZGZaA4Ht6iTkle-Mfu4GE-rndMcU-M,29069
|
|
2
2
|
posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
|
|
3
3
|
posthoganalytics/client.py,sha256=_I7EqllI9WxqKL_9J0Fy532W71bb45z0ZoT6lEANw-E,82368
|
|
4
|
-
posthoganalytics/consumer.py,sha256=
|
|
4
|
+
posthoganalytics/consumer.py,sha256=vfc5KrCaWTwyizH0oxAH6gFBtbRwpSk_YMxqzSPoCNg,4747
|
|
5
5
|
posthoganalytics/contexts.py,sha256=Y-31Wq2HF6erB8FFPCSyMJH170lz9mBBpgSxkxBdy6w,12552
|
|
6
6
|
posthoganalytics/exception_capture.py,sha256=1VHBfffrXXrkK0PT8iVgKPpj_R1pGAzG5f3Qw0WF79w,1783
|
|
7
7
|
posthoganalytics/exception_utils.py,sha256=dO2XYCl78db-4_FHONS9aOybOnxEI5poX3XkfLthXgo,33747
|
|
@@ -12,7 +12,7 @@ posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
12
12
|
posthoganalytics/request.py,sha256=sv5dVU4jg4nFI4BpGCGvyoiz6yZZBLMqoBjhmzoomYo,11844
|
|
13
13
|
posthoganalytics/types.py,sha256=OxGHSmmhVYwA7ecmJXUznDCZ1c4gAGtERzSLSYlyQFM,11540
|
|
14
14
|
posthoganalytics/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
|
|
15
|
-
posthoganalytics/version.py,sha256=
|
|
15
|
+
posthoganalytics/version.py,sha256=LMvATKqkjlQkAH72PmLuWIk_lEtgTE3LQljPg7fcOLo,87
|
|
16
16
|
posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
posthoganalytics/ai/sanitization.py,sha256=Dpx_5gKZfDS38KjmK1C0lvvjm9N8Pp_oIxusac888-g,6057
|
|
18
18
|
posthoganalytics/ai/types.py,sha256=arX98hR1PIPeJ3vFikxTlACIh1xPp6aEUw1gBLcKoB0,3273
|
|
@@ -38,7 +38,7 @@ posthoganalytics/integrations/django.py,sha256=aJ_fLjeMqnnF01Zp8N3c9OeXvWwDL_X_o
|
|
|
38
38
|
posthoganalytics/test/__init__.py,sha256=VYgM6xPbJbvS-xhIcDiBRs0MFC9V_jT65uNeerCz_rM,299
|
|
39
39
|
posthoganalytics/test/test_before_send.py,sha256=A1_UVMewhHAvO39rZDWfS606vG_X-q0KNXvh5DAKiB8,7930
|
|
40
40
|
posthoganalytics/test/test_client.py,sha256=p6ZX8-pyfMhaU_BAoPTZHnpkbJ5TRSwQi_r4C-kpCHY,102931
|
|
41
|
-
posthoganalytics/test/test_consumer.py,sha256=
|
|
41
|
+
posthoganalytics/test/test_consumer.py,sha256=OLCHnqZW1C-1KQptwpRmOJPL_mPRxo4G0Gr7v0ZGAIM,7018
|
|
42
42
|
posthoganalytics/test/test_contexts.py,sha256=Mb0XRMQNCdkK7lKWMPMc9fv1QCWvZzj8eqTd6SuD6C8,8104
|
|
43
43
|
posthoganalytics/test/test_exception_capture.py,sha256=s_hUphovgQoGeOwtElAJiZFxgGrvaNRhgSqLU9ZaBhc,13368
|
|
44
44
|
posthoganalytics/test/test_feature_flag.py,sha256=9RQwB5eCvVAGrrO7UnR3Z1OidP_YoL4iBl3A83fuAig,6824
|
|
@@ -50,8 +50,8 @@ posthoganalytics/test/test_request.py,sha256=1wdNXbSpTVieYMAzEwicahvk6RfvvTiRdqG
|
|
|
50
50
|
posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
|
|
51
51
|
posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
|
|
52
52
|
posthoganalytics/test/test_utils.py,sha256=MTz7-Fvffz2a9IRwyKsVy_TnrvIihs-Ap3hhtqGSSAs,9732
|
|
53
|
-
posthoganalytics-7.5.
|
|
54
|
-
posthoganalytics-7.5.
|
|
55
|
-
posthoganalytics-7.5.
|
|
56
|
-
posthoganalytics-7.5.
|
|
57
|
-
posthoganalytics-7.5.
|
|
53
|
+
posthoganalytics-7.5.1.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
|
|
54
|
+
posthoganalytics-7.5.1.dist-info/METADATA,sha256=XoWO8W1S_YGJLiXyHZvxHR7KJs7sSIkgySGQPEba8dw,6368
|
|
55
|
+
posthoganalytics-7.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
56
|
+
posthoganalytics-7.5.1.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
|
|
57
|
+
posthoganalytics-7.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|