posthoganalytics 6.0.2__tar.gz → 6.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {posthoganalytics-6.0.2/posthoganalytics.egg-info → posthoganalytics-6.0.3}/PKG-INFO +2 -2
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/README.md +1 -1
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/client.py +161 -1
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_utils.py +123 -0
- posthoganalytics-6.0.3/posthoganalytics/utils.py +519 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/version.py +1 -1
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3/posthoganalytics.egg-info}/PKG-INFO +2 -2
- posthoganalytics-6.0.2/posthoganalytics/utils.py +0 -257
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/LICENSE +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/MANIFEST.in +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/anthropic/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/anthropic/anthropic.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/gemini/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/gemini/gemini.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/langchain/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/langchain/callbacks.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/openai/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/openai/openai.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/openai/openai_async.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/openai/openai_providers.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/ai/utils.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/args.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/consumer.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/contexts.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/exception_capture.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/exception_utils.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/feature_flags.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/integrations/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/integrations/django.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/poller.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/py.typed +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/request.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/__init__.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_before_send.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_client.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_consumer.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_contexts.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_exception_capture.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_feature_flag.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_feature_flag_result.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_feature_flags.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_module.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_request.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_size_limited_dict.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/test/test_types.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics/types.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics.egg-info/SOURCES.txt +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics.egg-info/dependency_links.txt +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics.egg-info/requires.txt +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/posthoganalytics.egg-info/top_level.txt +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/pyproject.toml +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/setup.cfg +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/setup.py +0 -0
- {posthoganalytics-6.0.2 → posthoganalytics-6.0.3}/setup_analytics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: posthoganalytics
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.3
|
|
4
4
|
Summary: Integrate PostHog into any python application.
|
|
5
5
|
Home-page: https://github.com/posthog/posthog-python
|
|
6
6
|
Author: Posthog
|
|
@@ -107,7 +107,7 @@ We recommend using [uv](https://docs.astral.sh/uv/). It's super fast.
|
|
|
107
107
|
```bash
|
|
108
108
|
uv python install 3.9.19
|
|
109
109
|
uv python pin 3.9.19
|
|
110
|
-
uv venv
|
|
110
|
+
uv venv
|
|
111
111
|
source env/bin/activate
|
|
112
112
|
uv sync --extra dev --extra test
|
|
113
113
|
pre-commit install
|
|
@@ -50,6 +50,8 @@ from posthoganalytics.types import (
|
|
|
50
50
|
to_values,
|
|
51
51
|
)
|
|
52
52
|
from posthoganalytics.utils import (
|
|
53
|
+
FlagCache,
|
|
54
|
+
RedisFlagCache,
|
|
53
55
|
SizeLimitedDict,
|
|
54
56
|
clean,
|
|
55
57
|
guess_timezone,
|
|
@@ -95,7 +97,30 @@ def add_context_tags(properties):
|
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
class Client(object):
|
|
98
|
-
"""Create a new PostHog client.
|
|
100
|
+
"""Create a new PostHog client.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
Basic usage:
|
|
104
|
+
>>> client = Client("your-api-key")
|
|
105
|
+
|
|
106
|
+
With memory-based feature flag fallback cache:
|
|
107
|
+
>>> client = Client(
|
|
108
|
+
... "your-api-key",
|
|
109
|
+
... flag_fallback_cache_url="memory://local/?ttl=300&size=10000"
|
|
110
|
+
... )
|
|
111
|
+
|
|
112
|
+
With Redis fallback cache for high-scale applications:
|
|
113
|
+
>>> client = Client(
|
|
114
|
+
... "your-api-key",
|
|
115
|
+
... flag_fallback_cache_url="redis://localhost:6379/0/?ttl=300"
|
|
116
|
+
... )
|
|
117
|
+
|
|
118
|
+
With Redis authentication:
|
|
119
|
+
>>> client = Client(
|
|
120
|
+
... "your-api-key",
|
|
121
|
+
... flag_fallback_cache_url="redis://username:password@localhost:6379/0/?ttl=300"
|
|
122
|
+
... )
|
|
123
|
+
"""
|
|
99
124
|
|
|
100
125
|
log = logging.getLogger("posthog")
|
|
101
126
|
|
|
@@ -126,6 +151,7 @@ class Client(object):
|
|
|
126
151
|
project_root=None,
|
|
127
152
|
privacy_mode=False,
|
|
128
153
|
before_send=None,
|
|
154
|
+
flag_fallback_cache_url=None,
|
|
129
155
|
):
|
|
130
156
|
self.queue = queue.Queue(max_queue_size)
|
|
131
157
|
|
|
@@ -151,6 +177,8 @@ class Client(object):
|
|
|
151
177
|
)
|
|
152
178
|
self.poller = None
|
|
153
179
|
self.distinct_ids_feature_flags_reported = SizeLimitedDict(MAX_DICT_SIZE, set)
|
|
180
|
+
self.flag_cache = self._initialize_flag_cache(flag_fallback_cache_url)
|
|
181
|
+
self.flag_definition_version = 0
|
|
154
182
|
self.disabled = disabled
|
|
155
183
|
self.disable_geoip = disable_geoip
|
|
156
184
|
self.historical_migration = historical_migration
|
|
@@ -707,6 +735,9 @@ class Client(object):
|
|
|
707
735
|
|
|
708
736
|
def _load_feature_flags(self):
|
|
709
737
|
try:
|
|
738
|
+
# Store old flags to detect changes
|
|
739
|
+
old_flags_by_key: dict[str, dict] = self.feature_flags_by_key or {}
|
|
740
|
+
|
|
710
741
|
response = get(
|
|
711
742
|
self.personal_api_key,
|
|
712
743
|
f"/api/feature_flag/local_evaluation/?token={self.api_key}&send_cohorts",
|
|
@@ -718,6 +749,14 @@ class Client(object):
|
|
|
718
749
|
self.group_type_mapping = response["group_type_mapping"] or {}
|
|
719
750
|
self.cohorts = response["cohorts"] or {}
|
|
720
751
|
|
|
752
|
+
# Check if flag definitions changed and update version
|
|
753
|
+
if self.flag_cache and old_flags_by_key != (
|
|
754
|
+
self.feature_flags_by_key or {}
|
|
755
|
+
):
|
|
756
|
+
old_version = self.flag_definition_version
|
|
757
|
+
self.flag_definition_version += 1
|
|
758
|
+
self.flag_cache.invalidate_version(old_version)
|
|
759
|
+
|
|
721
760
|
except APIError as e:
|
|
722
761
|
if e.status == 401:
|
|
723
762
|
self.log.error(
|
|
@@ -739,6 +778,10 @@ class Client(object):
|
|
|
739
778
|
self.group_type_mapping = {}
|
|
740
779
|
self.cohorts = {}
|
|
741
780
|
|
|
781
|
+
# Clear flag cache when quota limited
|
|
782
|
+
if self.flag_cache:
|
|
783
|
+
self.flag_cache.clear()
|
|
784
|
+
|
|
742
785
|
if self.debug:
|
|
743
786
|
raise APIError(
|
|
744
787
|
status=402,
|
|
@@ -889,6 +932,12 @@ class Client(object):
|
|
|
889
932
|
flag_result = FeatureFlagResult.from_value_and_payload(
|
|
890
933
|
key, lookup_match_value, payload
|
|
891
934
|
)
|
|
935
|
+
|
|
936
|
+
# Cache successful local evaluation
|
|
937
|
+
if self.flag_cache and flag_result:
|
|
938
|
+
self.flag_cache.set_cached_flag(
|
|
939
|
+
distinct_id, key, flag_result, self.flag_definition_version
|
|
940
|
+
)
|
|
892
941
|
elif not only_evaluate_locally:
|
|
893
942
|
try:
|
|
894
943
|
flag_details, request_id = self._get_feature_flag_details_from_decide(
|
|
@@ -902,12 +951,30 @@ class Client(object):
|
|
|
902
951
|
flag_result = FeatureFlagResult.from_flag_details(
|
|
903
952
|
flag_details, override_match_value
|
|
904
953
|
)
|
|
954
|
+
|
|
955
|
+
# Cache successful remote evaluation
|
|
956
|
+
if self.flag_cache and flag_result:
|
|
957
|
+
self.flag_cache.set_cached_flag(
|
|
958
|
+
distinct_id, key, flag_result, self.flag_definition_version
|
|
959
|
+
)
|
|
960
|
+
|
|
905
961
|
self.log.debug(
|
|
906
962
|
f"Successfully computed flag remotely: #{key} -> #{flag_result}"
|
|
907
963
|
)
|
|
908
964
|
except Exception as e:
|
|
909
965
|
self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")
|
|
910
966
|
|
|
967
|
+
# Fallback to cached value if remote evaluation fails
|
|
968
|
+
if self.flag_cache:
|
|
969
|
+
stale_result = self.flag_cache.get_stale_cached_flag(
|
|
970
|
+
distinct_id, key
|
|
971
|
+
)
|
|
972
|
+
if stale_result:
|
|
973
|
+
self.log.info(
|
|
974
|
+
f"[FEATURE FLAGS] Using stale cached value for flag {key}"
|
|
975
|
+
)
|
|
976
|
+
flag_result = stale_result
|
|
977
|
+
|
|
911
978
|
if send_feature_flag_events:
|
|
912
979
|
self._capture_feature_flag_called(
|
|
913
980
|
distinct_id,
|
|
@@ -1278,6 +1345,99 @@ class Client(object):
|
|
|
1278
1345
|
"featureFlagPayloads": payloads,
|
|
1279
1346
|
}, fallback_to_decide
|
|
1280
1347
|
|
|
1348
|
+
def _initialize_flag_cache(self, cache_url):
|
|
1349
|
+
"""Initialize feature flag cache for graceful degradation during service outages.
|
|
1350
|
+
|
|
1351
|
+
When enabled, the cache stores flag evaluation results and serves them as fallback
|
|
1352
|
+
when the PostHog API is unavailable. This ensures your application continues to
|
|
1353
|
+
receive flag values even during outages.
|
|
1354
|
+
|
|
1355
|
+
Args:
|
|
1356
|
+
cache_url: Cache configuration URL. Examples:
|
|
1357
|
+
- None: Disable caching
|
|
1358
|
+
- "memory://local/?ttl=300&size=10000": Memory cache with TTL and size
|
|
1359
|
+
- "redis://localhost:6379/0/?ttl=300": Redis cache with TTL
|
|
1360
|
+
- "redis://username:password@host:port/?ttl=300": Redis with auth
|
|
1361
|
+
|
|
1362
|
+
Example usage:
|
|
1363
|
+
# Memory cache
|
|
1364
|
+
client = Client(
|
|
1365
|
+
"your-api-key",
|
|
1366
|
+
flag_fallback_cache_url="memory://local/?ttl=300&size=10000"
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
# Redis cache
|
|
1370
|
+
client = Client(
|
|
1371
|
+
"your-api-key",
|
|
1372
|
+
flag_fallback_cache_url="redis://localhost:6379/0/?ttl=300"
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
# Normal evaluation - cache is populated
|
|
1376
|
+
flag_value = client.get_feature_flag("my-flag", "user123")
|
|
1377
|
+
|
|
1378
|
+
# During API outage - returns cached value instead of None
|
|
1379
|
+
flag_value = client.get_feature_flag("my-flag", "user123") # Uses cache
|
|
1380
|
+
"""
|
|
1381
|
+
if not cache_url:
|
|
1382
|
+
return None
|
|
1383
|
+
|
|
1384
|
+
try:
|
|
1385
|
+
from urllib.parse import urlparse, parse_qs
|
|
1386
|
+
except ImportError:
|
|
1387
|
+
from urlparse import urlparse, parse_qs
|
|
1388
|
+
|
|
1389
|
+
try:
|
|
1390
|
+
parsed = urlparse(cache_url)
|
|
1391
|
+
scheme = parsed.scheme.lower()
|
|
1392
|
+
query_params = parse_qs(parsed.query)
|
|
1393
|
+
ttl = int(query_params.get("ttl", [300])[0])
|
|
1394
|
+
|
|
1395
|
+
if scheme == "memory":
|
|
1396
|
+
size = int(query_params.get("size", [10000])[0])
|
|
1397
|
+
return FlagCache(size, ttl)
|
|
1398
|
+
|
|
1399
|
+
elif scheme == "redis":
|
|
1400
|
+
try:
|
|
1401
|
+
# Not worth importing redis if we're not using it
|
|
1402
|
+
import redis
|
|
1403
|
+
|
|
1404
|
+
redis_url = f"{parsed.scheme}://"
|
|
1405
|
+
if parsed.username or parsed.password:
|
|
1406
|
+
redis_url += f"{parsed.username or ''}:{parsed.password or ''}@"
|
|
1407
|
+
redis_url += (
|
|
1408
|
+
f"{parsed.hostname or 'localhost'}:{parsed.port or 6379}"
|
|
1409
|
+
)
|
|
1410
|
+
if parsed.path:
|
|
1411
|
+
redis_url += parsed.path
|
|
1412
|
+
|
|
1413
|
+
client = redis.from_url(redis_url)
|
|
1414
|
+
|
|
1415
|
+
# Test connection before using it
|
|
1416
|
+
client.ping()
|
|
1417
|
+
|
|
1418
|
+
return RedisFlagCache(client, default_ttl=ttl)
|
|
1419
|
+
|
|
1420
|
+
except ImportError:
|
|
1421
|
+
self.log.warning(
|
|
1422
|
+
"[FEATURE FLAGS] Redis not available, flag caching disabled"
|
|
1423
|
+
)
|
|
1424
|
+
return None
|
|
1425
|
+
except Exception as e:
|
|
1426
|
+
self.log.warning(
|
|
1427
|
+
f"[FEATURE FLAGS] Redis connection failed: {e}, flag caching disabled"
|
|
1428
|
+
)
|
|
1429
|
+
return None
|
|
1430
|
+
else:
|
|
1431
|
+
raise ValueError(
|
|
1432
|
+
f"Unknown cache URL scheme: {scheme}. Supported schemes: memory, redis"
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
except Exception as e:
|
|
1436
|
+
self.log.warning(
|
|
1437
|
+
f"[FEATURE FLAGS] Failed to parse cache URL '{cache_url}': {e}"
|
|
1438
|
+
)
|
|
1439
|
+
return None
|
|
1440
|
+
|
|
1281
1441
|
def feature_flag_definitions(self):
|
|
1282
1442
|
return self.feature_flags
|
|
1283
1443
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import time
|
|
1
2
|
import unittest
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from datetime import date, datetime, timedelta
|
|
@@ -12,6 +13,7 @@ from pydantic import BaseModel
|
|
|
12
13
|
from pydantic.v1 import BaseModel as BaseModelV1
|
|
13
14
|
|
|
14
15
|
from posthoganalytics import utils
|
|
16
|
+
from posthoganalytics.types import FeatureFlagResult
|
|
15
17
|
|
|
16
18
|
TEST_API_KEY = "kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4"
|
|
17
19
|
FAKE_TEST_API_KEY = "random_key"
|
|
@@ -173,3 +175,124 @@ class TestUtils(unittest.TestCase):
|
|
|
173
175
|
"inner_optional": None,
|
|
174
176
|
},
|
|
175
177
|
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestFlagCache(unittest.TestCase):
|
|
181
|
+
def setUp(self):
|
|
182
|
+
self.cache = utils.FlagCache(max_size=3, default_ttl=1)
|
|
183
|
+
self.flag_result = FeatureFlagResult.from_value_and_payload(
|
|
184
|
+
"test-flag", True, None
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def test_cache_basic_operations(self):
|
|
188
|
+
distinct_id = "user123"
|
|
189
|
+
flag_key = "test-flag"
|
|
190
|
+
flag_version = 1
|
|
191
|
+
|
|
192
|
+
# Test cache miss
|
|
193
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, flag_version)
|
|
194
|
+
assert result is None
|
|
195
|
+
|
|
196
|
+
# Test cache set and hit
|
|
197
|
+
self.cache.set_cached_flag(
|
|
198
|
+
distinct_id, flag_key, self.flag_result, flag_version
|
|
199
|
+
)
|
|
200
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, flag_version)
|
|
201
|
+
assert result is not None
|
|
202
|
+
assert result.get_value()
|
|
203
|
+
|
|
204
|
+
def test_cache_ttl_expiration(self):
|
|
205
|
+
distinct_id = "user123"
|
|
206
|
+
flag_key = "test-flag"
|
|
207
|
+
flag_version = 1
|
|
208
|
+
|
|
209
|
+
# Set flag in cache
|
|
210
|
+
self.cache.set_cached_flag(
|
|
211
|
+
distinct_id, flag_key, self.flag_result, flag_version
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Should be available immediately
|
|
215
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, flag_version)
|
|
216
|
+
assert result is not None
|
|
217
|
+
|
|
218
|
+
# Wait for TTL to expire (1 second + buffer)
|
|
219
|
+
time.sleep(1.1)
|
|
220
|
+
|
|
221
|
+
# Should be expired
|
|
222
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, flag_version)
|
|
223
|
+
assert result is None
|
|
224
|
+
|
|
225
|
+
def test_cache_version_invalidation(self):
|
|
226
|
+
distinct_id = "user123"
|
|
227
|
+
flag_key = "test-flag"
|
|
228
|
+
old_version = 1
|
|
229
|
+
new_version = 2
|
|
230
|
+
|
|
231
|
+
# Set flag with old version
|
|
232
|
+
self.cache.set_cached_flag(distinct_id, flag_key, self.flag_result, old_version)
|
|
233
|
+
|
|
234
|
+
# Should hit with old version
|
|
235
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, old_version)
|
|
236
|
+
assert result is not None
|
|
237
|
+
|
|
238
|
+
# Should miss with new version
|
|
239
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, new_version)
|
|
240
|
+
assert result is None
|
|
241
|
+
|
|
242
|
+
# Invalidate old version
|
|
243
|
+
self.cache.invalidate_version(old_version)
|
|
244
|
+
|
|
245
|
+
# Should miss even with old version after invalidation
|
|
246
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, old_version)
|
|
247
|
+
assert result is None
|
|
248
|
+
|
|
249
|
+
def test_stale_cache_functionality(self):
|
|
250
|
+
distinct_id = "user123"
|
|
251
|
+
flag_key = "test-flag"
|
|
252
|
+
flag_version = 1
|
|
253
|
+
|
|
254
|
+
# Set flag in cache
|
|
255
|
+
self.cache.set_cached_flag(
|
|
256
|
+
distinct_id, flag_key, self.flag_result, flag_version
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Wait for TTL to expire
|
|
260
|
+
time.sleep(1.1)
|
|
261
|
+
|
|
262
|
+
# Should not get fresh cache
|
|
263
|
+
result = self.cache.get_cached_flag(distinct_id, flag_key, flag_version)
|
|
264
|
+
assert result is None
|
|
265
|
+
|
|
266
|
+
# Should get stale cache (within 1 hour default)
|
|
267
|
+
stale_result = self.cache.get_stale_cached_flag(distinct_id, flag_key)
|
|
268
|
+
assert stale_result is not None
|
|
269
|
+
assert stale_result.get_value()
|
|
270
|
+
|
|
271
|
+
def test_lru_eviction(self):
|
|
272
|
+
# Cache has max_size=3, so adding 4 users should evict the LRU one
|
|
273
|
+
flag_version = 1
|
|
274
|
+
|
|
275
|
+
# Add 3 users
|
|
276
|
+
for i in range(3):
|
|
277
|
+
user_id = f"user{i}"
|
|
278
|
+
self.cache.set_cached_flag(
|
|
279
|
+
user_id, "test-flag", self.flag_result, flag_version
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Access user0 to make it recently used
|
|
283
|
+
self.cache.get_cached_flag("user0", "test-flag", flag_version)
|
|
284
|
+
|
|
285
|
+
# Add 4th user, should evict user1 (least recently used)
|
|
286
|
+
self.cache.set_cached_flag("user3", "test-flag", self.flag_result, flag_version)
|
|
287
|
+
|
|
288
|
+
# user0 should still be there (was recently accessed)
|
|
289
|
+
result = self.cache.get_cached_flag("user0", "test-flag", flag_version)
|
|
290
|
+
assert result is not None
|
|
291
|
+
|
|
292
|
+
# user2 should still be there (was recently added)
|
|
293
|
+
result = self.cache.get_cached_flag("user2", "test-flag", flag_version)
|
|
294
|
+
assert result is not None
|
|
295
|
+
|
|
296
|
+
# user3 should be there (just added)
|
|
297
|
+
result = self.cache.get_cached_flag("user3", "test-flag", flag_version)
|
|
298
|
+
assert result is not None
|