chore: 添加虚拟环境到仓库

- 添加 backend_service/venv 虚拟环境
- 包含所有Python依赖包
- 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
2025-12-03 10:19:25 +08:00
parent a6c2027caa
commit c4f851d387
12655 changed files with 3009376 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import logging
import pkgutil
import sys
import unittest
def all_names():
for _, modname, _ in pkgutil.iter_modules(__path__):
yield "posthog.test." + modname
def all():
logging.basicConfig(stream=sys.stderr)
return unittest.defaultTestLoader.loadTestsFromNames(all_names())

View File

@@ -0,0 +1,171 @@
import unittest
import mock
from posthog.client import Client
from posthog.test.test_utils import FAKE_TEST_API_KEY
class TestClient(unittest.TestCase):
@classmethod
def setUpClass(cls):
# This ensures no real HTTP POST requests are made
cls.client_post_patcher = mock.patch("posthog.client.batch_post")
cls.consumer_post_patcher = mock.patch("posthog.consumer.batch_post")
cls.client_post_patcher.start()
cls.consumer_post_patcher.start()
@classmethod
def tearDownClass(cls):
cls.client_post_patcher.stop()
cls.consumer_post_patcher.stop()
def set_fail(self, e, batch):
"""Mark the failure handler"""
print("FAIL", e, batch) # noqa: T201
self.failed = True
def setUp(self):
self.failed = False
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
def test_before_send_callback_modifies_event(self):
"""Test that before_send callback can modify events."""
processed_events = []
def my_before_send(event):
processed_events.append(event.copy())
if "properties" not in event:
event["properties"] = {}
event["properties"]["processed_by_before_send"] = True
return event
client = Client(
FAKE_TEST_API_KEY, on_error=self.set_fail, before_send=my_before_send
)
success, msg = client.capture("user1", "test_event", {"original": "value"})
self.assertTrue(success)
self.assertEqual(msg["properties"]["processed_by_before_send"], True)
self.assertEqual(msg["properties"]["original"], "value")
self.assertEqual(len(processed_events), 1)
self.assertEqual(processed_events[0]["event"], "test_event")
def test_before_send_callback_drops_event(self):
"""Test that before_send callback can drop events by returning None."""
def drop_test_events(event):
if event.get("event") == "test_drop_me":
return None
return event
client = Client(
FAKE_TEST_API_KEY, on_error=self.set_fail, before_send=drop_test_events
)
# Event should be dropped
success, msg = client.capture("user1", "test_drop_me")
self.assertTrue(success)
self.assertIsNone(msg)
# Event should go through
success, msg = client.capture("user1", "keep_me")
self.assertTrue(success)
self.assertIsNotNone(msg)
self.assertEqual(msg["event"], "keep_me")
def test_before_send_callback_handles_exceptions(self):
"""Test that exceptions in before_send don't crash the client."""
def buggy_before_send(event):
raise ValueError("Oops!")
client = Client(
FAKE_TEST_API_KEY, on_error=self.set_fail, before_send=buggy_before_send
)
success, msg = client.capture("user1", "robust_event")
# Event should still be sent despite the exception
self.assertTrue(success)
self.assertIsNotNone(msg)
self.assertEqual(msg["event"], "robust_event")
def test_before_send_callback_works_with_all_event_types(self):
"""Test that before_send works with capture, identify, set, etc."""
def add_marker(event):
if "properties" not in event:
event["properties"] = {}
event["properties"]["marked"] = True
return event
client = Client(
FAKE_TEST_API_KEY, on_error=self.set_fail, before_send=add_marker
)
# Test capture
success, msg = client.capture("user1", "event")
self.assertTrue(success)
self.assertTrue(msg["properties"]["marked"])
# Test identify
success, msg = client.identify("user1", {"trait": "value"})
self.assertTrue(success)
self.assertTrue(msg["properties"]["marked"])
# Test set
success, msg = client.set("user1", {"prop": "value"})
self.assertTrue(success)
self.assertTrue(msg["properties"]["marked"])
# Test page
success, msg = client.page("user1", "https://example.com")
self.assertTrue(success)
self.assertTrue(msg["properties"]["marked"])
def test_before_send_callback_disabled_when_none(self):
"""Test that client works normally when before_send is None."""
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, before_send=None)
success, msg = client.capture("user1", "normal_event")
self.assertTrue(success)
self.assertIsNotNone(msg)
self.assertEqual(msg["event"], "normal_event")
def test_before_send_callback_pii_scrubbing_example(self):
"""Test a realistic PII scrubbing use case."""
def scrub_pii(event):
properties = event.get("properties", {})
# Mask email but keep domain
if "email" in properties:
email = properties["email"]
if "@" in email:
domain = email.split("@")[1]
properties["email"] = f"***@{domain}"
else:
properties["email"] = "***"
# Remove credit card
properties.pop("credit_card", None)
return event
client = Client(
FAKE_TEST_API_KEY, on_error=self.set_fail, before_send=scrub_pii
)
success, msg = client.capture(
"user1",
"form_submit",
{
"email": "user@example.com",
"credit_card": "1234-5678-9012-3456",
"form_name": "contact",
},
)
self.assertTrue(success)
self.assertEqual(msg["properties"]["email"], "***@example.com")
self.assertNotIn("credit_card", msg["properties"])
self.assertEqual(msg["properties"]["form_name"], "contact")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,196 @@
import json
import time
import unittest
import mock
try:
from queue import Queue
except ImportError:
from Queue import Queue
from posthog.consumer import MAX_MSG_SIZE, Consumer
from posthog.request import APIError
from posthog.test.test_utils import TEST_API_KEY
class TestConsumer(unittest.TestCase):
def test_next(self):
q = Queue()
consumer = Consumer(q, "")
q.put(1)
next = consumer.next()
self.assertEqual(next, [1])
def test_next_limit(self):
q = Queue()
flush_at = 50
consumer = Consumer(q, "", flush_at)
for i in range(10000):
q.put(i)
next = consumer.next()
self.assertEqual(next, list(range(flush_at)))
def test_dropping_oversize_msg(self):
q = Queue()
consumer = Consumer(q, "")
oversize_msg = {"m": "x" * MAX_MSG_SIZE}
q.put(oversize_msg)
next = consumer.next()
self.assertEqual(next, [])
self.assertTrue(q.empty())
def test_upload(self):
q = Queue()
consumer = Consumer(q, TEST_API_KEY)
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
q.put(track)
success = consumer.upload()
self.assertTrue(success)
def test_flush_interval(self):
# Put _n_ items in the queue, pausing a little bit more than
# _flush_interval_ after each one.
# The consumer should upload _n_ times.
q = Queue()
flush_interval = 0.3
consumer = Consumer(q, TEST_API_KEY, flush_at=10, flush_interval=flush_interval)
with mock.patch("posthog.consumer.batch_post") as mock_post:
consumer.start()
for i in range(0, 3):
track = {
"type": "track",
"event": "python event %d" % i,
"distinct_id": "distinct_id",
}
q.put(track)
time.sleep(flush_interval * 1.1)
self.assertEqual(mock_post.call_count, 3)
def test_multiple_uploads_per_interval(self):
# Put _flush_at*2_ items in the queue at once, then pause for
# _flush_interval_. The consumer should upload 2 times.
q = Queue()
flush_interval = 0.5
flush_at = 10
consumer = Consumer(
q, TEST_API_KEY, flush_at=flush_at, flush_interval=flush_interval
)
with mock.patch("posthog.consumer.batch_post") as mock_post:
consumer.start()
for i in range(0, flush_at * 2):
track = {
"type": "track",
"event": "python event %d" % i,
"distinct_id": "distinct_id",
}
q.put(track)
time.sleep(flush_interval * 1.1)
self.assertEqual(mock_post.call_count, 2)
def test_request(self):
consumer = Consumer(None, TEST_API_KEY)
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
consumer.request([track])
def _test_request_retry(self, consumer, expected_exception, exception_count):
def mock_post(*args, **kwargs):
mock_post.call_count += 1
if mock_post.call_count <= exception_count:
raise expected_exception
mock_post.call_count = 0
with mock.patch(
"posthog.consumer.batch_post", mock.Mock(side_effect=mock_post)
):
track = {
"type": "track",
"event": "python event",
"distinct_id": "distinct_id",
}
# request() should succeed if the number of exceptions raised is
# less than the retries paramater.
if exception_count <= consumer.retries:
consumer.request([track])
else:
# if exceptions are raised more times than the retries
# parameter, we expect the exception to be returned to
# the caller.
try:
consumer.request([track])
except type(expected_exception) as exc:
self.assertEqual(exc, expected_exception)
else:
self.fail(
"request() should raise an exception if still failing after %d retries"
% consumer.retries
)
def test_request_retry(self):
# we should retry on general errors
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, Exception("generic exception"), 2)
# we should retry on server errors
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 2)
# we should retry on HTTP 429 errors
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, APIError(429, "Too Many Requests"), 2)
# we should NOT retry on other client errors
consumer = Consumer(None, TEST_API_KEY)
api_error = APIError(400, "Client Errors")
try:
self._test_request_retry(consumer, api_error, 1)
except APIError:
pass
else:
self.fail("request() should not retry on client errors")
# test for number of exceptions raise > retries value
consumer = Consumer(None, TEST_API_KEY, retries=3)
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 3)
def test_pause(self):
consumer = Consumer(None, TEST_API_KEY)
consumer.pause()
self.assertFalse(consumer.running)
def test_max_batch_size(self):
q = Queue()
consumer = Consumer(q, TEST_API_KEY, flush_at=100000, flush_interval=3)
properties = {}
for n in range(0, 500):
properties[str(n)] = "one_long_property_value_to_build_a_big_event"
track = {
"type": "track",
"event": "python event",
"distinct_id": "distinct_id",
"properties": properties,
}
msg_size = len(json.dumps(track).encode())
# Let's capture 8MB of data to trigger two batches
n_msgs = int(8_000_000 / msg_size)
def mock_post_fn(_, data, **kwargs):
res = mock.Mock()
res.status_code = 200
request_size = len(data.encode())
# Batches close after the first message bringing it bigger than BATCH_SIZE_LIMIT, let's add 10% of margin
self.assertTrue(
request_size < (5 * 1024 * 1024) * 1.1,
"batch size (%d) higher than limit" % request_size,
)
return res
with mock.patch(
"posthog.request._session.post", side_effect=mock_post_fn
) as mock_post:
consumer.start()
for _ in range(0, n_msgs + 2):
q.put(track)
q.join()
self.assertEqual(mock_post.call_count, 2)

View File

@@ -0,0 +1,63 @@
import subprocess
import sys
from textwrap import dedent
import pytest
def test_excepthook(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
from posthog import Posthog
posthog = Posthog('phc_x', host='https://eu.i.posthog.com', enable_exception_autocapture=True, debug=True, on_error=lambda e, batch: print('error handling batch: ', e, batch))
# frame_value = "LOL"
1/0
"""
)
)
with pytest.raises(subprocess.CalledProcessError) as excinfo:
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
output = excinfo.value.output
assert b"ZeroDivisionError" in output
assert b"LOL" in output
assert b"DEBUG:posthog:data uploaded successfully" in output
assert (
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "app.py", "abs_path"'
in output
)
def test_trying_to_use_django_integration(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
from posthog import Posthog, Integrations
posthog = Posthog('phc_x', host='https://eu.i.posthog.com', enable_exception_autocapture=True, exception_autocapture_integrations=[Integrations.Django], debug=True, on_error=lambda e, batch: print('error handling batch: ', e, batch))
# frame_value = "LOL"
1/0
"""
)
)
with pytest.raises(subprocess.CalledProcessError) as excinfo:
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
output = excinfo.value.output
assert b"ZeroDivisionError" in output
assert b"LOL" in output
assert b"DEBUG:posthog:data uploaded successfully" in output
assert (
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "app.py", "abs_path"'
in output
)

View File

@@ -0,0 +1,189 @@
import unittest
from posthog.types import FeatureFlag, FlagMetadata, FlagReason, LegacyFlagMetadata
class TestFeatureFlag(unittest.TestCase):
def test_feature_flag_from_json(self):
# Test with full metadata
resp = {
"key": "test-flag",
"enabled": True,
"variant": "test-variant",
"reason": {
"code": "matched_condition",
"condition_index": 0,
"description": "Matched condition set 1",
},
"metadata": {
"id": 1,
"payload": '{"some": "json"}',
"version": 2,
"description": "test-description",
},
}
flag = FeatureFlag.from_json(resp)
self.assertEqual(flag.key, "test-flag")
self.assertTrue(flag.enabled)
self.assertEqual(flag.variant, "test-variant")
self.assertEqual(flag.get_value(), "test-variant")
self.assertEqual(
flag.reason,
FlagReason(
code="matched_condition",
condition_index=0,
description="Matched condition set 1",
),
)
self.assertEqual(
flag.metadata,
FlagMetadata(
id=1,
payload='{"some": "json"}',
version=2,
description="test-description",
),
)
def test_feature_flag_from_json_minimal(self):
# Test with minimal required fields
resp = {"key": "test-flag", "enabled": True}
flag = FeatureFlag.from_json(resp)
self.assertEqual(flag.key, "test-flag")
self.assertTrue(flag.enabled)
self.assertIsNone(flag.variant)
self.assertEqual(flag.get_value(), True)
self.assertIsNone(flag.reason)
self.assertEqual(flag.metadata, LegacyFlagMetadata(payload=None))
def test_feature_flag_from_json_without_metadata(self):
# Test with reason but no metadata
resp = {
"key": "test-flag",
"enabled": True,
"variant": "test-variant",
"reason": {
"code": "matched_condition",
"condition_index": 0,
"description": "Matched condition set 1",
},
}
flag = FeatureFlag.from_json(resp)
self.assertEqual(flag.key, "test-flag")
self.assertTrue(flag.enabled)
self.assertEqual(flag.variant, "test-variant")
self.assertEqual(flag.get_value(), "test-variant")
self.assertEqual(
flag.reason,
FlagReason(
code="matched_condition",
condition_index=0,
description="Matched condition set 1",
),
)
self.assertEqual(flag.metadata, LegacyFlagMetadata(payload=None))
def test_flag_reason_from_json(self):
# Test with complete data
resp = {
"code": "user_in_segment",
"condition_index": 1,
"description": "User is in segment 'beta_users'",
}
reason = FlagReason.from_json(resp)
self.assertEqual(reason.code, "user_in_segment")
self.assertEqual(reason.condition_index, 1)
self.assertEqual(reason.description, "User is in segment 'beta_users'")
# Test with partial data
resp = {"code": "user_in_segment"}
reason = FlagReason.from_json(resp)
self.assertEqual(reason.code, "user_in_segment")
self.assertIsNone(reason.condition_index) # default value
self.assertEqual(reason.description, "")
# Test with None
self.assertIsNone(FlagReason.from_json(None))
def test_flag_metadata_from_json(self):
# Test with complete data
resp = {
"id": 123,
"payload": {"key": "value"},
"version": 1,
"description": "Test flag",
}
metadata = FlagMetadata.from_json(resp)
self.assertEqual(metadata.id, 123)
self.assertEqual(metadata.payload, {"key": "value"})
self.assertEqual(metadata.version, 1)
self.assertEqual(metadata.description, "Test flag")
# Test with partial data
resp = {"id": 123}
metadata = FlagMetadata.from_json(resp)
self.assertEqual(metadata.id, 123)
self.assertIsNone(metadata.payload)
self.assertEqual(metadata.version, 0) # default value
self.assertEqual(metadata.description, "") # default value
# Test with None
self.assertIsInstance(FlagMetadata.from_json(None), LegacyFlagMetadata)
def test_feature_flag_from_json_complete(self):
# Test with complete data
resp = {
"key": "test-flag",
"enabled": True,
"variant": "control",
"reason": {
"code": "user_in_segment",
"condition_index": 1,
"description": "User is in segment 'beta_users'",
},
"metadata": {
"id": 123,
"payload": {"key": "value"},
"version": 1,
"description": "Test flag",
},
}
flag = FeatureFlag.from_json(resp)
self.assertEqual(flag.key, "test-flag")
self.assertTrue(flag.enabled)
self.assertEqual(flag.variant, "control")
self.assertIsInstance(flag.reason, FlagReason)
self.assertEqual(flag.reason.code, "user_in_segment")
self.assertIsInstance(flag.metadata, FlagMetadata)
self.assertEqual(flag.metadata.id, 123)
self.assertEqual(flag.metadata.payload, {"key": "value"})
def test_feature_flag_from_json_minimal_data(self):
# Test with minimal data
resp = {"key": "test-flag", "enabled": False}
flag = FeatureFlag.from_json(resp)
self.assertEqual(flag.key, "test-flag")
self.assertFalse(flag.enabled)
self.assertIsNone(flag.variant)
self.assertIsNone(flag.reason)
self.assertIsInstance(flag.metadata, LegacyFlagMetadata)
self.assertIsNone(flag.metadata.payload)
def test_feature_flag_from_json_with_reason(self):
# Test with reason but no metadata
resp = {
"key": "test-flag",
"enabled": True,
"reason": {"code": "user_in_segment"},
}
flag = FeatureFlag.from_json(resp)
self.assertEqual(flag.key, "test-flag")
self.assertTrue(flag.enabled)
self.assertIsNone(flag.variant)
self.assertIsInstance(flag.reason, FlagReason)
self.assertEqual(flag.reason.code, "user_in_segment")
self.assertIsInstance(flag.metadata, LegacyFlagMetadata)
self.assertIsNone(flag.metadata.payload)

View File

@@ -0,0 +1,444 @@
import unittest
import mock
from posthog.client import Client
from posthog.test.test_utils import FAKE_TEST_API_KEY
from posthog.types import FeatureFlag, FeatureFlagResult, FlagMetadata, FlagReason
class TestFeatureFlagResult(unittest.TestCase):
def test_from_bool_value_and_payload(self):
result = FeatureFlagResult.from_value_and_payload(
"test-flag", True, "[1, 2, 3]"
)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, True)
self.assertEqual(result.variant, None)
self.assertEqual(result.payload, [1, 2, 3])
def test_from_false_value_and_payload(self):
result = FeatureFlagResult.from_value_and_payload(
"test-flag", False, '{"some": "value"}'
)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, False)
self.assertEqual(result.variant, None)
self.assertEqual(result.payload, {"some": "value"})
def test_from_variant_value_and_payload(self):
result = FeatureFlagResult.from_value_and_payload(
"test-flag", "control", "true"
)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, True)
self.assertEqual(result.variant, "control")
self.assertEqual(result.payload, True)
def test_from_none_value_and_payload(self):
result = FeatureFlagResult.from_value_and_payload(
"test-flag", None, '{"some": "value"}'
)
self.assertIsNone(result)
def test_from_boolean_flag_details(self):
flag_details = FeatureFlag(
key="test-flag",
enabled=True,
variant=None,
metadata=FlagMetadata(
id=1, version=1, description="test-flag", payload='"Some string"'
),
reason=FlagReason(
code="test-reason", description="test-reason", condition_index=0
),
)
result = FeatureFlagResult.from_flag_details(flag_details)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, True)
self.assertEqual(result.variant, None)
self.assertEqual(result.payload, "Some string")
def test_from_boolean_flag_details_with_override_variant_match_value(self):
flag_details = FeatureFlag(
key="test-flag",
enabled=True,
variant=None,
metadata=FlagMetadata(
id=1, version=1, description="test-flag", payload='"Some string"'
),
reason=FlagReason(
code="test-reason", description="test-reason", condition_index=0
),
)
result = FeatureFlagResult.from_flag_details(
flag_details, override_match_value="control"
)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, True)
self.assertEqual(result.variant, "control")
self.assertEqual(result.payload, "Some string")
def test_from_boolean_flag_details_with_override_boolean_match_value(self):
flag_details = FeatureFlag(
key="test-flag",
enabled=True,
variant="control",
metadata=FlagMetadata(
id=1, version=1, description="test-flag", payload='{"some": "value"}'
),
reason=FlagReason(
code="test-reason", description="test-reason", condition_index=0
),
)
result = FeatureFlagResult.from_flag_details(
flag_details, override_match_value=True
)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, True)
self.assertEqual(result.variant, None)
self.assertEqual(result.payload, {"some": "value"})
def test_from_boolean_flag_details_with_override_false_match_value(self):
flag_details = FeatureFlag(
key="test-flag",
enabled=True,
variant="control",
metadata=FlagMetadata(
id=1, version=1, description="test-flag", payload='{"some": "value"}'
),
reason=FlagReason(
code="test-reason", description="test-reason", condition_index=0
),
)
result = FeatureFlagResult.from_flag_details(
flag_details, override_match_value=False
)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, False)
self.assertEqual(result.variant, None)
self.assertEqual(result.payload, {"some": "value"})
def test_from_variant_flag_details(self):
flag_details = FeatureFlag(
key="test-flag",
enabled=True,
variant="control",
metadata=FlagMetadata(
id=1, version=1, description="test-flag", payload='{"some": "value"}'
),
reason=FlagReason(
code="test-reason", description="test-reason", condition_index=0
),
)
result = FeatureFlagResult.from_flag_details(flag_details)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, True)
self.assertEqual(result.variant, "control")
self.assertEqual(result.payload, {"some": "value"})
def test_from_none_flag_details(self):
result = FeatureFlagResult.from_flag_details(None)
self.assertIsNone(result)
def test_from_flag_details_with_none_payload(self):
flag_details = FeatureFlag(
key="test-flag",
enabled=True,
variant=None,
metadata=FlagMetadata(
id=1, version=1, description="test-flag", payload=None
),
reason=FlagReason(
code="test-reason", description="test-reason", condition_index=0
),
)
result = FeatureFlagResult.from_flag_details(flag_details)
self.assertEqual(result.key, "test-flag")
self.assertEqual(result.enabled, True)
self.assertEqual(result.variant, None)
self.assertIsNone(result.payload)
class TestGetFeatureFlagResult(unittest.TestCase):
@classmethod
def setUpClass(cls):
# This ensures no real HTTP POST requests are made
cls.capture_patch = mock.patch.object(Client, "capture")
cls.capture_patch.start()
@classmethod
def tearDownClass(cls):
cls.capture_patch.stop()
def set_fail(self, e, batch):
"""Mark the failure handler"""
print("FAIL", e, batch) # noqa: T201
self.failed = True
def setUp(self):
self.failed = False
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
@mock.patch.object(Client, "capture")
def test_get_feature_flag_result_boolean_local_evaluation(self, patch_capture):
basic_flag = {
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
}
],
"rollout_percentage": 100,
}
],
"payloads": {"true": "300"},
},
}
self.client.feature_flags = [basic_flag]
flag_result = self.client.get_feature_flag_result(
"person-flag", "some-distinct-id", person_properties={"region": "USA"}
)
self.assertEqual(flag_result.enabled, True)
self.assertEqual(flag_result.variant, None)
self.assertEqual(flag_result.payload, 300)
patch_capture.assert_called_with(
"some-distinct-id",
"$feature_flag_called",
{
"$feature_flag": "person-flag",
"$feature_flag_response": True,
"locally_evaluated": True,
"$feature/person-flag": True,
"$feature_flag_payload": 300,
},
groups={},
disable_geoip=None,
)
@mock.patch.object(Client, "capture")
def test_get_feature_flag_result_variant_local_evaluation(self, patch_capture):
basic_flag = {
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
}
],
"rollout_percentage": 100,
}
],
"multivariate": {
"variants": [
{"key": "variant-1", "rollout_percentage": 50},
{"key": "variant-2", "rollout_percentage": 50},
]
},
"payloads": {"variant-1": '{"some": "value"}'},
},
}
self.client.feature_flags = [basic_flag]
flag_result = self.client.get_feature_flag_result(
"person-flag", "distinct_id", person_properties={"region": "USA"}
)
self.assertEqual(flag_result.enabled, True)
self.assertEqual(flag_result.variant, "variant-1")
self.assertEqual(flag_result.get_value(), "variant-1")
self.assertEqual(flag_result.payload, {"some": "value"})
patch_capture.assert_called_with(
"distinct_id",
"$feature_flag_called",
{
"$feature_flag": "person-flag",
"$feature_flag_response": "variant-1",
"locally_evaluated": True,
"$feature/person-flag": "variant-1",
"$feature_flag_payload": {"some": "value"},
},
groups={},
disable_geoip=None,
)
another_flag_result = self.client.get_feature_flag_result(
"person-flag", "another-distinct-id", person_properties={"region": "USA"}
)
self.assertEqual(another_flag_result.enabled, True)
self.assertEqual(another_flag_result.variant, "variant-2")
self.assertEqual(another_flag_result.get_value(), "variant-2")
self.assertIsNone(another_flag_result.payload)
patch_capture.assert_called_with(
"another-distinct-id",
"$feature_flag_called",
{
"$feature_flag": "person-flag",
"$feature_flag_response": "variant-2",
"locally_evaluated": True,
"$feature/person-flag": "variant-2",
},
groups={},
disable_geoip=None,
)
@mock.patch("posthog.client.flags")
@mock.patch.object(Client, "capture")
def test_get_feature_flag_result_boolean_decide(self, patch_capture, patch_flags):
patch_flags.return_value = {
"flags": {
"person-flag": {
"key": "person-flag",
"enabled": True,
"variant": None,
"reason": {
"description": "Matched condition set 1",
},
"metadata": {
"id": 23,
"version": 42,
"payload": "300",
},
},
},
}
flag_result = self.client.get_feature_flag_result(
"person-flag", "some-distinct-id"
)
self.assertEqual(flag_result.enabled, True)
self.assertEqual(flag_result.variant, None)
self.assertEqual(flag_result.payload, 300)
patch_capture.assert_called_with(
"some-distinct-id",
"$feature_flag_called",
{
"$feature_flag": "person-flag",
"$feature_flag_response": True,
"locally_evaluated": False,
"$feature/person-flag": True,
"$feature_flag_reason": "Matched condition set 1",
"$feature_flag_id": 23,
"$feature_flag_version": 42,
"$feature_flag_payload": 300,
},
groups={},
disable_geoip=None,
)
@mock.patch("posthog.client.flags")
@mock.patch.object(Client, "capture")
def test_get_feature_flag_result_variant_decide(self, patch_capture, patch_flags):
patch_flags.return_value = {
"flags": {
"person-flag": {
"key": "person-flag",
"enabled": True,
"variant": "variant-1",
"reason": {
"description": "Matched condition set 1",
},
"metadata": {
"id": 1,
"version": 2,
"payload": "[1, 2, 3]",
},
},
},
}
flag_result = self.client.get_feature_flag_result("person-flag", "distinct_id")
self.assertEqual(flag_result.enabled, True)
self.assertEqual(flag_result.variant, "variant-1")
self.assertEqual(flag_result.get_value(), "variant-1")
self.assertEqual(flag_result.payload, [1, 2, 3])
patch_capture.assert_called_with(
"distinct_id",
"$feature_flag_called",
{
"$feature_flag": "person-flag",
"$feature_flag_response": "variant-1",
"locally_evaluated": False,
"$feature/person-flag": "variant-1",
"$feature_flag_reason": "Matched condition set 1",
"$feature_flag_id": 1,
"$feature_flag_version": 2,
"$feature_flag_payload": [1, 2, 3],
},
groups={},
disable_geoip=None,
)
@mock.patch("posthog.client.flags")
@mock.patch.object(Client, "capture")
def test_get_feature_flag_result_unknown_flag(self, patch_capture, patch_flags):
patch_flags.return_value = {
"flags": {
"person-flag": {
"key": "person-flag",
"enabled": True,
"variant": None,
"reason": {
"description": "Matched condition set 1",
},
"metadata": {
"id": 23,
"version": 42,
"payload": "300",
},
},
},
}
flag_result = self.client.get_feature_flag_result(
"no-person-flag", "some-distinct-id"
)
self.assertIsNone(flag_result)
patch_capture.assert_called_with(
"some-distinct-id",
"$feature_flag_called",
{
"$feature_flag": "no-person-flag",
"$feature_flag_response": None,
"locally_evaluated": False,
"$feature/no-person-flag": None,
},
groups={},
disable_geoip=None,
)

View File

@@ -0,0 +1,50 @@
import unittest
from posthog import Posthog
class TestModule(unittest.TestCase):
posthog = None
def _assert_enqueue_result(self, result):
self.assertEqual(type(result[0]), bool)
self.assertEqual(type(result[1]), dict)
def failed(self):
self.failed = True
def setUp(self):
self.failed = False
self.posthog = Posthog(
"testsecret", host="http://localhost:8000", on_error=self.failed
)
def test_no_api_key(self):
self.posthog.api_key = None
self.assertRaises(Exception, self.posthog.capture)
def test_no_host(self):
self.posthog.host = None
self.assertRaises(Exception, self.posthog.capture)
def test_track(self):
res = self.posthog.capture("distinct_id", "python module event")
self._assert_enqueue_result(res)
self.posthog.flush()
def test_identify(self):
res = self.posthog.identify("distinct_id", {"email": "user@email.com"})
self._assert_enqueue_result(res)
self.posthog.flush()
def test_alias(self):
res = self.posthog.alias("previousId", "distinct_id")
self._assert_enqueue_result(res)
self.posthog.flush()
def test_page(self):
self.posthog.page("distinct_id", "https://posthog.com/contact")
self.posthog.flush()
def test_flush(self):
self.posthog.flush()

View File

@@ -0,0 +1,130 @@
import json
import unittest
from datetime import date, datetime
import mock
import pytest
import requests
from posthog.request import (
DatetimeSerializer,
QuotaLimitError,
batch_post,
decide,
determine_server_host,
)
from posthog.test.test_utils import TEST_API_KEY
class TestRequests(unittest.TestCase):
def test_valid_request(self):
res = batch_post(
TEST_API_KEY,
batch=[
{"distinct_id": "distinct_id", "event": "python event", "type": "track"}
],
)
self.assertEqual(res.status_code, 200)
def test_invalid_request_error(self):
self.assertRaises(
Exception, batch_post, "testsecret", "https://t.posthog.com", False, "[{]"
)
def test_invalid_host(self):
self.assertRaises(
Exception, batch_post, "testsecret", "t.posthog.com/", batch=[]
)
def test_datetime_serialization(self):
data = {"created": datetime(2012, 3, 4, 5, 6, 7, 891011)}
result = json.dumps(data, cls=DatetimeSerializer)
self.assertEqual(result, '{"created": "2012-03-04T05:06:07.891011"}')
def test_date_serialization(self):
today = date.today()
data = {"created": today}
result = json.dumps(data, cls=DatetimeSerializer)
expected = '{"created": "%s"}' % today.isoformat()
self.assertEqual(result, expected)
def test_should_not_timeout(self):
res = batch_post(
TEST_API_KEY,
batch=[
{"distinct_id": "distinct_id", "event": "python event", "type": "track"}
],
timeout=15,
)
self.assertEqual(res.status_code, 200)
def test_should_timeout(self):
with self.assertRaises(requests.ReadTimeout):
batch_post(
"key",
batch=[
{
"distinct_id": "distinct_id",
"event": "python event",
"type": "track",
}
],
timeout=0.0001,
)
def test_quota_limited_response(self):
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = json.dumps(
{
"quotaLimited": ["feature_flags"],
"featureFlags": {},
"featureFlagPayloads": {},
"errorsWhileComputingFlags": False,
}
).encode("utf-8")
with mock.patch("posthog.request._session.post", return_value=mock_response):
with self.assertRaises(QuotaLimitError) as cm:
decide("fake_key", "fake_host")
self.assertEqual(cm.exception.status, 200)
self.assertEqual(cm.exception.message, "Feature flags quota limited")
def test_normal_decide_response(self):
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = json.dumps(
{
"featureFlags": {"flag1": True},
"featureFlagPayloads": {},
"errorsWhileComputingFlags": False,
}
).encode("utf-8")
with mock.patch("posthog.request._session.post", return_value=mock_response):
response = decide("fake_key", "fake_host")
self.assertEqual(response["featureFlags"], {"flag1": True})
@pytest.mark.parametrize(
"host, expected",
[
("https://t.posthog.com", "https://t.posthog.com"),
("https://t.posthog.com/", "https://t.posthog.com/"),
("t.posthog.com", "t.posthog.com"),
("t.posthog.com/", "t.posthog.com/"),
("https://us.posthog.com.rg.proxy.com", "https://us.posthog.com.rg.proxy.com"),
("app.posthog.com", "app.posthog.com"),
("eu.posthog.com", "eu.posthog.com"),
("https://app.posthog.com", "https://us.i.posthog.com"),
("https://eu.posthog.com", "https://eu.i.posthog.com"),
("https://us.posthog.com", "https://us.i.posthog.com"),
("https://app.posthog.com/", "https://us.i.posthog.com"),
("https://eu.posthog.com/", "https://eu.i.posthog.com"),
("https://us.posthog.com/", "https://us.i.posthog.com"),
(None, "https://us.i.posthog.com"),
],
)
def test_routing_to_custom_host(host, expected):
assert determine_server_host(host) == expected

View File

@@ -0,0 +1,220 @@
import unittest
from unittest.mock import patch
from posthog.scopes import (
clear_tags,
get_tags,
new_context,
scoped,
tag,
identify_context,
set_context_session,
get_context_session_id,
get_context_distinct_id,
)
class TestScopes(unittest.TestCase):
def setUp(self):
# Reset any context between tests
clear_tags()
def test_tag_and_get_tags(self):
with new_context(fresh=True):
tag("key1", "value1")
tag("key2", 2)
tags = get_tags()
assert tags["key1"] == "value1"
assert tags["key2"] == 2
def test_clear_tags(self):
with new_context(fresh=True):
tag("key1", "value1")
assert get_tags()["key1"] == "value1"
clear_tags()
assert get_tags() == {}
def test_new_context_isolation(self):
with new_context(fresh=True):
# Set tag in outer context
tag("outer", "value")
with new_context(fresh=True):
# Inner context should start empty
assert get_tags() == {}
# Set tag in inner context
tag("inner", "value")
assert get_tags()["inner"] == "value"
# Outer tag should not be visible
self.assertNotIn("outer", get_tags())
with new_context(fresh=False):
# Inner context should inherit outer tag
assert get_tags() == {"outer": "value"}
# After exiting context, inner tag should be gone
self.assertNotIn("inner", get_tags())
# Outer tag should still be there
assert get_tags()["outer"] == "value"
def test_nested_contexts(self):
with new_context(fresh=True):
tag("level1", "value1")
with new_context(fresh=True):
tag("level2", "value2")
with new_context(fresh=True):
tag("level3", "value3")
assert get_tags() == {"level3": "value3"}
# Back to level 2
assert get_tags() == {"level2": "value2"}
# Back to level 1
assert get_tags() == {"level1": "value1"}
@patch("posthog.capture_exception")
def test_scoped_decorator_success(self, mock_capture):
@scoped()
def successful_function(x, y):
tag("x", x)
tag("y", y)
return x + y
result = successful_function(1, 2)
# Function should execute normally
assert result == 3
# No exception should be captured
mock_capture.assert_not_called()
# Context should be cleared after function execution
assert get_tags() == {}
@patch("posthog.capture_exception")
def test_scoped_decorator_exception(self, mock_capture):
test_exception = ValueError("Test exception")
def check_context_on_capture(exception, **kwargs):
# Assert tags are available when capture_exception is called
current_tags = get_tags()
assert current_tags.get("important_context") == "value"
mock_capture.side_effect = check_context_on_capture
@scoped()
def failing_function():
tag("important_context", "value")
raise test_exception
# Function should raise the exception
with self.assertRaises(ValueError):
failing_function()
# Verify capture_exception was called
mock_capture.assert_called_once_with(test_exception)
# Context should be cleared after function execution
assert get_tags() == {}
@patch("posthog.capture_exception")
def test_new_context_exception_handling(self, mock_capture):
test_exception = RuntimeError("Context exception")
def check_context_on_capture(exception, **kwargs):
# Assert inner context tags are available when capture_exception is called
current_tags = get_tags()
assert current_tags.get("inner_context") == "inner_value"
mock_capture.side_effect = check_context_on_capture
# Set up outer context
with new_context():
tag("outer_context", "outer_value")
try:
with new_context():
tag("inner_context", "inner_value")
raise test_exception
except RuntimeError:
pass # Expected exception
# Outer context should still be intact
assert get_tags()["outer_context"] == "outer_value"
# Verify capture_exception was called
mock_capture.assert_called_once_with(test_exception)
def test_identify_context(self):
with new_context(fresh=True):
# Initially no distinct ID
assert get_context_distinct_id() is None
# Set distinct ID
identify_context("user123")
assert get_context_distinct_id() == "user123"
def test_set_context_session(self):
with new_context(fresh=True):
# Initially no session ID
assert get_context_session_id() is None
# Set session ID
set_context_session("session456")
assert get_context_session_id() == "session456"
def test_context_inheritance_fresh_context(self):
with new_context(fresh=True):
identify_context("user123")
set_context_session("session456")
with new_context(fresh=True):
# Fresh context should not inherit
assert get_context_distinct_id() is None
assert get_context_session_id() is None
# Original context should still have values
assert get_context_distinct_id() == "user123"
assert get_context_session_id() == "session456"
def test_context_inheritance_non_fresh_context(self):
with new_context(fresh=True):
identify_context("user123")
set_context_session("session456")
with new_context(fresh=False):
# Non-fresh context should inherit
assert get_context_distinct_id() == "user123"
assert get_context_session_id() == "session456"
# Override in child context
identify_context("user789")
set_context_session("session999")
assert get_context_distinct_id() == "user789"
assert get_context_session_id() == "session999"
# Original context should still have original values
assert get_context_distinct_id() == "user123"
assert get_context_session_id() == "session456"
def test_scoped_decorator_with_context_ids(self):
@scoped()
def function_with_context():
identify_context("user456")
set_context_session("session789")
return get_context_distinct_id(), get_context_session_id()
distinct_id, session_id = function_with_context()
assert distinct_id == "user456"
assert session_id == "session789"
# Context should be cleared after function execution
assert get_context_distinct_id() is None
assert get_context_session_id() is None

View File

@@ -0,0 +1,24 @@
import unittest
from parameterized import parameterized
from posthog import utils
class TestSizeLimitedDict(unittest.TestCase):
@parameterized.expand([(10, 100), (5, 20), (20, 200)])
def test_size_limited_dict(self, size: int, iterations: int) -> None:
values = utils.SizeLimitedDict(size, lambda _: -1)
for i in range(iterations):
values[i] = i
assert values[i] == i
assert len(values) == i % size + 1
if i % size == 0:
# old numbers should've been removed
self.assertIsNone(values.get(i - 1))
self.assertIsNone(values.get(i - 3))
self.assertIsNone(values.get(i - 5))
self.assertIsNone(values.get(i - 9))

View File

@@ -0,0 +1,208 @@
import unittest
from parameterized import parameterized
from posthog.types import (
FeatureFlag,
FlagMetadata,
FlagReason,
LegacyFlagMetadata,
normalize_flags_response,
to_flags_and_payloads,
)
class TestTypes(unittest.TestCase):
@parameterized.expand([(True,), (False,)])
def test_normalize_decide_response_v4(self, has_errors: bool):
resp = {
"flags": {
"my-flag": FeatureFlag(
key="my-flag",
enabled=True,
variant="test-variant",
reason=FlagReason(
code="matched_condition",
condition_index=0,
description="Matched condition set 1",
),
metadata=FlagMetadata(
id=1,
payload='{"some": "json"}',
version=2,
description="test-description",
),
)
},
"errorsWhileComputingFlags": has_errors,
"requestId": "test-id",
}
result = normalize_flags_response(resp)
flag = result["flags"]["my-flag"]
self.assertEqual(flag.key, "my-flag")
self.assertTrue(flag.enabled)
self.assertEqual(flag.variant, "test-variant")
self.assertEqual(flag.get_value(), "test-variant")
self.assertEqual(
flag.reason,
FlagReason(
code="matched_condition",
condition_index=0,
description="Matched condition set 1",
),
)
self.assertEqual(
flag.metadata,
FlagMetadata(
id=1,
payload='{"some": "json"}',
version=2,
description="test-description",
),
)
self.assertEqual(result["errorsWhileComputingFlags"], has_errors)
self.assertEqual(result["requestId"], "test-id")
def test_normalize_decide_response_legacy(self):
# Test legacy response format with "featureFlags" and "featureFlagPayloads"
resp = {
"featureFlags": {"my-flag": "test-variant"},
"featureFlagPayloads": {"my-flag": '{"some": "json-payload"}'},
"errorsWhileComputingFlags": False,
"requestId": "test-id",
}
result = normalize_flags_response(resp)
flag = result["flags"]["my-flag"]
self.assertEqual(flag.key, "my-flag")
self.assertTrue(flag.enabled)
self.assertEqual(flag.variant, "test-variant")
self.assertEqual(flag.get_value(), "test-variant")
self.assertIsNone(flag.reason)
self.assertEqual(
flag.metadata, LegacyFlagMetadata(payload='{"some": "json-payload"}')
)
self.assertFalse(result["errorsWhileComputingFlags"])
self.assertEqual(result["requestId"], "test-id")
# Verify legacy fields are removed
self.assertNotIn("featureFlags", result)
self.assertNotIn("featureFlagPayloads", result)
def test_normalize_decide_response_boolean_flag(self):
# Test legacy response with boolean flag
resp = {"featureFlags": {"my-flag": True}, "errorsWhileComputingFlags": False}
result = normalize_flags_response(resp)
self.assertIn("requestId", result)
self.assertIsNone(result["requestId"])
flag = result["flags"]["my-flag"]
self.assertEqual(flag.key, "my-flag")
self.assertTrue(flag.enabled)
self.assertIsNone(flag.variant)
self.assertIsNone(flag.reason)
self.assertEqual(flag.metadata, LegacyFlagMetadata(payload=None))
self.assertFalse(result["errorsWhileComputingFlags"])
self.assertNotIn("featureFlags", result)
self.assertNotIn("featureFlagPayloads", result)
def test_to_flags_and_payloads_v4(self):
# Test v4 response format
resp = {
"flags": {
"my-variant-flag": FeatureFlag(
key="my-variant-flag",
enabled=True,
variant="test-variant",
reason=FlagReason(
code="matched_condition",
condition_index=0,
description="Matched condition set 1",
),
metadata=FlagMetadata(
id=1,
payload='{"some": "json"}',
version=2,
description="test-description",
),
),
"my-boolean-flag": FeatureFlag(
key="my-boolean-flag",
enabled=True,
variant=None,
reason=FlagReason(
code="matched_condition",
condition_index=0,
description="Matched condition set 1",
),
metadata=FlagMetadata(
id=1, payload=None, version=2, description="test-description"
),
),
"disabled-flag": FeatureFlag(
key="disabled-flag",
enabled=False,
variant=None,
reason=None,
metadata=LegacyFlagMetadata(payload=None),
),
},
"errorsWhileComputingFlags": False,
"requestId": "test-id",
}
result = to_flags_and_payloads(resp)
self.assertEqual(result["featureFlags"]["my-variant-flag"], "test-variant")
self.assertEqual(result["featureFlags"]["my-boolean-flag"], True)
self.assertEqual(result["featureFlags"]["disabled-flag"], False)
self.assertEqual(
result["featureFlagPayloads"]["my-variant-flag"], '{"some": "json"}'
)
self.assertNotIn("my-boolean-flag", result["featureFlagPayloads"])
self.assertNotIn("disabled-flag", result["featureFlagPayloads"])
def test_to_flags_and_payloads_empty(self):
# Test empty response
resp = {
"flags": {},
"errorsWhileComputingFlags": False,
"requestId": "test-id",
}
result = to_flags_and_payloads(resp)
self.assertEqual(result["featureFlags"], {})
self.assertEqual(result["featureFlagPayloads"], {})
def test_to_flags_and_payloads_with_payload(self):
resp = {
"flags": {
"decide-flag": {
"key": "decide-flag",
"enabled": True,
"variant": "decide-variant",
"reason": {
"code": "matched_condition",
"condition_index": 0,
"description": "Matched condition set 1",
},
"metadata": {
"id": 23,
"version": 42,
"payload": '{"foo": "bar"}',
},
}
},
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
}
normalized = normalize_flags_response(resp)
result = to_flags_and_payloads(normalized)
self.assertEqual(result["featureFlags"]["decide-flag"], "decide-variant")
self.assertEqual(result["featureFlagPayloads"]["decide-flag"], '{"foo": "bar"}')

View File

@@ -0,0 +1,175 @@
import unittest
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from decimal import Decimal
from typing import Optional
from uuid import UUID
import six
from dateutil.tz import tzutc
from parameterized import parameterized
from pydantic import BaseModel
from pydantic.v1 import BaseModel as BaseModelV1
from posthog import utils
TEST_API_KEY = "kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4"
FAKE_TEST_API_KEY = "random_key"
class TestUtils(unittest.TestCase):
@parameterized.expand(
[
("naive datetime should be naive", True),
("timezone-aware datetime should not be naive", False),
]
)
def test_is_naive(self, _name: str, expected_naive: bool):
if expected_naive:
dt = datetime.now() # naive datetime
else:
dt = datetime.now(tz=tzutc()) # timezone-aware datetime
assert utils.is_naive(dt) is expected_naive
def test_timezone_utils(self):
now = datetime.now()
utcnow = datetime.now(tz=tzutc())
fixed = utils.guess_timezone(now)
assert utils.is_naive(fixed) is False
shouldnt_be_edited = utils.guess_timezone(utcnow)
assert utcnow == shouldnt_be_edited
def test_clean(self):
simple = {
"decimal": Decimal("0.142857"),
"unicode": six.u("woo"),
"date": datetime.now(),
"long": 200000000,
"integer": 1,
"float": 2.0,
"bool": True,
"str": "woo",
"none": None,
}
complicated = {
"exception": Exception("This should show up"),
"timedelta": timedelta(microseconds=20),
"list": [1, 2, 3],
}
combined = dict(simple.items())
combined.update(complicated.items())
pre_clean_keys = combined.keys()
utils.clean(combined)
assert combined.keys() == pre_clean_keys
# test UUID separately, as the UUID object doesn't equal its string representation according to Python
assert (
utils.clean(UUID("12345678123456781234567812345678"))
== "12345678-1234-5678-1234-567812345678"
)
def test_clean_with_dates(self):
dict_with_dates = {
"birthdate": date(1980, 1, 1),
"registration": datetime.now(tz=tzutc()),
}
assert dict_with_dates == utils.clean(dict_with_dates)
def test_bytes(self):
item = bytes(10)
utils.clean(item)
assert utils.clean(item) == "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
def test_clean_fn(self):
cleaned = utils.clean({"fn": lambda x: x, "number": 4})
assert cleaned == {"fn": None, "number": 4}
@parameterized.expand(
[
("http://posthog.io/", "http://posthog.io"),
("http://posthog.io", "http://posthog.io"),
("https://example.com/path/", "https://example.com/path"),
("https://example.com/path", "https://example.com/path"),
]
)
def test_remove_slash(self, input_url, expected_url):
assert expected_url == utils.remove_trailing_slash(input_url)
def test_clean_pydantic(self):
class ModelV2(BaseModel):
foo: str
bar: int
baz: Optional[str] = None
class ModelV1(BaseModelV1):
foo: int
bar: str
class NestedModel(BaseModel):
foo: ModelV2
assert utils.clean(ModelV2(foo="1", bar=2)) == {
"foo": "1",
"bar": 2,
"baz": None,
}
assert utils.clean(ModelV1(foo=1, bar="2")) == {"foo": 1, "bar": "2"}
assert utils.clean(NestedModel(foo=ModelV2(foo="1", bar=2, baz="3"))) == {
"foo": {"foo": "1", "bar": 2, "baz": "3"}
}
def test_clean_pydantic_like_class(self) -> None:
class Dummy:
def model_dump(self, required_param: str) -> dict:
return {}
# previously python 2 code would cause an error while cleaning,
# and this entire object would be None, and we would log an error
# let's allow ourselves to clean `Dummy` as None,
# without blatting the `test` key
assert utils.clean({"test": Dummy()}) == {"test": None}
def test_clean_dataclass(self):
@dataclass
class InnerDataClass:
inner_foo: str
inner_bar: int
inner_uuid: UUID
inner_date: datetime
inner_optional: Optional[str] = None
@dataclass
class TestDataClass:
foo: str
bar: int
nested: InnerDataClass
assert utils.clean(
TestDataClass(
foo="1",
bar=2,
nested=InnerDataClass(
inner_foo="3",
inner_bar=4,
inner_uuid=UUID("12345678123456781234567812345678"),
inner_date=datetime(2025, 1, 1),
),
)
) == {
"foo": "1",
"bar": 2,
"nested": {
"inner_foo": "3",
"inner_bar": 4,
"inner_uuid": "12345678-1234-5678-1234-567812345678",
"inner_date": datetime(2025, 1, 1),
"inner_optional": None,
},
}