-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengine.py
More file actions
77 lines (54 loc) · 2.56 KB
/
Copy pathengine.py
File metadata and controls
77 lines (54 loc) · 2.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
"""Feature-flag rules stored as data, evaluated per user.
A rule is a sentence in a tiny boolean language::
("and", (">=", "age", 18), ("==", "country", "CA"))
Rules live in config (they're just tuples — JSON-serializable shapes), and
extending the language is one entry in ``OPERATIONS``. Leaves that name a
context field resolve to the user's value; anything else is a literal.
"""
from __future__ import annotations
from collections.abc import Mapping
from patterns.behavioral.interpreter.pattern import Expr, Interpreter, Operation, Value
def _cmp(pair: tuple[Value, ...]) -> tuple[float, float]:
left, right = pair
if isinstance(left, bool) or isinstance(right, bool):
raise ValueError("ordered comparison on booleans")
if not isinstance(left, int | float) or not isinstance(right, int | float):
raise ValueError(f"ordered comparison needs numbers, got {pair!r}")
return float(left), float(right)
def _all(args: tuple[Value, ...]) -> Value:
return all(bool(a) for a in args)
def _any(args: tuple[Value, ...]) -> Value:
return any(bool(a) for a in args)
def _not(args: tuple[Value, ...]) -> Value:
(only,) = args
return not bool(only)
OPERATIONS: dict[str, Operation] = {
"and": _all,
"or": _any,
"not": _not,
"==": lambda a: a[0] == a[1],
"!=": lambda a: a[0] != a[1],
">=": lambda a: _cmp(a)[0] >= _cmp(a)[1],
"<=": lambda a: _cmp(a)[0] <= _cmp(a)[1],
">": lambda a: _cmp(a)[0] > _cmp(a)[1],
"<": lambda a: _cmp(a)[0] < _cmp(a)[1],
}
class FlagEngine:
"""Evaluate named feature flags against a user context."""
def __init__(self, flags: Mapping[str, Expr]) -> None:
self._flags = dict(flags)
def is_enabled(self, flag: str, user: Mapping[str, Value]) -> bool:
"""True if ``flag``'s rule accepts this user; KeyError on unknown flag."""
if flag not in self._flags:
raise KeyError(f"unknown flag {flag!r} (has: {sorted(self._flags)})")
def resolve(leaf: Value) -> Value:
# A string leaf names a context field when the user has one;
# otherwise it is a literal ("CA" in a country comparison).
if isinstance(leaf, str) and leaf in user:
return user[leaf]
return leaf
interpreter = Interpreter(OPERATIONS, resolve=resolve)
return bool(interpreter.evaluate(self._flags[flag]))
def rollout(self, user: Mapping[str, Value]) -> dict[str, bool]:
"""Every flag's verdict for one user."""
return {flag: self.is_enabled(flag, user) for flag in sorted(self._flags)}