posthoganalytics 6.5.0__tar.gz → 6.6.1__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.5.0/posthoganalytics.egg-info → posthoganalytics-6.6.1}/PKG-INFO +1 -1
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/client.py +87 -17
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/feature_flags.py +169 -20
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_client.py +56 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_feature_flags.py +546 -26
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/types.py +3 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/version.py +1 -1
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1/posthoganalytics.egg-info}/PKG-INFO +1 -1
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/LICENSE +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/MANIFEST.in +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/README.md +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/anthropic/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/anthropic/anthropic.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/gemini/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/gemini/gemini.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/langchain/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/langchain/callbacks.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/openai/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/openai/openai.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/openai/openai_async.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/openai/openai_providers.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/ai/utils.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/args.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/consumer.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/contexts.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/exception_capture.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/exception_utils.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/integrations/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/integrations/django.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/poller.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/py.typed +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/request.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/__init__.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_before_send.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_consumer.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_contexts.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_exception_capture.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_feature_flag.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_feature_flag_result.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_module.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_request.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_size_limited_dict.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_types.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/test/test_utils.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics/utils.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics.egg-info/SOURCES.txt +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics.egg-info/dependency_links.txt +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics.egg-info/requires.txt +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/posthoganalytics.egg-info/top_level.txt +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/pyproject.toml +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/setup.cfg +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/setup.py +0 -0
- {posthoganalytics-6.5.0 → posthoganalytics-6.6.1}/setup_analytics.py +0 -0
|
@@ -44,6 +44,7 @@ from posthoganalytics.types import (
|
|
|
44
44
|
FlagsAndPayloads,
|
|
45
45
|
FlagsResponse,
|
|
46
46
|
FlagValue,
|
|
47
|
+
SendFeatureFlagsOptions,
|
|
47
48
|
normalize_flags_response,
|
|
48
49
|
to_flags_and_payloads,
|
|
49
50
|
to_payloads,
|
|
@@ -313,6 +314,7 @@ class Client(object):
|
|
|
313
314
|
person_properties=None,
|
|
314
315
|
group_properties=None,
|
|
315
316
|
disable_geoip=None,
|
|
317
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
316
318
|
) -> dict[str, Union[bool, str]]:
|
|
317
319
|
"""
|
|
318
320
|
Get feature flag variants for a user by calling decide.
|
|
@@ -323,12 +325,19 @@ class Client(object):
|
|
|
323
325
|
person_properties: A dictionary of person properties.
|
|
324
326
|
group_properties: A dictionary of group properties.
|
|
325
327
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
328
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
329
|
+
only these flags will be evaluated, improving performance.
|
|
326
330
|
|
|
327
331
|
Category:
|
|
328
332
|
Feature Flags
|
|
329
333
|
"""
|
|
330
334
|
resp_data = self.get_flags_decision(
|
|
331
|
-
distinct_id,
|
|
335
|
+
distinct_id,
|
|
336
|
+
groups,
|
|
337
|
+
person_properties,
|
|
338
|
+
group_properties,
|
|
339
|
+
disable_geoip,
|
|
340
|
+
flag_keys_to_evaluate,
|
|
332
341
|
)
|
|
333
342
|
return to_values(resp_data) or {}
|
|
334
343
|
|
|
@@ -339,6 +348,7 @@ class Client(object):
|
|
|
339
348
|
person_properties=None,
|
|
340
349
|
group_properties=None,
|
|
341
350
|
disable_geoip=None,
|
|
351
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
342
352
|
) -> dict[str, str]:
|
|
343
353
|
"""
|
|
344
354
|
Get feature flag payloads for a user by calling decide.
|
|
@@ -349,6 +359,8 @@ class Client(object):
|
|
|
349
359
|
person_properties: A dictionary of person properties.
|
|
350
360
|
group_properties: A dictionary of group properties.
|
|
351
361
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
362
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
363
|
+
only these flags will be evaluated, improving performance.
|
|
352
364
|
|
|
353
365
|
Examples:
|
|
354
366
|
```python
|
|
@@ -359,7 +371,12 @@ class Client(object):
|
|
|
359
371
|
Feature Flags
|
|
360
372
|
"""
|
|
361
373
|
resp_data = self.get_flags_decision(
|
|
362
|
-
distinct_id,
|
|
374
|
+
distinct_id,
|
|
375
|
+
groups,
|
|
376
|
+
person_properties,
|
|
377
|
+
group_properties,
|
|
378
|
+
disable_geoip,
|
|
379
|
+
flag_keys_to_evaluate,
|
|
363
380
|
)
|
|
364
381
|
return to_payloads(resp_data) or {}
|
|
365
382
|
|
|
@@ -370,6 +387,7 @@ class Client(object):
|
|
|
370
387
|
person_properties=None,
|
|
371
388
|
group_properties=None,
|
|
372
389
|
disable_geoip=None,
|
|
390
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
373
391
|
) -> FlagsAndPayloads:
|
|
374
392
|
"""
|
|
375
393
|
Get feature flags and payloads for a user by calling decide.
|
|
@@ -380,6 +398,8 @@ class Client(object):
|
|
|
380
398
|
person_properties: A dictionary of person properties.
|
|
381
399
|
group_properties: A dictionary of group properties.
|
|
382
400
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
401
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
402
|
+
only these flags will be evaluated, improving performance.
|
|
383
403
|
|
|
384
404
|
Examples:
|
|
385
405
|
```python
|
|
@@ -390,7 +410,12 @@ class Client(object):
|
|
|
390
410
|
Feature Flags
|
|
391
411
|
"""
|
|
392
412
|
resp = self.get_flags_decision(
|
|
393
|
-
distinct_id,
|
|
413
|
+
distinct_id,
|
|
414
|
+
groups,
|
|
415
|
+
person_properties,
|
|
416
|
+
group_properties,
|
|
417
|
+
disable_geoip,
|
|
418
|
+
flag_keys_to_evaluate,
|
|
394
419
|
)
|
|
395
420
|
return to_flags_and_payloads(resp)
|
|
396
421
|
|
|
@@ -401,6 +426,7 @@ class Client(object):
|
|
|
401
426
|
person_properties=None,
|
|
402
427
|
group_properties=None,
|
|
403
428
|
disable_geoip=None,
|
|
429
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
404
430
|
) -> FlagsResponse:
|
|
405
431
|
"""
|
|
406
432
|
Get feature flags decision.
|
|
@@ -411,6 +437,8 @@ class Client(object):
|
|
|
411
437
|
person_properties: A dictionary of person properties.
|
|
412
438
|
group_properties: A dictionary of group properties.
|
|
413
439
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
440
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
441
|
+
only these flags will be evaluated, improving performance.
|
|
414
442
|
|
|
415
443
|
Examples:
|
|
416
444
|
```python
|
|
@@ -441,6 +469,9 @@ class Client(object):
|
|
|
441
469
|
"geoip_disable": disable_geoip,
|
|
442
470
|
}
|
|
443
471
|
|
|
472
|
+
if flag_keys_to_evaluate:
|
|
473
|
+
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
|
|
474
|
+
|
|
444
475
|
resp_data = flags(
|
|
445
476
|
self.api_key,
|
|
446
477
|
self.host,
|
|
@@ -545,6 +576,7 @@ class Client(object):
|
|
|
545
576
|
group_properties=flag_options["group_properties"],
|
|
546
577
|
disable_geoip=disable_geoip,
|
|
547
578
|
only_evaluate_locally=True,
|
|
579
|
+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
|
|
548
580
|
)
|
|
549
581
|
else:
|
|
550
582
|
# Default behavior - use remote evaluation
|
|
@@ -554,6 +586,7 @@ class Client(object):
|
|
|
554
586
|
person_properties=flag_options["person_properties"],
|
|
555
587
|
group_properties=flag_options["group_properties"],
|
|
556
588
|
disable_geoip=disable_geoip,
|
|
589
|
+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
|
|
557
590
|
)
|
|
558
591
|
except Exception as e:
|
|
559
592
|
self.log.exception(
|
|
@@ -586,7 +619,7 @@ class Client(object):
|
|
|
586
619
|
|
|
587
620
|
return self._enqueue(msg, disable_geoip)
|
|
588
621
|
|
|
589
|
-
def _parse_send_feature_flags(self, send_feature_flags) ->
|
|
622
|
+
def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
|
|
590
623
|
"""
|
|
591
624
|
Parse and normalize send_feature_flags parameter into a standard format.
|
|
592
625
|
|
|
@@ -594,8 +627,8 @@ class Client(object):
|
|
|
594
627
|
send_feature_flags: Either bool or SendFeatureFlagsOptions dict
|
|
595
628
|
|
|
596
629
|
Returns:
|
|
597
|
-
|
|
598
|
-
person_properties, group_properties
|
|
630
|
+
SendFeatureFlagsOptions: Normalized options with keys: should_send, only_evaluate_locally,
|
|
631
|
+
person_properties, group_properties, flag_keys_filter
|
|
599
632
|
|
|
600
633
|
Raises:
|
|
601
634
|
TypeError: If send_feature_flags is not bool or dict
|
|
@@ -608,6 +641,7 @@ class Client(object):
|
|
|
608
641
|
),
|
|
609
642
|
"person_properties": send_feature_flags.get("person_properties"),
|
|
610
643
|
"group_properties": send_feature_flags.get("group_properties"),
|
|
644
|
+
"flag_keys_filter": send_feature_flags.get("flag_keys_filter"),
|
|
611
645
|
}
|
|
612
646
|
elif isinstance(send_feature_flags, bool):
|
|
613
647
|
return {
|
|
@@ -615,6 +649,7 @@ class Client(object):
|
|
|
615
649
|
"only_evaluate_locally": None,
|
|
616
650
|
"person_properties": None,
|
|
617
651
|
"group_properties": None,
|
|
652
|
+
"flag_keys_filter": None,
|
|
618
653
|
}
|
|
619
654
|
else:
|
|
620
655
|
raise TypeError(
|
|
@@ -1169,6 +1204,9 @@ class Client(object):
|
|
|
1169
1204
|
person_properties = person_properties or {}
|
|
1170
1205
|
group_properties = group_properties or {}
|
|
1171
1206
|
|
|
1207
|
+
# Create evaluation cache for flag dependencies
|
|
1208
|
+
evaluation_cache: dict[str, Optional[FlagValue]] = {}
|
|
1209
|
+
|
|
1172
1210
|
if feature_flag.get("ensure_experience_continuity", False):
|
|
1173
1211
|
raise InconclusiveMatchError("Flag has experience continuity enabled")
|
|
1174
1212
|
|
|
@@ -1184,12 +1222,12 @@ class Client(object):
|
|
|
1184
1222
|
self.log.warning(
|
|
1185
1223
|
f"[FEATURE FLAGS] Unknown group type index {aggregation_group_type_index} for feature flag {feature_flag['key']}"
|
|
1186
1224
|
)
|
|
1187
|
-
# failover to `/
|
|
1225
|
+
# failover to `/flags`
|
|
1188
1226
|
raise InconclusiveMatchError("Flag has unknown group type index")
|
|
1189
1227
|
|
|
1190
1228
|
if group_name not in groups:
|
|
1191
1229
|
# Group flags are never enabled in `groups` aren't passed in
|
|
1192
|
-
# don't failover to `/
|
|
1230
|
+
# don't failover to `/flags`, since response will be the same
|
|
1193
1231
|
if warn_on_unknown_groups:
|
|
1194
1232
|
self.log.warning(
|
|
1195
1233
|
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
|
|
@@ -1202,11 +1240,20 @@ class Client(object):
|
|
|
1202
1240
|
|
|
1203
1241
|
focused_group_properties = group_properties[group_name]
|
|
1204
1242
|
return match_feature_flag_properties(
|
|
1205
|
-
feature_flag,
|
|
1243
|
+
feature_flag,
|
|
1244
|
+
groups[group_name],
|
|
1245
|
+
focused_group_properties,
|
|
1246
|
+
self.feature_flags_by_key,
|
|
1247
|
+
evaluation_cache,
|
|
1206
1248
|
)
|
|
1207
1249
|
else:
|
|
1208
1250
|
return match_feature_flag_properties(
|
|
1209
|
-
feature_flag,
|
|
1251
|
+
feature_flag,
|
|
1252
|
+
distinct_id,
|
|
1253
|
+
person_properties,
|
|
1254
|
+
self.cohorts,
|
|
1255
|
+
self.feature_flags_by_key,
|
|
1256
|
+
evaluation_cache,
|
|
1210
1257
|
)
|
|
1211
1258
|
|
|
1212
1259
|
def feature_enabled(
|
|
@@ -1317,7 +1364,7 @@ class Client(object):
|
|
|
1317
1364
|
)
|
|
1318
1365
|
elif not only_evaluate_locally:
|
|
1319
1366
|
try:
|
|
1320
|
-
flag_details, request_id = self.
|
|
1367
|
+
flag_details, request_id = self._get_feature_flag_details_from_server(
|
|
1321
1368
|
key,
|
|
1322
1369
|
distinct_id,
|
|
1323
1370
|
groups,
|
|
@@ -1557,7 +1604,7 @@ class Client(object):
|
|
|
1557
1604
|
)
|
|
1558
1605
|
return feature_flag_result.payload if feature_flag_result else None
|
|
1559
1606
|
|
|
1560
|
-
def
|
|
1607
|
+
def _get_feature_flag_details_from_server(
|
|
1561
1608
|
self,
|
|
1562
1609
|
key: str,
|
|
1563
1610
|
distinct_id: ID_TYPES,
|
|
@@ -1567,10 +1614,15 @@ class Client(object):
|
|
|
1567
1614
|
disable_geoip: Optional[bool],
|
|
1568
1615
|
) -> tuple[Optional[FeatureFlag], Optional[str]]:
|
|
1569
1616
|
"""
|
|
1570
|
-
Calls /
|
|
1617
|
+
Calls /flags and returns the flag details and request id
|
|
1571
1618
|
"""
|
|
1572
1619
|
resp_data = self.get_flags_decision(
|
|
1573
|
-
distinct_id,
|
|
1620
|
+
distinct_id,
|
|
1621
|
+
groups,
|
|
1622
|
+
person_properties,
|
|
1623
|
+
group_properties,
|
|
1624
|
+
disable_geoip,
|
|
1625
|
+
flag_keys_to_evaluate=[key],
|
|
1574
1626
|
)
|
|
1575
1627
|
request_id = resp_data.get("requestId")
|
|
1576
1628
|
flags = resp_data.get("flags")
|
|
@@ -1686,6 +1738,7 @@ class Client(object):
|
|
|
1686
1738
|
group_properties=None,
|
|
1687
1739
|
only_evaluate_locally=False,
|
|
1688
1740
|
disable_geoip=None,
|
|
1741
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1689
1742
|
) -> Optional[dict[str, Union[bool, str]]]:
|
|
1690
1743
|
"""
|
|
1691
1744
|
Get all feature flags for a user.
|
|
@@ -1697,6 +1750,8 @@ class Client(object):
|
|
|
1697
1750
|
group_properties: A dictionary of group properties.
|
|
1698
1751
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1699
1752
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1753
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
1754
|
+
only these flags will be evaluated, improving performance.
|
|
1700
1755
|
|
|
1701
1756
|
Examples:
|
|
1702
1757
|
```python
|
|
@@ -1713,6 +1768,7 @@ class Client(object):
|
|
|
1713
1768
|
group_properties=group_properties,
|
|
1714
1769
|
only_evaluate_locally=only_evaluate_locally,
|
|
1715
1770
|
disable_geoip=disable_geoip,
|
|
1771
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1716
1772
|
)
|
|
1717
1773
|
|
|
1718
1774
|
return response["featureFlags"]
|
|
@@ -1726,6 +1782,7 @@ class Client(object):
|
|
|
1726
1782
|
group_properties=None,
|
|
1727
1783
|
only_evaluate_locally=False,
|
|
1728
1784
|
disable_geoip=None,
|
|
1785
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1729
1786
|
) -> FlagsAndPayloads:
|
|
1730
1787
|
"""
|
|
1731
1788
|
Get all feature flags and their payloads for a user.
|
|
@@ -1737,6 +1794,8 @@ class Client(object):
|
|
|
1737
1794
|
group_properties: A dictionary of group properties.
|
|
1738
1795
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1739
1796
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1797
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
1798
|
+
only these flags will be evaluated, improving performance.
|
|
1740
1799
|
|
|
1741
1800
|
Examples:
|
|
1742
1801
|
```python
|
|
@@ -1760,6 +1819,7 @@ class Client(object):
|
|
|
1760
1819
|
groups=groups,
|
|
1761
1820
|
person_properties=person_properties,
|
|
1762
1821
|
group_properties=group_properties,
|
|
1822
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1763
1823
|
)
|
|
1764
1824
|
|
|
1765
1825
|
if fallback_to_decide and not only_evaluate_locally:
|
|
@@ -1770,6 +1830,7 @@ class Client(object):
|
|
|
1770
1830
|
person_properties=person_properties,
|
|
1771
1831
|
group_properties=group_properties,
|
|
1772
1832
|
disable_geoip=disable_geoip,
|
|
1833
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1773
1834
|
)
|
|
1774
1835
|
return to_flags_and_payloads(decide_response)
|
|
1775
1836
|
except Exception as e:
|
|
@@ -1787,6 +1848,7 @@ class Client(object):
|
|
|
1787
1848
|
person_properties=None,
|
|
1788
1849
|
group_properties=None,
|
|
1789
1850
|
warn_on_unknown_groups=False,
|
|
1851
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1790
1852
|
) -> tuple[FlagsAndPayloads, bool]:
|
|
1791
1853
|
person_properties = person_properties or {}
|
|
1792
1854
|
group_properties = group_properties or {}
|
|
@@ -1799,7 +1861,15 @@ class Client(object):
|
|
|
1799
1861
|
fallback_to_decide = False
|
|
1800
1862
|
# If loading in previous line failed
|
|
1801
1863
|
if self.feature_flags:
|
|
1802
|
-
|
|
1864
|
+
# Filter flags based on flag_keys_to_evaluate if provided
|
|
1865
|
+
flags_to_process = self.feature_flags
|
|
1866
|
+
if flag_keys_to_evaluate:
|
|
1867
|
+
flag_keys_set = set(flag_keys_to_evaluate)
|
|
1868
|
+
flags_to_process = [
|
|
1869
|
+
flag for flag in self.feature_flags if flag["key"] in flag_keys_set
|
|
1870
|
+
]
|
|
1871
|
+
|
|
1872
|
+
for flag in flags_to_process:
|
|
1803
1873
|
try:
|
|
1804
1874
|
flags[flag["key"]] = self._compute_flag_locally(
|
|
1805
1875
|
flag,
|
|
@@ -1815,7 +1885,7 @@ class Client(object):
|
|
|
1815
1885
|
if matched_payload is not None:
|
|
1816
1886
|
payloads[flag["key"]] = matched_payload
|
|
1817
1887
|
except InconclusiveMatchError:
|
|
1818
|
-
# No need to log this, since it's just telling us to fall back to `/
|
|
1888
|
+
# No need to log this, since it's just telling us to fall back to `/flags`
|
|
1819
1889
|
fallback_to_decide = True
|
|
1820
1890
|
except Exception as e:
|
|
1821
1891
|
self.log.exception(
|
|
@@ -1939,7 +2009,7 @@ class Client(object):
|
|
|
1939
2009
|
for group_name in groups:
|
|
1940
2010
|
all_group_properties[group_name] = {
|
|
1941
2011
|
"$group_key": groups[group_name],
|
|
1942
|
-
**(group_properties.get(group_name) or {}),
|
|
2012
|
+
**((group_properties or {}).get(group_name) or {}),
|
|
1943
2013
|
}
|
|
1944
2014
|
|
|
1945
2015
|
return all_person_properties, all_group_properties
|
|
@@ -55,8 +55,100 @@ def variant_lookup_table(feature_flag):
|
|
|
55
55
|
return lookup_table
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def evaluate_flag_dependency(
|
|
59
|
+
property, flags_by_key, evaluation_cache, distinct_id, properties, cohort_properties
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Evaluate a flag dependency property according to the dependency chain algorithm.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
property: Flag property with type="flag" and dependency_chain
|
|
66
|
+
flags_by_key: Dictionary of all flags by their key
|
|
67
|
+
evaluation_cache: Cache for storing evaluation results
|
|
68
|
+
distinct_id: The distinct ID being evaluated
|
|
69
|
+
properties: Person properties for evaluation
|
|
70
|
+
cohort_properties: Cohort properties for evaluation
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
bool: True if all dependencies in the chain evaluate to True, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
if flags_by_key is None or evaluation_cache is None:
|
|
76
|
+
# Cannot evaluate flag dependencies without required context
|
|
77
|
+
raise InconclusiveMatchError(
|
|
78
|
+
f"Cannot evaluate flag dependency on '{property.get('key', 'unknown')}' without flags_by_key and evaluation_cache"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Check if dependency_chain is present - it should always be provided for flag dependencies
|
|
82
|
+
if "dependency_chain" not in property:
|
|
83
|
+
# Missing dependency_chain indicates malformed server data
|
|
84
|
+
raise InconclusiveMatchError(
|
|
85
|
+
f"Flag dependency property for '{property.get('key', 'unknown')}' is missing required 'dependency_chain' field"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
dependency_chain = property["dependency_chain"]
|
|
89
|
+
|
|
90
|
+
# Handle circular dependency (empty chain means circular)
|
|
91
|
+
if len(dependency_chain) == 0:
|
|
92
|
+
log.debug(f"Circular dependency detected for flag: {property.get('key')}")
|
|
93
|
+
raise InconclusiveMatchError(
|
|
94
|
+
f"Circular dependency detected for flag '{property.get('key', 'unknown')}'"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Evaluate all dependencies in the chain order
|
|
98
|
+
for dep_flag_key in dependency_chain:
|
|
99
|
+
if dep_flag_key not in evaluation_cache:
|
|
100
|
+
# Need to evaluate this dependency first
|
|
101
|
+
dep_flag = flags_by_key.get(dep_flag_key)
|
|
102
|
+
if not dep_flag:
|
|
103
|
+
# Missing flag dependency - cannot evaluate locally
|
|
104
|
+
evaluation_cache[dep_flag_key] = None
|
|
105
|
+
raise InconclusiveMatchError(
|
|
106
|
+
f"Cannot evaluate flag dependency '{dep_flag_key}' - flag not found in local flags"
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
# Check if the flag is active (same check as in client._compute_flag_locally)
|
|
110
|
+
if not dep_flag.get("active"):
|
|
111
|
+
evaluation_cache[dep_flag_key] = False
|
|
112
|
+
else:
|
|
113
|
+
# Recursively evaluate the dependency
|
|
114
|
+
try:
|
|
115
|
+
dep_result = match_feature_flag_properties(
|
|
116
|
+
dep_flag,
|
|
117
|
+
distinct_id,
|
|
118
|
+
properties,
|
|
119
|
+
cohort_properties,
|
|
120
|
+
flags_by_key,
|
|
121
|
+
evaluation_cache,
|
|
122
|
+
)
|
|
123
|
+
evaluation_cache[dep_flag_key] = dep_result
|
|
124
|
+
except InconclusiveMatchError as e:
|
|
125
|
+
# If we can't evaluate a dependency, store None and propagate the error
|
|
126
|
+
evaluation_cache[dep_flag_key] = None
|
|
127
|
+
raise InconclusiveMatchError(
|
|
128
|
+
f"Cannot evaluate flag dependency '{dep_flag_key}': {e}"
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
# Check the cached result
|
|
132
|
+
cached_result = evaluation_cache[dep_flag_key]
|
|
133
|
+
if cached_result is None:
|
|
134
|
+
# Previously inconclusive - raise error again
|
|
135
|
+
raise InconclusiveMatchError(
|
|
136
|
+
f"Flag dependency '{dep_flag_key}' was previously inconclusive"
|
|
137
|
+
)
|
|
138
|
+
elif not cached_result:
|
|
139
|
+
# Definitive False result - dependency failed
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
|
|
58
145
|
def match_feature_flag_properties(
|
|
59
|
-
flag,
|
|
146
|
+
flag,
|
|
147
|
+
distinct_id,
|
|
148
|
+
properties,
|
|
149
|
+
cohort_properties=None,
|
|
150
|
+
flags_by_key=None,
|
|
151
|
+
evaluation_cache=None,
|
|
60
152
|
) -> FlagValue:
|
|
61
153
|
flag_conditions = (flag.get("filters") or {}).get("groups") or []
|
|
62
154
|
is_inconclusive = False
|
|
@@ -79,7 +171,13 @@ def match_feature_flag_properties(
|
|
|
79
171
|
# if any one condition resolves to True, we can shortcircuit and return
|
|
80
172
|
# the matching variant
|
|
81
173
|
if is_condition_match(
|
|
82
|
-
flag,
|
|
174
|
+
flag,
|
|
175
|
+
distinct_id,
|
|
176
|
+
condition,
|
|
177
|
+
properties,
|
|
178
|
+
cohort_properties,
|
|
179
|
+
flags_by_key,
|
|
180
|
+
evaluation_cache,
|
|
83
181
|
):
|
|
84
182
|
variant_override = condition.get("variant")
|
|
85
183
|
if variant_override and variant_override in valid_variant_keys:
|
|
@@ -101,22 +199,36 @@ def match_feature_flag_properties(
|
|
|
101
199
|
|
|
102
200
|
|
|
103
201
|
def is_condition_match(
|
|
104
|
-
feature_flag,
|
|
202
|
+
feature_flag,
|
|
203
|
+
distinct_id,
|
|
204
|
+
condition,
|
|
205
|
+
properties,
|
|
206
|
+
cohort_properties,
|
|
207
|
+
flags_by_key=None,
|
|
208
|
+
evaluation_cache=None,
|
|
105
209
|
) -> bool:
|
|
106
210
|
rollout_percentage = condition.get("rollout_percentage")
|
|
107
211
|
if len(condition.get("properties") or []) > 0:
|
|
108
212
|
for prop in condition.get("properties"):
|
|
109
213
|
property_type = prop.get("type")
|
|
110
214
|
if property_type == "cohort":
|
|
111
|
-
matches = match_cohort(
|
|
215
|
+
matches = match_cohort(
|
|
216
|
+
prop,
|
|
217
|
+
properties,
|
|
218
|
+
cohort_properties,
|
|
219
|
+
flags_by_key,
|
|
220
|
+
evaluation_cache,
|
|
221
|
+
distinct_id,
|
|
222
|
+
)
|
|
112
223
|
elif property_type == "flag":
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
224
|
+
matches = evaluate_flag_dependency(
|
|
225
|
+
prop,
|
|
226
|
+
flags_by_key,
|
|
227
|
+
evaluation_cache,
|
|
228
|
+
distinct_id,
|
|
229
|
+
properties,
|
|
230
|
+
cohort_properties,
|
|
118
231
|
)
|
|
119
|
-
continue
|
|
120
232
|
else:
|
|
121
233
|
matches = match_property(prop, properties)
|
|
122
234
|
if not matches:
|
|
@@ -264,7 +376,14 @@ def match_property(property, property_values) -> bool:
|
|
|
264
376
|
raise InconclusiveMatchError(f"Unknown operator {operator}")
|
|
265
377
|
|
|
266
378
|
|
|
267
|
-
def match_cohort(
|
|
379
|
+
def match_cohort(
|
|
380
|
+
property,
|
|
381
|
+
property_values,
|
|
382
|
+
cohort_properties,
|
|
383
|
+
flags_by_key=None,
|
|
384
|
+
evaluation_cache=None,
|
|
385
|
+
distinct_id=None,
|
|
386
|
+
) -> bool:
|
|
268
387
|
# Cohort properties are in the form of property groups like this:
|
|
269
388
|
# {
|
|
270
389
|
# "cohort_id": {
|
|
@@ -281,10 +400,24 @@ def match_cohort(property, property_values, cohort_properties) -> bool:
|
|
|
281
400
|
)
|
|
282
401
|
|
|
283
402
|
property_group = cohort_properties[cohort_id]
|
|
284
|
-
return match_property_group(
|
|
403
|
+
return match_property_group(
|
|
404
|
+
property_group,
|
|
405
|
+
property_values,
|
|
406
|
+
cohort_properties,
|
|
407
|
+
flags_by_key,
|
|
408
|
+
evaluation_cache,
|
|
409
|
+
distinct_id,
|
|
410
|
+
)
|
|
285
411
|
|
|
286
412
|
|
|
287
|
-
def match_property_group(
|
|
413
|
+
def match_property_group(
|
|
414
|
+
property_group,
|
|
415
|
+
property_values,
|
|
416
|
+
cohort_properties,
|
|
417
|
+
flags_by_key=None,
|
|
418
|
+
evaluation_cache=None,
|
|
419
|
+
distinct_id=None,
|
|
420
|
+
) -> bool:
|
|
288
421
|
if not property_group:
|
|
289
422
|
return True
|
|
290
423
|
|
|
@@ -301,7 +434,14 @@ def match_property_group(property_group, property_values, cohort_properties) ->
|
|
|
301
434
|
# a nested property group
|
|
302
435
|
for prop in properties:
|
|
303
436
|
try:
|
|
304
|
-
matches = match_property_group(
|
|
437
|
+
matches = match_property_group(
|
|
438
|
+
prop,
|
|
439
|
+
property_values,
|
|
440
|
+
cohort_properties,
|
|
441
|
+
flags_by_key,
|
|
442
|
+
evaluation_cache,
|
|
443
|
+
distinct_id,
|
|
444
|
+
)
|
|
305
445
|
if property_group_type == "AND":
|
|
306
446
|
if not matches:
|
|
307
447
|
return False
|
|
@@ -324,14 +464,23 @@ def match_property_group(property_group, property_values, cohort_properties) ->
|
|
|
324
464
|
for prop in properties:
|
|
325
465
|
try:
|
|
326
466
|
if prop.get("type") == "cohort":
|
|
327
|
-
matches = match_cohort(
|
|
467
|
+
matches = match_cohort(
|
|
468
|
+
prop,
|
|
469
|
+
property_values,
|
|
470
|
+
cohort_properties,
|
|
471
|
+
flags_by_key,
|
|
472
|
+
evaluation_cache,
|
|
473
|
+
distinct_id,
|
|
474
|
+
)
|
|
328
475
|
elif prop.get("type") == "flag":
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
476
|
+
matches = evaluate_flag_dependency(
|
|
477
|
+
prop,
|
|
478
|
+
flags_by_key,
|
|
479
|
+
evaluation_cache,
|
|
480
|
+
distinct_id,
|
|
481
|
+
property_values,
|
|
482
|
+
cohort_properties,
|
|
333
483
|
)
|
|
334
|
-
continue
|
|
335
484
|
else:
|
|
336
485
|
matches = match_property(prop, property_values)
|
|
337
486
|
|