openfeature-provider-flagd 0.1.5__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- openfeature/.gitignore +2 -0
- openfeature/contrib/provider/flagd/config.py +214 -23
- openfeature/contrib/provider/flagd/provider.py +88 -12
- openfeature/contrib/provider/flagd/resolvers/__init__.py +1 -47
- openfeature/contrib/provider/flagd/resolvers/grpc.py +229 -17
- openfeature/contrib/provider/flagd/resolvers/in_process.py +40 -31
- openfeature/contrib/provider/flagd/resolvers/process/connector/__init__.py +11 -0
- openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py +107 -0
- openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +218 -0
- openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +58 -19
- openfeature/contrib/provider/flagd/resolvers/process/flags.py +50 -6
- openfeature/contrib/provider/flagd/resolvers/process/targeting.py +35 -0
- openfeature/contrib/provider/flagd/resolvers/protocol.py +47 -0
- openfeature/contrib/provider/flagd/sync_metadata_hook.py +14 -0
- openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.py +72 -0
- openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.pyi +450 -0
- openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.py +358 -0
- openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.pyi +155 -0
- openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.py +50 -0
- openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.pyi +148 -0
- openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.py +186 -0
- openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.pyi +86 -0
- openfeature/schemas/protobuf/schema/v1/schema_pb2.py +72 -0
- openfeature/schemas/protobuf/schema/v1/schema_pb2.pyi +451 -0
- openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.py +358 -0
- openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.pyi +156 -0
- openfeature/schemas/protobuf/sync/v1/sync_service_pb2.py +47 -0
- openfeature/schemas/protobuf/sync/v1/sync_service_pb2.pyi +174 -0
- openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.py +143 -0
- openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.pyi +70 -0
- {openfeature_provider_flagd-0.1.5.dist-info → openfeature_provider_flagd-0.2.1.dist-info}/METADATA +116 -15
- openfeature_provider_flagd-0.2.1.dist-info/RECORD +36 -0
- {openfeature_provider_flagd-0.1.5.dist-info → openfeature_provider_flagd-0.2.1.dist-info}/WHEEL +1 -1
- {openfeature_provider_flagd-0.1.5.dist-info → openfeature_provider_flagd-0.2.1.dist-info}/licenses/LICENSE +1 -1
- openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2.py +0 -62
- openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2_grpc.py +0 -267
- openfeature/contrib/provider/flagd/proto/flagd/sync/v1/sync_pb2.py +0 -40
- openfeature/contrib/provider/flagd/proto/flagd/sync/v1/sync_pb2_grpc.py +0 -135
- openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2.py +0 -62
- openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2_grpc.py +0 -267
- openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2.py +0 -37
- openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2_grpc.py +0 -102
- openfeature/contrib/provider/flagd/resolvers/process/file_watcher.py +0 -89
- openfeature_provider_flagd-0.1.5.dist-info/RECORD +0 -22
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
from google.protobuf.json_format import MessageToDict
|
|
9
|
+
|
|
10
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
11
|
+
from openfeature.event import ProviderEventDetails
|
|
12
|
+
from openfeature.exception import ErrorCode, ParseError, ProviderNotReadyError
|
|
13
|
+
from openfeature.schemas.protobuf.flagd.sync.v1 import (
|
|
14
|
+
sync_pb2,
|
|
15
|
+
sync_pb2_grpc,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ....config import Config
|
|
19
|
+
from ..connector import FlagStateConnector
|
|
20
|
+
from ..flags import FlagStore
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("openfeature.contrib")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GrpcWatcher(FlagStateConnector):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
config: Config,
|
|
29
|
+
flag_store: FlagStore,
|
|
30
|
+
emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None],
|
|
31
|
+
emit_provider_error: typing.Callable[[ProviderEventDetails], None],
|
|
32
|
+
emit_provider_stale: typing.Callable[[ProviderEventDetails], None],
|
|
33
|
+
):
|
|
34
|
+
self.flag_store = flag_store
|
|
35
|
+
self.config = config
|
|
36
|
+
|
|
37
|
+
self.channel = self._generate_channel(config)
|
|
38
|
+
self.stub = sync_pb2_grpc.FlagSyncServiceStub(self.channel)
|
|
39
|
+
self.retry_backoff_seconds = config.retry_backoff_ms * 0.001
|
|
40
|
+
self.retry_backoff_max_seconds = config.retry_backoff_ms * 0.001
|
|
41
|
+
self.retry_grace_period = config.retry_grace_period
|
|
42
|
+
self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001
|
|
43
|
+
self.deadline = config.deadline_ms * 0.001
|
|
44
|
+
self.selector = config.selector
|
|
45
|
+
self.provider_id = config.provider_id
|
|
46
|
+
self.emit_provider_ready = emit_provider_ready
|
|
47
|
+
self.emit_provider_error = emit_provider_error
|
|
48
|
+
self.emit_provider_stale = emit_provider_stale
|
|
49
|
+
|
|
50
|
+
self.connected = False
|
|
51
|
+
self.thread: typing.Optional[threading.Thread] = None
|
|
52
|
+
self.timer: typing.Optional[threading.Timer] = None
|
|
53
|
+
|
|
54
|
+
self.start_time = time.time()
|
|
55
|
+
|
|
56
|
+
def _generate_channel(self, config: Config) -> grpc.Channel:
|
|
57
|
+
target = f"{config.host}:{config.port}"
|
|
58
|
+
# Create the channel with the service config
|
|
59
|
+
options: list[tuple[str, typing.Any]] = [
|
|
60
|
+
("grpc.keepalive_time_ms", config.keep_alive_time),
|
|
61
|
+
("grpc.initial_reconnect_backoff_ms", config.retry_backoff_ms),
|
|
62
|
+
("grpc.max_reconnect_backoff_ms", config.retry_backoff_max_ms),
|
|
63
|
+
("grpc.min_reconnect_backoff_ms", config.stream_deadline_ms),
|
|
64
|
+
]
|
|
65
|
+
if config.default_authority is not None:
|
|
66
|
+
options.append(("grpc.default_authority", config.default_authority))
|
|
67
|
+
|
|
68
|
+
if config.channel_credentials is not None:
|
|
69
|
+
channel_args = {
|
|
70
|
+
"options": options,
|
|
71
|
+
"credentials": config.channel_credentials,
|
|
72
|
+
}
|
|
73
|
+
channel = grpc.secure_channel(target, **channel_args)
|
|
74
|
+
|
|
75
|
+
elif config.tls:
|
|
76
|
+
channel_args = {
|
|
77
|
+
"options": options,
|
|
78
|
+
"credentials": grpc.ssl_channel_credentials(),
|
|
79
|
+
}
|
|
80
|
+
if config.cert_path:
|
|
81
|
+
with open(config.cert_path, "rb") as f:
|
|
82
|
+
channel_args["credentials"] = grpc.ssl_channel_credentials(f.read())
|
|
83
|
+
|
|
84
|
+
channel = grpc.secure_channel(target, **channel_args)
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
channel = grpc.insecure_channel(
|
|
88
|
+
target,
|
|
89
|
+
options=options,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return channel
|
|
93
|
+
|
|
94
|
+
def initialize(self, context: EvaluationContext) -> None:
|
|
95
|
+
self.connect()
|
|
96
|
+
|
|
97
|
+
def connect(self) -> None:
|
|
98
|
+
self.active = True
|
|
99
|
+
|
|
100
|
+
# Run monitoring in a separate thread
|
|
101
|
+
self.monitor_thread = threading.Thread(
|
|
102
|
+
target=self.monitor, daemon=True, name="FlagdGrpcSyncServiceMonitorThread"
|
|
103
|
+
)
|
|
104
|
+
self.monitor_thread.start()
|
|
105
|
+
## block until ready or deadline reached
|
|
106
|
+
timeout = self.deadline + time.time()
|
|
107
|
+
while not self.connected and time.time() < timeout:
|
|
108
|
+
time.sleep(0.05)
|
|
109
|
+
logger.debug("Finished blocking gRPC state initialization")
|
|
110
|
+
|
|
111
|
+
if not self.connected:
|
|
112
|
+
raise ProviderNotReadyError(
|
|
113
|
+
"Blocking init finished before data synced. Consider increasing startup deadline to avoid inconsistent evaluations."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def monitor(self) -> None:
|
|
117
|
+
self.channel.subscribe(self._state_change_callback, try_to_connect=True)
|
|
118
|
+
|
|
119
|
+
def _state_change_callback(self, new_state: grpc.ChannelConnectivity) -> None:
|
|
120
|
+
logger.debug(f"gRPC state change: {new_state}")
|
|
121
|
+
if (
|
|
122
|
+
new_state == grpc.ChannelConnectivity.READY
|
|
123
|
+
or new_state == grpc.ChannelConnectivity.IDLE
|
|
124
|
+
):
|
|
125
|
+
if not self.thread or not self.thread.is_alive():
|
|
126
|
+
self.thread = threading.Thread(
|
|
127
|
+
target=self.listen,
|
|
128
|
+
daemon=True,
|
|
129
|
+
name="FlagdGrpcSyncWorkerThread",
|
|
130
|
+
)
|
|
131
|
+
self.thread.start()
|
|
132
|
+
|
|
133
|
+
if self.timer and self.timer.is_alive():
|
|
134
|
+
logger.debug("gRPC error timer expired")
|
|
135
|
+
self.timer.cancel()
|
|
136
|
+
|
|
137
|
+
elif new_state == grpc.ChannelConnectivity.TRANSIENT_FAILURE:
|
|
138
|
+
# this is the failed reconnect attempt so we are going into stale
|
|
139
|
+
self.emit_provider_stale(
|
|
140
|
+
ProviderEventDetails(
|
|
141
|
+
message="gRPC sync disconnected, reconnecting",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
self.start_time = time.time()
|
|
145
|
+
# adding a timer, so we can emit the error event after time
|
|
146
|
+
self.timer = threading.Timer(self.retry_grace_period, self.emit_error)
|
|
147
|
+
|
|
148
|
+
logger.debug("gRPC error timer started")
|
|
149
|
+
self.timer.start()
|
|
150
|
+
self.connected = False
|
|
151
|
+
|
|
152
|
+
def emit_error(self) -> None:
|
|
153
|
+
logger.debug("gRPC error emitted")
|
|
154
|
+
self.emit_provider_error(
|
|
155
|
+
ProviderEventDetails(
|
|
156
|
+
message="gRPC sync disconnected, reconnecting",
|
|
157
|
+
error_code=ErrorCode.GENERAL,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def shutdown(self) -> None:
|
|
162
|
+
self.active = False
|
|
163
|
+
self.channel.close()
|
|
164
|
+
|
|
165
|
+
def listen(self) -> None:
|
|
166
|
+
call_args = (
|
|
167
|
+
{"timeout": self.streamline_deadline_seconds}
|
|
168
|
+
if self.streamline_deadline_seconds > 0
|
|
169
|
+
else {}
|
|
170
|
+
)
|
|
171
|
+
request_args = {}
|
|
172
|
+
if self.selector is not None:
|
|
173
|
+
request_args["selector"] = self.selector
|
|
174
|
+
if self.provider_id is not None:
|
|
175
|
+
request_args["provider_id"] = self.provider_id
|
|
176
|
+
|
|
177
|
+
while self.active:
|
|
178
|
+
try:
|
|
179
|
+
context_values_request = sync_pb2.GetMetadataRequest()
|
|
180
|
+
context_values_response: sync_pb2.GetMetadataResponse = (
|
|
181
|
+
self.stub.GetMetadata(context_values_request, wait_for_ready=True)
|
|
182
|
+
)
|
|
183
|
+
context_values = MessageToDict(context_values_response)
|
|
184
|
+
|
|
185
|
+
request = sync_pb2.SyncFlagsRequest(**request_args)
|
|
186
|
+
|
|
187
|
+
logger.debug("Setting up gRPC sync flags connection")
|
|
188
|
+
for flag_rsp in self.stub.SyncFlags(
|
|
189
|
+
request, wait_for_ready=True, **call_args
|
|
190
|
+
):
|
|
191
|
+
flag_str = flag_rsp.flag_configuration
|
|
192
|
+
logger.debug(
|
|
193
|
+
f"Received flag configuration - {abs(hash(flag_str)) % (10**8)}"
|
|
194
|
+
)
|
|
195
|
+
self.flag_store.update(json.loads(flag_str))
|
|
196
|
+
|
|
197
|
+
if not self.connected:
|
|
198
|
+
self.emit_provider_ready(
|
|
199
|
+
ProviderEventDetails(
|
|
200
|
+
message="gRPC sync connection established"
|
|
201
|
+
),
|
|
202
|
+
context_values["metadata"],
|
|
203
|
+
)
|
|
204
|
+
self.connected = True
|
|
205
|
+
|
|
206
|
+
if not self.active:
|
|
207
|
+
logger.debug("Terminating gRPC sync thread")
|
|
208
|
+
return
|
|
209
|
+
except grpc.RpcError as e: # noqa: PERF203
|
|
210
|
+
logger.error(f"SyncFlags stream error, {e.code()=} {e.details()=}")
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
logger.exception(
|
|
213
|
+
f"Could not parse JSON flag data from SyncFlags endpoint: {flag_str=}"
|
|
214
|
+
)
|
|
215
|
+
except ParseError:
|
|
216
|
+
logger.exception(
|
|
217
|
+
f"Could not parse flag data using flagd syntax: {flag_str=}"
|
|
218
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import typing
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
|
|
4
5
|
import mmh3
|
|
5
6
|
import semver
|
|
@@ -10,6 +11,12 @@ JsonLogicArg = typing.Union[JsonPrimitive, typing.Sequence[JsonPrimitive]]
|
|
|
10
11
|
logger = logging.getLogger("openfeature.contrib")
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
@dataclass
|
|
15
|
+
class Fraction:
|
|
16
|
+
variant: str
|
|
17
|
+
weight: int = 1
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
|
|
14
21
|
if not args:
|
|
15
22
|
logger.error("No arguments provided to fractional operator.")
|
|
@@ -32,28 +39,52 @@ def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
|
|
|
32
39
|
return None
|
|
33
40
|
|
|
34
41
|
hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
|
|
35
|
-
bucket =
|
|
36
|
-
|
|
37
|
-
for arg in args:
|
|
38
|
-
if (
|
|
39
|
-
not isinstance(arg, (tuple, list))
|
|
40
|
-
or len(arg) != 2
|
|
41
|
-
or not isinstance(arg[0], str)
|
|
42
|
-
or not isinstance(arg[1], int)
|
|
43
|
-
):
|
|
44
|
-
logger.error("Fractional variant weights must be (str, int) tuple")
|
|
45
|
-
return None
|
|
46
|
-
variant_weights: typing.Tuple[typing.Tuple[str, int]] = args # type: ignore[assignment]
|
|
42
|
+
bucket = hash_ratio * 100
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
total_weight = 0
|
|
45
|
+
fractions = []
|
|
46
|
+
try:
|
|
47
|
+
for arg in args:
|
|
48
|
+
fraction = _parse_fraction(arg)
|
|
49
|
+
if fraction:
|
|
50
|
+
fractions.append(fraction)
|
|
51
|
+
total_weight += fraction.weight
|
|
52
|
+
|
|
53
|
+
except ValueError:
|
|
54
|
+
logger.debug(f"Invalid {args} configuration")
|
|
55
|
+
return None
|
|
53
56
|
|
|
57
|
+
range_end: float = 0
|
|
58
|
+
for fraction in fractions:
|
|
59
|
+
range_end += fraction.weight * 100 / total_weight
|
|
60
|
+
if bucket < range_end:
|
|
61
|
+
return fraction.variant
|
|
54
62
|
return None
|
|
55
63
|
|
|
56
64
|
|
|
65
|
+
def _parse_fraction(arg: JsonLogicArg) -> Fraction:
|
|
66
|
+
if not isinstance(arg, (tuple, list)) or not arg or len(arg) > 2:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"Fractional variant weights must be (str, int) tuple or [str] list"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if not isinstance(arg[0], str):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"Fractional variant identifier (first element) isn't of type 'str'"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if len(arg) >= 2 and not isinstance(arg[1], int):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"Fractional variant weight value (second element) isn't of type 'int'"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
fraction = Fraction(variant=arg[0])
|
|
82
|
+
if len(arg) >= 2:
|
|
83
|
+
fraction.weight = arg[1]
|
|
84
|
+
|
|
85
|
+
return fraction
|
|
86
|
+
|
|
87
|
+
|
|
57
88
|
def starts_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
|
|
58
89
|
def f(s1: str, s2: str) -> bool:
|
|
59
90
|
return s1.startswith(s2)
|
|
@@ -99,8 +130,8 @@ def sem_ver(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]: # noqa:
|
|
|
99
130
|
arg1, op, arg2 = args
|
|
100
131
|
|
|
101
132
|
try:
|
|
102
|
-
v1 =
|
|
103
|
-
v2 =
|
|
133
|
+
v1 = parse_version(arg1)
|
|
134
|
+
v2 = parse_version(arg2)
|
|
104
135
|
except ValueError as e:
|
|
105
136
|
logger.exception(e)
|
|
106
137
|
return None
|
|
@@ -124,3 +155,11 @@ def sem_ver(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]: # noqa:
|
|
|
124
155
|
else:
|
|
125
156
|
logger.error(f"Op not supported by sem_ver: {op}")
|
|
126
157
|
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def parse_version(arg: typing.Any) -> semver.Version:
|
|
161
|
+
version = str(arg)
|
|
162
|
+
if version.startswith(("v", "V")):
|
|
163
|
+
version = version[1:]
|
|
164
|
+
|
|
165
|
+
return semver.Version.parse(version)
|
|
@@ -1,9 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
1
3
|
import typing
|
|
2
4
|
from dataclasses import dataclass
|
|
3
5
|
|
|
6
|
+
from openfeature.event import ProviderEventDetails
|
|
4
7
|
from openfeature.exception import ParseError
|
|
5
8
|
|
|
6
9
|
|
|
10
|
+
class FlagStore:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
emit_provider_configuration_changed: typing.Callable[
|
|
14
|
+
[ProviderEventDetails], None
|
|
15
|
+
],
|
|
16
|
+
):
|
|
17
|
+
self.emit_provider_configuration_changed = emit_provider_configuration_changed
|
|
18
|
+
self.flags: typing.Mapping[str, Flag] = {}
|
|
19
|
+
|
|
20
|
+
def get_flag(self, key: str) -> typing.Optional["Flag"]:
|
|
21
|
+
return self.flags.get(key)
|
|
22
|
+
|
|
23
|
+
def update(self, flags_data: dict) -> None:
|
|
24
|
+
flags = flags_data.get("flags", {})
|
|
25
|
+
evaluators: typing.Optional[dict] = flags_data.get("$evaluators")
|
|
26
|
+
if evaluators:
|
|
27
|
+
transposed = json.dumps(flags)
|
|
28
|
+
for name, rule in evaluators.items():
|
|
29
|
+
transposed = re.sub(
|
|
30
|
+
rf"{{\s*\"\$ref\":\s*\"{name}\"\s*}}", json.dumps(rule), transposed
|
|
31
|
+
)
|
|
32
|
+
flags = json.loads(transposed)
|
|
33
|
+
|
|
34
|
+
if not isinstance(flags, dict):
|
|
35
|
+
raise ParseError("`flags` key of configuration must be a dictionary")
|
|
36
|
+
self.flags = {key: Flag.from_dict(key, data) for key, data in flags.items()}
|
|
37
|
+
|
|
38
|
+
self.emit_provider_configuration_changed(
|
|
39
|
+
ProviderEventDetails(flags_changed=list(self.flags.keys()))
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
7
43
|
@dataclass
|
|
8
44
|
class Flag:
|
|
9
45
|
key: str
|
|
@@ -32,19 +68,27 @@ class Flag:
|
|
|
32
68
|
|
|
33
69
|
@classmethod
|
|
34
70
|
def from_dict(cls, key: str, data: dict) -> "Flag":
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
71
|
+
if "defaultVariant" in data:
|
|
72
|
+
data["default_variant"] = data["defaultVariant"]
|
|
73
|
+
del data["defaultVariant"]
|
|
38
74
|
|
|
39
|
-
|
|
75
|
+
if "source" in data:
|
|
76
|
+
del data["source"]
|
|
77
|
+
if "selector" in data:
|
|
78
|
+
del data["selector"]
|
|
79
|
+
try:
|
|
80
|
+
flag = cls(key=key, **data)
|
|
81
|
+
return flag
|
|
82
|
+
except Exception as err:
|
|
83
|
+
raise ParseError from err
|
|
40
84
|
|
|
41
85
|
@property
|
|
42
|
-
def default(self) ->
|
|
86
|
+
def default(self) -> tuple[str, typing.Any]:
|
|
43
87
|
return self.get_variant(self.default_variant)
|
|
44
88
|
|
|
45
89
|
def get_variant(
|
|
46
90
|
self, variant_key: typing.Union[str, bool]
|
|
47
|
-
) ->
|
|
91
|
+
) -> tuple[str, typing.Any]:
|
|
48
92
|
if isinstance(variant_key, bool):
|
|
49
93
|
variant_key = str(variant_key).lower()
|
|
50
94
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from json_logic import builtins, jsonLogic
|
|
5
|
+
from json_logic.types import JsonValue
|
|
6
|
+
|
|
7
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
8
|
+
|
|
9
|
+
from .custom_ops import (
|
|
10
|
+
ends_with,
|
|
11
|
+
fractional,
|
|
12
|
+
sem_ver,
|
|
13
|
+
starts_with,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
OPERATORS = {
|
|
17
|
+
**builtins.BUILTINS,
|
|
18
|
+
"fractional": fractional,
|
|
19
|
+
"starts_with": starts_with,
|
|
20
|
+
"ends_with": ends_with,
|
|
21
|
+
"sem_ver": sem_ver,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def targeting(
|
|
26
|
+
key: str,
|
|
27
|
+
targeting: dict,
|
|
28
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
29
|
+
) -> JsonValue:
|
|
30
|
+
json_logic_context = evaluation_context.attributes if evaluation_context else {}
|
|
31
|
+
json_logic_context["$flagd"] = {"flagKey": key, "timestamp": int(time.time())}
|
|
32
|
+
json_logic_context["targetingKey"] = (
|
|
33
|
+
evaluation_context.targeting_key if evaluation_context else None
|
|
34
|
+
)
|
|
35
|
+
return jsonLogic(targeting, json_logic_context, OPERATORS)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Protocol
|
|
4
|
+
|
|
5
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
6
|
+
from openfeature.flag_evaluation import FlagResolutionDetails
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AbstractResolver(Protocol):
|
|
10
|
+
def initialize(self, evaluation_context: EvaluationContext) -> None: ...
|
|
11
|
+
|
|
12
|
+
def shutdown(self) -> None: ...
|
|
13
|
+
|
|
14
|
+
def resolve_boolean_details(
|
|
15
|
+
self,
|
|
16
|
+
key: str,
|
|
17
|
+
default_value: bool,
|
|
18
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
19
|
+
) -> FlagResolutionDetails[bool]: ...
|
|
20
|
+
|
|
21
|
+
def resolve_string_details(
|
|
22
|
+
self,
|
|
23
|
+
key: str,
|
|
24
|
+
default_value: str,
|
|
25
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
26
|
+
) -> FlagResolutionDetails[str]: ...
|
|
27
|
+
|
|
28
|
+
def resolve_float_details(
|
|
29
|
+
self,
|
|
30
|
+
key: str,
|
|
31
|
+
default_value: float,
|
|
32
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
33
|
+
) -> FlagResolutionDetails[float]: ...
|
|
34
|
+
|
|
35
|
+
def resolve_integer_details(
|
|
36
|
+
self,
|
|
37
|
+
key: str,
|
|
38
|
+
default_value: int,
|
|
39
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
40
|
+
) -> FlagResolutionDetails[int]: ...
|
|
41
|
+
|
|
42
|
+
def resolve_object_details(
|
|
43
|
+
self,
|
|
44
|
+
key: str,
|
|
45
|
+
default_value: typing.Union[dict, list],
|
|
46
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
47
|
+
) -> FlagResolutionDetails[typing.Union[dict, list]]: ...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
4
|
+
from openfeature.hook import Hook, HookContext, HookHints
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SyncMetadataHook(Hook):
|
|
8
|
+
def __init__(self, context_supplier: typing.Callable[[], EvaluationContext]):
|
|
9
|
+
self.context_supplier = context_supplier
|
|
10
|
+
|
|
11
|
+
def before(
|
|
12
|
+
self, hook_context: HookContext, hints: HookHints
|
|
13
|
+
) -> typing.Optional[EvaluationContext]:
|
|
14
|
+
return self.context_supplier()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation.proto
|
|
5
|
+
# Protobuf Python Version: 5.29.0
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
5,
|
|
15
|
+
29,
|
|
16
|
+
0,
|
|
17
|
+
'',
|
|
18
|
+
'openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\nAopenfeature/schemas/protobuf/flagd/evaluation/v1/evaluation.proto\x12\x13\x66lagd.evaluation.v1\x1a\x1cgoogle/protobuf/struct.proto\"=\n\x11ResolveAllRequest\x12(\n\x07\x63ontext\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xa3\x01\n\x12ResolveAllResponse\x12\x41\n\x05\x66lags\x18\x01 \x03(\x0b\x32\x32.flagd.evaluation.v1.ResolveAllResponse.FlagsEntry\x1aJ\n\nFlagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.flagd.evaluation.v1.AnyFlag:\x02\x38\x01\"\xaa\x01\n\x07\x41nyFlag\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x0f\n\x07variant\x18\x02 \x01(\t\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x16\n\x0cstring_value\x18\x04 \x01(\tH\x00\x12\x16\n\x0c\x64ouble_value\x18\x05 \x01(\x01H\x00\x12/\n\x0cobject_value\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x42\x07\n\x05value\"S\n\x15ResolveBooleanRequest\x12\x10\n\x08\x66lag_key\x18\x01 \x01(\t\x12(\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"s\n\x16ResolveBooleanResponse\x12\r\n\x05value\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0f\n\x07variant\x18\x03 \x01(\t\x12)\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"R\n\x14ResolveStringRequest\x12\x10\n\x08\x66lag_key\x18\x01 \x01(\t\x12(\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"r\n\x15ResolveStringResponse\x12\r\n\x05value\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0f\n\x07variant\x18\x03 \x01(\t\x12)\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"Q\n\x13ResolveFloatRequest\x12\x10\n\x08\x66lag_key\x18\x01 \x01(\t\x12(\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"q\n\x14ResolveFloatResponse\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0f\n\x07variant\x18\x03 \x01(\t\x12)\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"O\n\x11ResolveIntRequest\x12\x10\n\x08\x66lag_key\x18\x01 \x01(\t\x12(\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"o\n\x12ResolveIntResponse\x12\r\n\x05value\x18\x01 \x01(\x03\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0f\n\x07variant\x18\x03 \x01(\t\x12)\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"R\n\x14ResolveObjectRequest\x12\x10\n\x08\x66lag_key\x18\x01 \x01(\t\x12(\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x8b\x01\n\x15ResolveObjectResponse\x12&\n\x05value\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0f\n\x07variant\x18\x03 \x01(\t\x12)\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"J\n\x13\x45ventStreamResponse\x12\x0c\n\x04type\x18\x01 \x01(\t\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x14\n\x12\x45ventStreamRequest2\xd9\x05\n\x07Service\x12_\n\nResolveAll\x12&.flagd.evaluation.v1.ResolveAllRequest\x1a\'.flagd.evaluation.v1.ResolveAllResponse\"\x00\x12k\n\x0eResolveBoolean\x12*.flagd.evaluation.v1.ResolveBooleanRequest\x1a+.flagd.evaluation.v1.ResolveBooleanResponse\"\x00\x12h\n\rResolveString\x12).flagd.evaluation.v1.ResolveStringRequest\x1a*.flagd.evaluation.v1.ResolveStringResponse\"\x00\x12\x65\n\x0cResolveFloat\x12(.flagd.evaluation.v1.ResolveFloatRequest\x1a).flagd.evaluation.v1.ResolveFloatResponse\"\x00\x12_\n\nResolveInt\x12&.flagd.evaluation.v1.ResolveIntRequest\x1a\'.flagd.evaluation.v1.ResolveIntResponse\"\x00\x12h\n\rResolveObject\x12).flagd.evaluation.v1.ResolveObjectRequest\x1a*.flagd.evaluation.v1.ResolveObjectResponse\"\x00\x12\x64\n\x0b\x45ventStream\x12\'.flagd.evaluation.v1.EventStreamRequest\x1a(.flagd.evaluation.v1.EventStreamResponse\"\x00\x30\x01\x42\xc6\x01\n%dev.openfeature.flagd.grpc.evaluationZ\x13\x66lagd/evaluation/v1\xaa\x02!OpenFeature.Flagd.Grpc.Evaluation\xca\x02\x32OpenFeature\\Providers\\Flagd\\Schema\\Grpc\\Evaluation\xea\x02.OpenFeature::Flagd::Provider::Grpc::Evaluationb\x06proto3')
|
|
29
|
+
|
|
30
|
+
_globals = globals()
|
|
31
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
32
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'openfeature.schemas.protobuf.flagd.evaluation.v1.evaluation_pb2', _globals)
|
|
33
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
34
|
+
_globals['DESCRIPTOR']._loaded_options = None
|
|
35
|
+
_globals['DESCRIPTOR']._serialized_options = b'\n%dev.openfeature.flagd.grpc.evaluationZ\023flagd/evaluation/v1\252\002!OpenFeature.Flagd.Grpc.Evaluation\312\0022OpenFeature\\Providers\\Flagd\\Schema\\Grpc\\Evaluation\352\002.OpenFeature::Flagd::Provider::Grpc::Evaluation'
|
|
36
|
+
_globals['_RESOLVEALLRESPONSE_FLAGSENTRY']._loaded_options = None
|
|
37
|
+
_globals['_RESOLVEALLRESPONSE_FLAGSENTRY']._serialized_options = b'8\001'
|
|
38
|
+
_globals['_RESOLVEALLREQUEST']._serialized_start=120
|
|
39
|
+
_globals['_RESOLVEALLREQUEST']._serialized_end=181
|
|
40
|
+
_globals['_RESOLVEALLRESPONSE']._serialized_start=184
|
|
41
|
+
_globals['_RESOLVEALLRESPONSE']._serialized_end=347
|
|
42
|
+
_globals['_RESOLVEALLRESPONSE_FLAGSENTRY']._serialized_start=273
|
|
43
|
+
_globals['_RESOLVEALLRESPONSE_FLAGSENTRY']._serialized_end=347
|
|
44
|
+
_globals['_ANYFLAG']._serialized_start=350
|
|
45
|
+
_globals['_ANYFLAG']._serialized_end=520
|
|
46
|
+
_globals['_RESOLVEBOOLEANREQUEST']._serialized_start=522
|
|
47
|
+
_globals['_RESOLVEBOOLEANREQUEST']._serialized_end=605
|
|
48
|
+
_globals['_RESOLVEBOOLEANRESPONSE']._serialized_start=607
|
|
49
|
+
_globals['_RESOLVEBOOLEANRESPONSE']._serialized_end=722
|
|
50
|
+
_globals['_RESOLVESTRINGREQUEST']._serialized_start=724
|
|
51
|
+
_globals['_RESOLVESTRINGREQUEST']._serialized_end=806
|
|
52
|
+
_globals['_RESOLVESTRINGRESPONSE']._serialized_start=808
|
|
53
|
+
_globals['_RESOLVESTRINGRESPONSE']._serialized_end=922
|
|
54
|
+
_globals['_RESOLVEFLOATREQUEST']._serialized_start=924
|
|
55
|
+
_globals['_RESOLVEFLOATREQUEST']._serialized_end=1005
|
|
56
|
+
_globals['_RESOLVEFLOATRESPONSE']._serialized_start=1007
|
|
57
|
+
_globals['_RESOLVEFLOATRESPONSE']._serialized_end=1120
|
|
58
|
+
_globals['_RESOLVEINTREQUEST']._serialized_start=1122
|
|
59
|
+
_globals['_RESOLVEINTREQUEST']._serialized_end=1201
|
|
60
|
+
_globals['_RESOLVEINTRESPONSE']._serialized_start=1203
|
|
61
|
+
_globals['_RESOLVEINTRESPONSE']._serialized_end=1314
|
|
62
|
+
_globals['_RESOLVEOBJECTREQUEST']._serialized_start=1316
|
|
63
|
+
_globals['_RESOLVEOBJECTREQUEST']._serialized_end=1398
|
|
64
|
+
_globals['_RESOLVEOBJECTRESPONSE']._serialized_start=1401
|
|
65
|
+
_globals['_RESOLVEOBJECTRESPONSE']._serialized_end=1540
|
|
66
|
+
_globals['_EVENTSTREAMRESPONSE']._serialized_start=1542
|
|
67
|
+
_globals['_EVENTSTREAMRESPONSE']._serialized_end=1616
|
|
68
|
+
_globals['_EVENTSTREAMREQUEST']._serialized_start=1618
|
|
69
|
+
_globals['_EVENTSTREAMREQUEST']._serialized_end=1638
|
|
70
|
+
_globals['_SERVICE']._serialized_start=1641
|
|
71
|
+
_globals['_SERVICE']._serialized_end=2370
|
|
72
|
+
# @@protoc_insertion_point(module_scope)
|