|
1 | | -import hashlib |
2 | 1 | import logging |
3 | | -from typing import Any, Awaitable, Callable, Optional, Set, Tuple |
| 2 | +from typing import Awaitable, Callable, Optional, Tuple |
4 | 3 |
|
5 | 4 | from ldclient.context import Context |
6 | 5 | from ldclient.evaluation import BigSegmentsStatus, EvaluationDetail |
7 | | -from ldclient.impl import operators |
8 | | -from ldclient.impl.evaluator_common import EvalResult, EvaluationException |
| 6 | +from ldclient.impl.evaluator_common import ( |
| 7 | + EvalResult, |
| 8 | + EvaluationException, |
| 9 | + _bucket_context, |
| 10 | + _bucketable_string_value, |
| 11 | + _context_key_is_in_target_list, |
| 12 | + _get_context_value_by_attr_ref, |
| 13 | + _get_off_value, |
| 14 | + _get_value_for_variation_or_rollout, |
| 15 | + _get_variation, |
| 16 | + _make_big_segment_ref, |
| 17 | + _match_clause_by_kind, |
| 18 | + _match_single_context_value, |
| 19 | + _maybe_negate, |
| 20 | + _target_match_result, |
| 21 | + _variation_index_for_context, |
| 22 | + error_reason |
| 23 | +) |
9 | 24 | from ldclient.impl.events.types import EventFactory |
10 | 25 | from ldclient.impl.model import * |
11 | 26 |
|
12 | 27 | # For consistency with past logging behavior, we are pretending that the evaluation logic still lives in |
13 | 28 | # the ldclient.evaluation module. |
14 | 29 | log = logging.getLogger('ldclient.flag') |
15 | 30 |
|
16 | | -__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF) |
17 | 31 |
|
18 | | -__BUILTINS__ = ["key", "secondary", "ip", "country", "email", "firstName", "lastName", "avatar", "name", "anonymous"] |
19 | | - |
20 | | - |
21 | | -# EvalResult is used internally to hold the EvaluationDetail result of an evaluation along with |
22 | | -# other side effects that are not exposed to the application, such as events generated by |
23 | | -# prerequisite evaluations, and the cached state of any Big Segments query that we may have |
24 | | -# ended up having to do for the context. |
25 | 32 | class AsyncEvaluator: |
26 | 33 | """ |
27 | 34 | Async version of Evaluator. Encapsulates the feature flag evaluation logic. The AsyncEvaluator |
@@ -291,162 +298,3 @@ async def _big_segment_match_context(self, segment: Segment, context: Context, s |
291 | 298 | if included is not None: |
292 | 299 | return included |
293 | 300 | return await self._simple_segment_match_context(segment, context, state, False) |
294 | | - |
295 | | - |
296 | | -# The following functions are declared outside AsyncEvaluator because they do not depend on any |
297 | | -# of AsyncEvaluator's state. They are also used by the generated sync Evaluator. |
298 | | - |
299 | | - |
300 | | -def _get_variation(flag: FeatureFlag, variation: int, reason: dict) -> EvaluationDetail: |
301 | | - vars = flag.variations |
302 | | - if variation < 0 or variation >= len(vars): |
303 | | - return EvaluationDetail(None, None, error_reason('MALFORMED_FLAG')) |
304 | | - return EvaluationDetail(vars[variation], variation, reason) |
305 | | - |
306 | | - |
307 | | -def _get_off_value(flag: FeatureFlag, reason: dict) -> EvaluationDetail: |
308 | | - off_var = flag.off_variation |
309 | | - if off_var is None: |
310 | | - return EvaluationDetail(None, None, reason) |
311 | | - return _get_variation(flag, off_var, reason) |
312 | | - |
313 | | - |
314 | | -def _get_value_for_variation_or_rollout(flag: FeatureFlag, vr: VariationOrRollout, context: Context, reason: dict) -> EvaluationDetail: |
315 | | - index, inExperiment = _variation_index_for_context(flag, vr, context) |
316 | | - if index is None: |
317 | | - return EvaluationDetail(None, None, error_reason('MALFORMED_FLAG')) |
318 | | - if inExperiment: |
319 | | - reason['inExperiment'] = inExperiment |
320 | | - return _get_variation(flag, index, reason) |
321 | | - |
322 | | - |
323 | | -def _variation_index_for_context(flag: FeatureFlag, vr: VariationOrRollout, context: Context) -> Tuple[Optional[int], bool]: |
324 | | - var = vr.variation |
325 | | - if var is not None: |
326 | | - return var, False |
327 | | - |
328 | | - rollout = vr.rollout |
329 | | - if rollout is None: |
330 | | - return None, False |
331 | | - variations = rollout.variations |
332 | | - if len(variations) == 0: |
333 | | - return None, False |
334 | | - |
335 | | - bucket_by = None if rollout.is_experiment else rollout.bucket_by |
336 | | - bucket = _bucket_context(rollout.seed, context, rollout.context_kind, flag.key, flag.salt, bucket_by) |
337 | | - is_experiment = rollout.is_experiment and bucket >= 0 |
338 | | - # _bucket_context returns a negative value if the context didn't exist, in which case we |
339 | | - # still end up returning the first bucket, but we will force the "in experiment" state to be false. |
340 | | - |
341 | | - sum = 0.0 |
342 | | - for wv in variations: |
343 | | - sum += wv.weight / 100000.0 |
344 | | - if bucket < sum: |
345 | | - is_experiment_partition = is_experiment and not wv.untracked |
346 | | - return wv.variation, is_experiment_partition |
347 | | - |
348 | | - # The context's bucket value was greater than or equal to the end of the last bucket. This could happen due |
349 | | - # to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag |
350 | | - # data could contain buckets that don't actually add up to 100000. Rather than returning an error in |
351 | | - # this case (or changing the scaling, which would potentially change the results for *all* contexts), we |
352 | | - # will simply put the context in the last bucket. |
353 | | - is_experiment_partition = is_experiment and not variations[-1].untracked |
354 | | - return variations[-1].variation, is_experiment_partition |
355 | | - |
356 | | - |
357 | | -def _bucket_context(seed: Optional[int], context: Context, context_kind: Optional[str], key: str, salt: str, bucket_by: Optional[AttributeRef]) -> float: |
358 | | - match_context = context.get_individual_context(context_kind or Context.DEFAULT_KIND) |
359 | | - if match_context is None: |
360 | | - return -1 |
361 | | - clause_value = match_context.key if bucket_by is None else _get_context_value_by_attr_ref(match_context, bucket_by) |
362 | | - if clause_value is None: |
363 | | - return 0.0 |
364 | | - bucket_by_value = _bucketable_string_value(clause_value) |
365 | | - if bucket_by_value is None: |
366 | | - return 0.0 |
367 | | - id_hash = clause_value |
368 | | - if seed is not None: |
369 | | - prefix = str(seed) |
370 | | - else: |
371 | | - prefix = '%s.%s' % (key, salt) |
372 | | - hash_key = '%s.%s' % (prefix, id_hash) |
373 | | - hash_val = int(hashlib.sha1(hash_key.encode('utf-8')).hexdigest()[:15], 16) |
374 | | - result = hash_val / __LONG_SCALE__ |
375 | | - return result |
376 | | - |
377 | | - |
378 | | -def _bucketable_string_value(u_value) -> Optional[str]: |
379 | | - if isinstance(u_value, bool): |
380 | | - return None |
381 | | - elif isinstance(u_value, (str, int)): |
382 | | - return str(u_value) |
383 | | - |
384 | | - return None |
385 | | - |
386 | | - |
387 | | -def _context_key_is_in_target_list(context: Context, context_kind: Optional[str], keys: Set[str]) -> bool: |
388 | | - if keys is None or len(keys) == 0: |
389 | | - return False |
390 | | - match_context = context.get_individual_context(context_kind or Context.DEFAULT_KIND) |
391 | | - return match_context is not None and match_context.key in keys |
392 | | - |
393 | | - |
394 | | -def _get_context_value_by_attr_ref(context: Context, attr: AttributeRef) -> Any: |
395 | | - if attr is None: |
396 | | - raise EvaluationException("rule clause did not specify an attribute") |
397 | | - if attr.error is not None: |
398 | | - raise EvaluationException("invalid attribute reference: " + attr.error) |
399 | | - name = attr[0] |
400 | | - if name is None: |
401 | | - return None |
402 | | - value = context.get(name) |
403 | | - depth = attr.depth |
404 | | - i = 1 |
405 | | - while i < depth: |
406 | | - if not isinstance(value, dict): |
407 | | - return None # can't get subproperty if we're not in a JSON object |
408 | | - value = value.get(attr[i]) |
409 | | - i += 1 |
410 | | - return value |
411 | | - |
412 | | - |
413 | | -def _match_single_context_value(clause: Clause, context_value: Any) -> bool: |
414 | | - op_fn = operators.ops.get(clause.op) |
415 | | - if op_fn is None: |
416 | | - return False |
417 | | - values_preprocessed = clause.values_preprocessed |
418 | | - for i, v in enumerate(clause.values): |
419 | | - preprocessed = None if values_preprocessed is None else values_preprocessed[i] |
420 | | - if op_fn(context_value, v, preprocessed): |
421 | | - return True |
422 | | - return False |
423 | | - |
424 | | - |
425 | | -def _match_clause_by_kind(clause: Clause, context: Context) -> bool: |
426 | | - # If attribute is "kind", then we treat operator and values as a match expression against a list |
427 | | - # of all individual kinds in the context. That is, for a multi-kind context with kinds of "org" |
428 | | - # and "user", it is a match if either of those strings is a match with Operator and Values. |
429 | | - for i in range(context.individual_context_count): |
430 | | - c = context.get_individual_context(i) |
431 | | - if c is not None and _match_single_context_value(clause, c.kind): |
432 | | - return True |
433 | | - return False |
434 | | - |
435 | | - |
436 | | -def _maybe_negate(clause: Clause, val: bool) -> bool: |
437 | | - return not val if clause.negate else val |
438 | | - |
439 | | - |
440 | | -def _make_big_segment_ref(segment: Segment) -> str: |
441 | | - # The format of Big Segment references is independent of what store implementation is being |
442 | | - # used; the store implementation receives only this string and does not know the details of |
443 | | - # the data model. The Relay Proxy will use the same format when writing to the store. |
444 | | - return "%s.g%d" % (segment.key, segment.generation or 0) |
445 | | - |
446 | | - |
447 | | -def _target_match_result(flag: FeatureFlag, var: int) -> EvaluationDetail: |
448 | | - return _get_variation(flag, var, {'kind': 'TARGET_MATCH'}) |
449 | | - |
450 | | - |
451 | | -def error_reason(error_kind: str) -> dict: |
452 | | - return {'kind': 'ERROR', 'errorKind': error_kind} |
0 commit comments