posthog 7.3.1__py3-none-any.whl → 7.4.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.
- posthog/request.py +55 -6
- posthog/test/test_request.py +271 -0
- posthog/version.py +1 -1
- {posthog-7.3.1.dist-info → posthog-7.4.0.dist-info}/METADATA +1 -1
- {posthog-7.3.1.dist-info → posthog-7.4.0.dist-info}/RECORD +8 -8
- {posthog-7.3.1.dist-info → posthog-7.4.0.dist-info}/WHEEL +0 -0
- {posthog-7.3.1.dist-info → posthog-7.4.0.dist-info}/licenses/LICENSE +0 -0
- {posthog-7.3.1.dist-info → posthog-7.4.0.dist-info}/top_level.txt +0 -0
posthog/request.py
CHANGED
|
@@ -8,7 +8,6 @@ from gzip import GzipFile
|
|
|
8
8
|
from io import BytesIO
|
|
9
9
|
from typing import Any, List, Optional, Tuple, Union
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
import requests
|
|
13
12
|
from dateutil.tz import tzutc
|
|
14
13
|
from requests.adapters import HTTPAdapter # type: ignore[import-untyped]
|
|
@@ -42,6 +41,9 @@ for attr, value in [
|
|
|
42
41
|
if hasattr(socket, attr):
|
|
43
42
|
KEEP_ALIVE_SOCKET_OPTIONS.append((socket.SOL_TCP, getattr(socket, attr), value))
|
|
44
43
|
|
|
44
|
+
# Status codes that indicate transient server errors worth retrying
|
|
45
|
+
RETRY_STATUS_FORCELIST = [408, 500, 502, 503, 504]
|
|
46
|
+
|
|
45
47
|
|
|
46
48
|
def _mask_tokens_in_url(url: str) -> str:
|
|
47
49
|
"""Mask token values in URLs for safe logging, keeping first 10 chars visible."""
|
|
@@ -71,20 +73,49 @@ class HTTPAdapterWithSocketOptions(HTTPAdapter):
|
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
def _build_session(socket_options: Optional[SocketOptions] = None) -> requests.Session:
|
|
76
|
+
"""Build a session for general requests (batch, decide, etc.)."""
|
|
77
|
+
adapter = HTTPAdapterWithSocketOptions(
|
|
78
|
+
max_retries=Retry(
|
|
79
|
+
total=2,
|
|
80
|
+
connect=2,
|
|
81
|
+
read=2,
|
|
82
|
+
),
|
|
83
|
+
socket_options=socket_options,
|
|
84
|
+
)
|
|
85
|
+
session = requests.Session()
|
|
86
|
+
session.mount("https://", adapter)
|
|
87
|
+
return session
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _build_flags_session(
|
|
91
|
+
socket_options: Optional[SocketOptions] = None,
|
|
92
|
+
) -> requests.Session:
|
|
93
|
+
"""
|
|
94
|
+
Build a session for feature flag requests with POST retries.
|
|
95
|
+
|
|
96
|
+
Feature flag requests are idempotent (read-only), so retrying POST
|
|
97
|
+
requests is safe. This session retries on transient server errors
|
|
98
|
+
(408, 5xx) and network failures with exponential backoff
|
|
99
|
+
(0.5s, 1s delays between retries).
|
|
100
|
+
"""
|
|
74
101
|
adapter = HTTPAdapterWithSocketOptions(
|
|
75
102
|
max_retries=Retry(
|
|
76
103
|
total=2,
|
|
77
104
|
connect=2,
|
|
78
105
|
read=2,
|
|
106
|
+
backoff_factor=0.5,
|
|
107
|
+
status_forcelist=RETRY_STATUS_FORCELIST,
|
|
108
|
+
allowed_methods=["POST"],
|
|
79
109
|
),
|
|
80
110
|
socket_options=socket_options,
|
|
81
111
|
)
|
|
82
|
-
session = requests.
|
|
112
|
+
session = requests.Session()
|
|
83
113
|
session.mount("https://", adapter)
|
|
84
114
|
return session
|
|
85
115
|
|
|
86
116
|
|
|
87
117
|
_session = _build_session()
|
|
118
|
+
_flags_session = _build_flags_session()
|
|
88
119
|
_socket_options: Optional[SocketOptions] = None
|
|
89
120
|
_pooling_enabled = True
|
|
90
121
|
|
|
@@ -95,6 +126,12 @@ def _get_session() -> requests.Session:
|
|
|
95
126
|
return _build_session(_socket_options)
|
|
96
127
|
|
|
97
128
|
|
|
129
|
+
def _get_flags_session() -> requests.Session:
|
|
130
|
+
if _pooling_enabled:
|
|
131
|
+
return _flags_session
|
|
132
|
+
return _build_flags_session(_socket_options)
|
|
133
|
+
|
|
134
|
+
|
|
98
135
|
def set_socket_options(socket_options: Optional[SocketOptions]) -> None:
|
|
99
136
|
"""
|
|
100
137
|
Configure socket options for all HTTP connections.
|
|
@@ -103,11 +140,12 @@ def set_socket_options(socket_options: Optional[SocketOptions]) -> None:
|
|
|
103
140
|
from posthog import set_socket_options
|
|
104
141
|
set_socket_options([(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)])
|
|
105
142
|
"""
|
|
106
|
-
global _session, _socket_options
|
|
143
|
+
global _session, _flags_session, _socket_options
|
|
107
144
|
if socket_options == _socket_options:
|
|
108
145
|
return
|
|
109
146
|
_socket_options = socket_options
|
|
110
147
|
_session = _build_session(socket_options)
|
|
148
|
+
_flags_session = _build_flags_session(socket_options)
|
|
111
149
|
|
|
112
150
|
|
|
113
151
|
def enable_keep_alive() -> None:
|
|
@@ -145,6 +183,7 @@ def post(
|
|
|
145
183
|
path=None,
|
|
146
184
|
gzip: bool = False,
|
|
147
185
|
timeout: int = 15,
|
|
186
|
+
session: Optional[requests.Session] = None,
|
|
148
187
|
**kwargs,
|
|
149
188
|
) -> requests.Response:
|
|
150
189
|
"""Post the `kwargs` to the API"""
|
|
@@ -165,7 +204,9 @@ def post(
|
|
|
165
204
|
gz.write(data.encode("utf-8"))
|
|
166
205
|
data = buf.getvalue()
|
|
167
206
|
|
|
168
|
-
res = _get_session().post(
|
|
207
|
+
res = (session or _get_session()).post(
|
|
208
|
+
url, data=data, headers=headers, timeout=timeout
|
|
209
|
+
)
|
|
169
210
|
|
|
170
211
|
if res.status_code == 200:
|
|
171
212
|
log.debug("data uploaded successfully")
|
|
@@ -221,8 +262,16 @@ def flags(
|
|
|
221
262
|
timeout: int = 15,
|
|
222
263
|
**kwargs,
|
|
223
264
|
) -> Any:
|
|
224
|
-
"""Post the
|
|
225
|
-
res = post(
|
|
265
|
+
"""Post the kwargs to the flags API endpoint with automatic retries."""
|
|
266
|
+
res = post(
|
|
267
|
+
api_key,
|
|
268
|
+
host,
|
|
269
|
+
"/flags/?v=2",
|
|
270
|
+
gzip,
|
|
271
|
+
timeout,
|
|
272
|
+
session=_get_flags_session(),
|
|
273
|
+
**kwargs,
|
|
274
|
+
)
|
|
226
275
|
return _process_response(
|
|
227
276
|
res, success_message="Feature flags evaluated successfully"
|
|
228
277
|
)
|
posthog/test/test_request.py
CHANGED
|
@@ -19,6 +19,7 @@ from posthog.request import (
|
|
|
19
19
|
determine_server_host,
|
|
20
20
|
disable_connection_reuse,
|
|
21
21
|
enable_keep_alive,
|
|
22
|
+
flags,
|
|
22
23
|
get,
|
|
23
24
|
set_socket_options,
|
|
24
25
|
)
|
|
@@ -393,3 +394,273 @@ def test_set_socket_options_is_idempotent():
|
|
|
393
394
|
assert session1 is session2
|
|
394
395
|
finally:
|
|
395
396
|
set_socket_options(None)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class TestFlagsSession(unittest.TestCase):
|
|
400
|
+
"""Tests for flags session configuration."""
|
|
401
|
+
|
|
402
|
+
def test_retry_status_forcelist_excludes_rate_limits(self):
|
|
403
|
+
"""Verify 429 (rate limit) is NOT retried - need to wait, not hammer."""
|
|
404
|
+
from posthog.request import RETRY_STATUS_FORCELIST
|
|
405
|
+
|
|
406
|
+
self.assertNotIn(429, RETRY_STATUS_FORCELIST)
|
|
407
|
+
|
|
408
|
+
def test_retry_status_forcelist_excludes_quota_errors(self):
|
|
409
|
+
"""Verify 402 (payment required/quota) is NOT retried - won't resolve."""
|
|
410
|
+
from posthog.request import RETRY_STATUS_FORCELIST
|
|
411
|
+
|
|
412
|
+
self.assertNotIn(402, RETRY_STATUS_FORCELIST)
|
|
413
|
+
|
|
414
|
+
@mock.patch("posthog.request._get_flags_session")
|
|
415
|
+
def test_flags_uses_flags_session(self, mock_get_flags_session):
|
|
416
|
+
"""flags() uses the dedicated flags session, not the general session."""
|
|
417
|
+
mock_response = requests.Response()
|
|
418
|
+
mock_response.status_code = 200
|
|
419
|
+
mock_response._content = json.dumps(
|
|
420
|
+
{
|
|
421
|
+
"featureFlags": {"test-flag": True},
|
|
422
|
+
"featureFlagPayloads": {},
|
|
423
|
+
"errorsWhileComputingFlags": False,
|
|
424
|
+
}
|
|
425
|
+
).encode("utf-8")
|
|
426
|
+
|
|
427
|
+
mock_session = mock.MagicMock()
|
|
428
|
+
mock_session.post.return_value = mock_response
|
|
429
|
+
mock_get_flags_session.return_value = mock_session
|
|
430
|
+
|
|
431
|
+
result = flags("test-key", "https://test.posthog.com", distinct_id="user123")
|
|
432
|
+
|
|
433
|
+
self.assertEqual(result["featureFlags"]["test-flag"], True)
|
|
434
|
+
mock_get_flags_session.assert_called_once()
|
|
435
|
+
mock_session.post.assert_called_once()
|
|
436
|
+
|
|
437
|
+
@mock.patch("posthog.request._get_flags_session")
|
|
438
|
+
def test_flags_no_retry_on_quota_limit(self, mock_get_flags_session):
|
|
439
|
+
"""flags() raises QuotaLimitError without retrying (at application level)."""
|
|
440
|
+
mock_response = requests.Response()
|
|
441
|
+
mock_response.status_code = 200
|
|
442
|
+
mock_response._content = json.dumps(
|
|
443
|
+
{
|
|
444
|
+
"quotaLimited": ["feature_flags"],
|
|
445
|
+
"featureFlags": {},
|
|
446
|
+
"featureFlagPayloads": {},
|
|
447
|
+
"errorsWhileComputingFlags": False,
|
|
448
|
+
}
|
|
449
|
+
).encode("utf-8")
|
|
450
|
+
|
|
451
|
+
mock_session = mock.MagicMock()
|
|
452
|
+
mock_session.post.return_value = mock_response
|
|
453
|
+
mock_get_flags_session.return_value = mock_session
|
|
454
|
+
|
|
455
|
+
with self.assertRaises(QuotaLimitError):
|
|
456
|
+
flags("test-key", "https://test.posthog.com", distinct_id="user123")
|
|
457
|
+
|
|
458
|
+
# QuotaLimitError is raised after response is received, not retried
|
|
459
|
+
self.assertEqual(mock_session.post.call_count, 1)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class TestFlagsSessionNetworkRetries(unittest.TestCase):
|
|
463
|
+
"""Tests for network failure retries in the flags session."""
|
|
464
|
+
|
|
465
|
+
def test_flags_session_retry_config_includes_connection_errors(self):
|
|
466
|
+
"""
|
|
467
|
+
Verify that the flags session is configured to retry on connection errors.
|
|
468
|
+
|
|
469
|
+
The urllib3 Retry adapter with connect=2 and read=2 automatically
|
|
470
|
+
retries on network-level failures (DNS failures, connection refused,
|
|
471
|
+
connection reset, etc.) up to 2 times each.
|
|
472
|
+
"""
|
|
473
|
+
from posthog.request import _build_flags_session
|
|
474
|
+
|
|
475
|
+
session = _build_flags_session()
|
|
476
|
+
|
|
477
|
+
# Get the adapter for https://
|
|
478
|
+
adapter = session.get_adapter("https://test.posthog.com")
|
|
479
|
+
|
|
480
|
+
# Verify retry configuration
|
|
481
|
+
retry = adapter.max_retries
|
|
482
|
+
self.assertEqual(retry.total, 2, "Should have 2 total retries")
|
|
483
|
+
self.assertEqual(retry.connect, 2, "Should retry connection errors twice")
|
|
484
|
+
self.assertEqual(retry.read, 2, "Should retry read errors twice")
|
|
485
|
+
self.assertIn("POST", retry.allowed_methods, "Should allow POST retries")
|
|
486
|
+
|
|
487
|
+
def test_flags_session_retries_on_server_errors(self):
|
|
488
|
+
"""
|
|
489
|
+
Verify that transient server errors (5xx) trigger retries.
|
|
490
|
+
|
|
491
|
+
This tests the status_forcelist configuration which specifies
|
|
492
|
+
which HTTP status codes should trigger a retry.
|
|
493
|
+
"""
|
|
494
|
+
from posthog.request import _build_flags_session, RETRY_STATUS_FORCELIST
|
|
495
|
+
|
|
496
|
+
session = _build_flags_session()
|
|
497
|
+
adapter = session.get_adapter("https://test.posthog.com")
|
|
498
|
+
retry = adapter.max_retries
|
|
499
|
+
|
|
500
|
+
# Verify the status codes that trigger retries
|
|
501
|
+
self.assertEqual(
|
|
502
|
+
set(retry.status_forcelist),
|
|
503
|
+
set(RETRY_STATUS_FORCELIST),
|
|
504
|
+
"Should retry on transient server errors",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Verify specific codes are included
|
|
508
|
+
self.assertIn(500, retry.status_forcelist)
|
|
509
|
+
self.assertIn(502, retry.status_forcelist)
|
|
510
|
+
self.assertIn(503, retry.status_forcelist)
|
|
511
|
+
self.assertIn(504, retry.status_forcelist)
|
|
512
|
+
|
|
513
|
+
# Verify rate limits and quota errors are NOT retried
|
|
514
|
+
self.assertNotIn(429, retry.status_forcelist)
|
|
515
|
+
self.assertNotIn(402, retry.status_forcelist)
|
|
516
|
+
|
|
517
|
+
def test_flags_session_has_backoff(self):
|
|
518
|
+
"""
|
|
519
|
+
Verify that retries use exponential backoff to avoid thundering herd.
|
|
520
|
+
"""
|
|
521
|
+
from posthog.request import _build_flags_session
|
|
522
|
+
|
|
523
|
+
session = _build_flags_session()
|
|
524
|
+
adapter = session.get_adapter("https://test.posthog.com")
|
|
525
|
+
retry = adapter.max_retries
|
|
526
|
+
|
|
527
|
+
self.assertEqual(
|
|
528
|
+
retry.backoff_factor,
|
|
529
|
+
0.5,
|
|
530
|
+
"Should use 0.5s backoff factor (0.5s, 1s delays)",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class TestFlagsSessionRetryIntegration(unittest.TestCase):
|
|
535
|
+
"""Integration tests that verify actual retry behavior with a local server."""
|
|
536
|
+
|
|
537
|
+
def test_retries_on_503_then_succeeds(self):
|
|
538
|
+
"""
|
|
539
|
+
Verify that 503 errors trigger retries and eventually succeed.
|
|
540
|
+
|
|
541
|
+
Uses a local HTTP server that fails twice with 503, then succeeds.
|
|
542
|
+
This tests the full retry flow including backoff timing.
|
|
543
|
+
"""
|
|
544
|
+
import threading
|
|
545
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
546
|
+
from socketserver import ThreadingMixIn
|
|
547
|
+
from urllib3.util.retry import Retry
|
|
548
|
+
from posthog.request import HTTPAdapterWithSocketOptions, RETRY_STATUS_FORCELIST
|
|
549
|
+
|
|
550
|
+
request_count = 0
|
|
551
|
+
|
|
552
|
+
class RetryTestHandler(BaseHTTPRequestHandler):
|
|
553
|
+
protocol_version = "HTTP/1.1"
|
|
554
|
+
|
|
555
|
+
def do_POST(self):
|
|
556
|
+
nonlocal request_count
|
|
557
|
+
request_count += 1
|
|
558
|
+
|
|
559
|
+
# Read and discard request body to prevent connection issues
|
|
560
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
561
|
+
if content_length > 0:
|
|
562
|
+
self.rfile.read(content_length)
|
|
563
|
+
|
|
564
|
+
if request_count <= 2:
|
|
565
|
+
self.send_response(503)
|
|
566
|
+
self.send_header("Content-Type", "application/json")
|
|
567
|
+
body = b'{"error": "Service unavailable"}'
|
|
568
|
+
self.send_header("Content-Length", str(len(body)))
|
|
569
|
+
self.end_headers()
|
|
570
|
+
self.wfile.write(body)
|
|
571
|
+
else:
|
|
572
|
+
self.send_response(200)
|
|
573
|
+
self.send_header("Content-Type", "application/json")
|
|
574
|
+
body = (
|
|
575
|
+
b'{"featureFlags": {"test": true}, "featureFlagPayloads": {}}'
|
|
576
|
+
)
|
|
577
|
+
self.send_header("Content-Length", str(len(body)))
|
|
578
|
+
self.end_headers()
|
|
579
|
+
self.wfile.write(body)
|
|
580
|
+
|
|
581
|
+
def log_message(self, format, *args):
|
|
582
|
+
pass # Suppress logging
|
|
583
|
+
|
|
584
|
+
# Use ThreadingMixIn for cleaner shutdown
|
|
585
|
+
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
|
586
|
+
daemon_threads = True
|
|
587
|
+
|
|
588
|
+
# Start server on a random available port
|
|
589
|
+
server = ThreadedHTTPServer(("127.0.0.1", 0), RetryTestHandler)
|
|
590
|
+
port = server.server_address[1]
|
|
591
|
+
server_thread = threading.Thread(target=server.serve_forever)
|
|
592
|
+
server_thread.daemon = True
|
|
593
|
+
server_thread.start()
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
# Build session with same retry config as _build_flags_session
|
|
597
|
+
# but mounted on http:// for local testing
|
|
598
|
+
adapter = HTTPAdapterWithSocketOptions(
|
|
599
|
+
max_retries=Retry(
|
|
600
|
+
total=2,
|
|
601
|
+
connect=2,
|
|
602
|
+
read=2,
|
|
603
|
+
backoff_factor=0.01, # Fast backoff for testing
|
|
604
|
+
status_forcelist=RETRY_STATUS_FORCELIST,
|
|
605
|
+
allowed_methods=["POST"],
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
session = requests.Session()
|
|
609
|
+
session.mount("http://", adapter)
|
|
610
|
+
|
|
611
|
+
response = session.post(
|
|
612
|
+
f"http://127.0.0.1:{port}/flags/?v=2",
|
|
613
|
+
json={"distinct_id": "user123"},
|
|
614
|
+
timeout=5,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Should succeed on 3rd attempt
|
|
618
|
+
self.assertEqual(response.status_code, 200)
|
|
619
|
+
self.assertEqual(request_count, 3) # 1 initial + 2 retries
|
|
620
|
+
finally:
|
|
621
|
+
server.shutdown()
|
|
622
|
+
server.server_close()
|
|
623
|
+
|
|
624
|
+
def test_connection_errors_are_retried(self):
|
|
625
|
+
"""
|
|
626
|
+
Verify that connection errors (no server) trigger retries.
|
|
627
|
+
|
|
628
|
+
Binds a socket to get a guaranteed available port, then closes it
|
|
629
|
+
so connection attempts fail with ConnectionError.
|
|
630
|
+
"""
|
|
631
|
+
import socket
|
|
632
|
+
import time
|
|
633
|
+
from urllib3.util.retry import Retry
|
|
634
|
+
from posthog.request import HTTPAdapterWithSocketOptions, RETRY_STATUS_FORCELIST
|
|
635
|
+
|
|
636
|
+
# Get an available port by binding then closing a socket
|
|
637
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
638
|
+
sock.bind(("127.0.0.1", 0))
|
|
639
|
+
port = sock.getsockname()[1]
|
|
640
|
+
sock.close() # Port is now available but nothing is listening
|
|
641
|
+
|
|
642
|
+
adapter = HTTPAdapterWithSocketOptions(
|
|
643
|
+
max_retries=Retry(
|
|
644
|
+
total=2,
|
|
645
|
+
connect=2,
|
|
646
|
+
read=2,
|
|
647
|
+
backoff_factor=0.05, # Very fast for testing
|
|
648
|
+
status_forcelist=RETRY_STATUS_FORCELIST,
|
|
649
|
+
allowed_methods=["POST"],
|
|
650
|
+
),
|
|
651
|
+
)
|
|
652
|
+
session = requests.Session()
|
|
653
|
+
session.mount("http://", adapter)
|
|
654
|
+
|
|
655
|
+
start = time.time()
|
|
656
|
+
with self.assertRaises(requests.exceptions.ConnectionError):
|
|
657
|
+
session.post(
|
|
658
|
+
f"http://127.0.0.1:{port}/flags/?v=2",
|
|
659
|
+
json={"distinct_id": "user123"},
|
|
660
|
+
timeout=1,
|
|
661
|
+
)
|
|
662
|
+
elapsed = time.time() - start
|
|
663
|
+
|
|
664
|
+
# With 3 attempts and backoff, should take more than instant
|
|
665
|
+
# but less than timeout (confirms retries happened)
|
|
666
|
+
self.assertGreater(elapsed, 0.05, "Should have some delay from retries")
|
posthog/version.py
CHANGED
|
@@ -9,10 +9,10 @@ posthog/feature_flags.py,sha256=4xAcYEpa97b5Lv9bIo5JHbCO6lhYBnH5EmJ2MrjbU3k,2251
|
|
|
9
9
|
posthog/flag_definition_cache.py,sha256=3LnB76BAU29FmypxagkqTpEW6MfjQQTw3E_xWJ679XQ,4697
|
|
10
10
|
posthog/poller.py,sha256=jBz5rfH_kn_bBz7wCB46Fpvso4ttx4uzqIZWvXBCFmQ,595
|
|
11
11
|
posthog/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
posthog/request.py,sha256=
|
|
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=
|
|
15
|
+
posthog/version.py,sha256=eltPOeTfTkhaA4B9Epak_jIZQQYePn2QTw1EpKwy99E,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
|
|
@@ -46,12 +46,12 @@ posthog/test/test_feature_flag_result.py,sha256=KCQHismwddFWl-PHtRpZzcL5M45q_oUe
|
|
|
46
46
|
posthog/test/test_feature_flags.py,sha256=5tfAYuzNjxgnxsQxlO7sDjmivUccTWf_26l_FEAJ3E8,222554
|
|
47
47
|
posthog/test/test_flag_definition_cache.py,sha256=_ssIKtrgNw3WsHv-GQNd_Nk-luDxAWgOVnAOMb8gWP8,21883
|
|
48
48
|
posthog/test/test_module.py,sha256=CERR0dTPGsAmd7YBxK0yKeB2Zr2b_Lv7hNQoeJauc9I,813
|
|
49
|
-
posthog/test/test_request.py,sha256=
|
|
49
|
+
posthog/test/test_request.py,sha256=vrZSayStAAcTr05h2ci3Y8qmKDIV2umBdiJqhr3NPp0,25176
|
|
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.
|
|
54
|
-
posthog-7.
|
|
55
|
-
posthog-7.
|
|
56
|
-
posthog-7.
|
|
57
|
-
posthog-7.
|
|
53
|
+
posthog-7.4.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
|
|
54
|
+
posthog-7.4.0.dist-info/METADATA,sha256=5LIZKQuKMWBWIgQ6jumdjTA3c-fHc1Si0JNCxTpZh0k,6010
|
|
55
|
+
posthog-7.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
56
|
+
posthog-7.4.0.dist-info/top_level.txt,sha256=7FBLsRjIUHVKQsXIhozuI3k-mun1tapp8iZO9EmUPEw,8
|
|
57
|
+
posthog-7.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|