openfeature-provider-flagd 0.1.4__py3-none-any.whl → 0.1.5__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/contrib/provider/flagd/config.py +25 -1
- openfeature/contrib/provider/flagd/provider.py +36 -94
- openfeature/contrib/provider/flagd/resolvers/__init__.py +51 -0
- openfeature/contrib/provider/flagd/resolvers/grpc.py +145 -0
- openfeature/contrib/provider/flagd/resolvers/in_process.py +122 -0
- openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +126 -0
- openfeature/contrib/provider/flagd/resolvers/process/file_watcher.py +89 -0
- openfeature/contrib/provider/flagd/resolvers/process/flags.py +51 -0
- {openfeature_provider_flagd-0.1.4.dist-info → openfeature_provider_flagd-0.1.5.dist-info}/METADATA +18 -1
- {openfeature_provider_flagd-0.1.4.dist-info → openfeature_provider_flagd-0.1.5.dist-info}/RECORD +12 -6
- {openfeature_provider_flagd-0.1.4.dist-info → openfeature_provider_flagd-0.1.5.dist-info}/WHEEL +1 -1
- {openfeature_provider_flagd-0.1.4.dist-info → openfeature_provider_flagd-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import typing
|
|
3
|
+
from enum import Enum
|
|
3
4
|
|
|
4
5
|
T = typing.TypeVar("T")
|
|
5
6
|
|
|
@@ -17,13 +18,21 @@ def env_or_default(
|
|
|
17
18
|
return val if cast is None else cast(val)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
class ResolverType(Enum):
|
|
22
|
+
GRPC = "grpc"
|
|
23
|
+
IN_PROCESS = "in-process"
|
|
24
|
+
|
|
25
|
+
|
|
20
26
|
class Config:
|
|
21
|
-
def __init__(
|
|
27
|
+
def __init__( # noqa: PLR0913
|
|
22
28
|
self,
|
|
23
29
|
host: typing.Optional[str] = None,
|
|
24
30
|
port: typing.Optional[int] = None,
|
|
25
31
|
tls: typing.Optional[bool] = None,
|
|
26
32
|
timeout: typing.Optional[int] = None,
|
|
33
|
+
resolver_type: typing.Optional[ResolverType] = None,
|
|
34
|
+
offline_flag_source_path: typing.Optional[str] = None,
|
|
35
|
+
offline_poll_interval_seconds: typing.Optional[float] = None,
|
|
27
36
|
):
|
|
28
37
|
self.host = env_or_default("FLAGD_HOST", "localhost") if host is None else host
|
|
29
38
|
self.port = (
|
|
@@ -33,3 +42,18 @@ class Config:
|
|
|
33
42
|
env_or_default("FLAGD_TLS", False, cast=str_to_bool) if tls is None else tls
|
|
34
43
|
)
|
|
35
44
|
self.timeout = 5 if timeout is None else timeout
|
|
45
|
+
self.resolver_type = (
|
|
46
|
+
ResolverType(env_or_default("FLAGD_RESOLVER_TYPE", "grpc"))
|
|
47
|
+
if resolver_type is None
|
|
48
|
+
else resolver_type
|
|
49
|
+
)
|
|
50
|
+
self.offline_flag_source_path = (
|
|
51
|
+
env_or_default("FLAGD_OFFLINE_FLAG_SOURCE_PATH", None)
|
|
52
|
+
if offline_flag_source_path is None
|
|
53
|
+
else offline_flag_source_path
|
|
54
|
+
)
|
|
55
|
+
self.offline_poll_interval_seconds = (
|
|
56
|
+
float(env_or_default("FLAGD_OFFLINE_POLL_INTERVAL_SECONDS", 1.0))
|
|
57
|
+
if offline_poll_interval_seconds is None
|
|
58
|
+
else offline_poll_interval_seconds
|
|
59
|
+
)
|
|
@@ -23,24 +23,13 @@
|
|
|
23
23
|
|
|
24
24
|
import typing
|
|
25
25
|
|
|
26
|
-
import grpc
|
|
27
|
-
from google.protobuf.struct_pb2 import Struct
|
|
28
|
-
|
|
29
26
|
from openfeature.evaluation_context import EvaluationContext
|
|
30
|
-
from openfeature.exception import (
|
|
31
|
-
FlagNotFoundError,
|
|
32
|
-
GeneralError,
|
|
33
|
-
InvalidContextError,
|
|
34
|
-
ParseError,
|
|
35
|
-
TypeMismatchError,
|
|
36
|
-
)
|
|
37
27
|
from openfeature.flag_evaluation import FlagResolutionDetails
|
|
38
28
|
from openfeature.provider.metadata import Metadata
|
|
39
29
|
from openfeature.provider.provider import AbstractProvider
|
|
40
30
|
|
|
41
|
-
from .config import Config
|
|
42
|
-
from .
|
|
43
|
-
from .proto.schema.v1 import schema_pb2, schema_pb2_grpc
|
|
31
|
+
from .config import Config, ResolverType
|
|
32
|
+
from .resolvers import AbstractResolver, GrpcResolver, InProcessResolver
|
|
44
33
|
|
|
45
34
|
T = typing.TypeVar("T")
|
|
46
35
|
|
|
@@ -48,12 +37,15 @@ T = typing.TypeVar("T")
|
|
|
48
37
|
class FlagdProvider(AbstractProvider):
|
|
49
38
|
"""Flagd OpenFeature Provider"""
|
|
50
39
|
|
|
51
|
-
def __init__(
|
|
40
|
+
def __init__( # noqa: PLR0913
|
|
52
41
|
self,
|
|
53
42
|
host: typing.Optional[str] = None,
|
|
54
43
|
port: typing.Optional[int] = None,
|
|
55
44
|
tls: typing.Optional[bool] = None,
|
|
56
45
|
timeout: typing.Optional[int] = None,
|
|
46
|
+
resolver_type: typing.Optional[ResolverType] = None,
|
|
47
|
+
offline_flag_source_path: typing.Optional[str] = None,
|
|
48
|
+
offline_poll_interval_seconds: typing.Optional[float] = None,
|
|
57
49
|
):
|
|
58
50
|
"""
|
|
59
51
|
Create an instance of the FlagdProvider
|
|
@@ -68,14 +60,26 @@ class FlagdProvider(AbstractProvider):
|
|
|
68
60
|
port=port,
|
|
69
61
|
tls=tls,
|
|
70
62
|
timeout=timeout,
|
|
63
|
+
resolver_type=resolver_type,
|
|
64
|
+
offline_flag_source_path=offline_flag_source_path,
|
|
65
|
+
offline_poll_interval_seconds=offline_poll_interval_seconds,
|
|
71
66
|
)
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
self.resolver = self.setup_resolver()
|
|
69
|
+
|
|
70
|
+
def setup_resolver(self) -> AbstractResolver:
|
|
71
|
+
if self.config.resolver_type == ResolverType.GRPC:
|
|
72
|
+
return GrpcResolver(self.config)
|
|
73
|
+
elif self.config.resolver_type == ResolverType.IN_PROCESS:
|
|
74
|
+
return InProcessResolver(self.config, self)
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"`resolver_type` parameter invalid: {self.config.resolver_type}"
|
|
78
|
+
)
|
|
76
79
|
|
|
77
80
|
def shutdown(self) -> None:
|
|
78
|
-
self.
|
|
81
|
+
if self.resolver:
|
|
82
|
+
self.resolver.shutdown()
|
|
79
83
|
|
|
80
84
|
def get_metadata(self) -> Metadata:
|
|
81
85
|
"""Returns provider metadata"""
|
|
@@ -87,7 +91,9 @@ class FlagdProvider(AbstractProvider):
|
|
|
87
91
|
default_value: bool,
|
|
88
92
|
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
89
93
|
) -> FlagResolutionDetails[bool]:
|
|
90
|
-
return self.
|
|
94
|
+
return self.resolver.resolve_boolean_details(
|
|
95
|
+
key, default_value, evaluation_context
|
|
96
|
+
)
|
|
91
97
|
|
|
92
98
|
def resolve_string_details(
|
|
93
99
|
self,
|
|
@@ -95,7 +101,9 @@ class FlagdProvider(AbstractProvider):
|
|
|
95
101
|
default_value: str,
|
|
96
102
|
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
97
103
|
) -> FlagResolutionDetails[str]:
|
|
98
|
-
return self.
|
|
104
|
+
return self.resolver.resolve_string_details(
|
|
105
|
+
key, default_value, evaluation_context
|
|
106
|
+
)
|
|
99
107
|
|
|
100
108
|
def resolve_float_details(
|
|
101
109
|
self,
|
|
@@ -103,7 +111,9 @@ class FlagdProvider(AbstractProvider):
|
|
|
103
111
|
default_value: float,
|
|
104
112
|
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
105
113
|
) -> FlagResolutionDetails[float]:
|
|
106
|
-
return self.
|
|
114
|
+
return self.resolver.resolve_float_details(
|
|
115
|
+
key, default_value, evaluation_context
|
|
116
|
+
)
|
|
107
117
|
|
|
108
118
|
def resolve_integer_details(
|
|
109
119
|
self,
|
|
@@ -111,7 +121,9 @@ class FlagdProvider(AbstractProvider):
|
|
|
111
121
|
default_value: int,
|
|
112
122
|
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
113
123
|
) -> FlagResolutionDetails[int]:
|
|
114
|
-
return self.
|
|
124
|
+
return self.resolver.resolve_integer_details(
|
|
125
|
+
key, default_value, evaluation_context
|
|
126
|
+
)
|
|
115
127
|
|
|
116
128
|
def resolve_object_details(
|
|
117
129
|
self,
|
|
@@ -119,76 +131,6 @@ class FlagdProvider(AbstractProvider):
|
|
|
119
131
|
default_value: typing.Union[dict, list],
|
|
120
132
|
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
121
133
|
) -> FlagResolutionDetails[typing.Union[dict, list]]:
|
|
122
|
-
return self.
|
|
123
|
-
|
|
124
|
-
def _resolve(
|
|
125
|
-
self,
|
|
126
|
-
flag_key: str,
|
|
127
|
-
flag_type: FlagType,
|
|
128
|
-
default_value: T,
|
|
129
|
-
evaluation_context: typing.Optional[EvaluationContext],
|
|
130
|
-
) -> FlagResolutionDetails[T]:
|
|
131
|
-
context = self._convert_context(evaluation_context)
|
|
132
|
-
call_args = {"timeout": self.config.timeout}
|
|
133
|
-
try:
|
|
134
|
-
if flag_type == FlagType.BOOLEAN:
|
|
135
|
-
request = schema_pb2.ResolveBooleanRequest( # type:ignore[attr-defined]
|
|
136
|
-
flag_key=flag_key, context=context
|
|
137
|
-
)
|
|
138
|
-
response = self.stub.ResolveBoolean(request, **call_args)
|
|
139
|
-
elif flag_type == FlagType.STRING:
|
|
140
|
-
request = schema_pb2.ResolveStringRequest( # type:ignore[attr-defined]
|
|
141
|
-
flag_key=flag_key, context=context
|
|
142
|
-
)
|
|
143
|
-
response = self.stub.ResolveString(request, **call_args)
|
|
144
|
-
elif flag_type == FlagType.OBJECT:
|
|
145
|
-
request = schema_pb2.ResolveObjectRequest( # type:ignore[attr-defined]
|
|
146
|
-
flag_key=flag_key, context=context
|
|
147
|
-
)
|
|
148
|
-
response = self.stub.ResolveObject(request, **call_args)
|
|
149
|
-
elif flag_type == FlagType.FLOAT:
|
|
150
|
-
request = schema_pb2.ResolveFloatRequest( # type:ignore[attr-defined]
|
|
151
|
-
flag_key=flag_key, context=context
|
|
152
|
-
)
|
|
153
|
-
response = self.stub.ResolveFloat(request, **call_args)
|
|
154
|
-
elif flag_type == FlagType.INTEGER:
|
|
155
|
-
request = schema_pb2.ResolveIntRequest( # type:ignore[attr-defined]
|
|
156
|
-
flag_key=flag_key, context=context
|
|
157
|
-
)
|
|
158
|
-
response = self.stub.ResolveInt(request, **call_args)
|
|
159
|
-
else:
|
|
160
|
-
raise ValueError(f"Unknown flag type: {flag_type}")
|
|
161
|
-
|
|
162
|
-
except grpc.RpcError as e:
|
|
163
|
-
code = e.code()
|
|
164
|
-
message = f"received grpc status code {code}"
|
|
165
|
-
|
|
166
|
-
if code == grpc.StatusCode.NOT_FOUND:
|
|
167
|
-
raise FlagNotFoundError(message) from e
|
|
168
|
-
elif code == grpc.StatusCode.INVALID_ARGUMENT:
|
|
169
|
-
raise TypeMismatchError(message) from e
|
|
170
|
-
elif code == grpc.StatusCode.DATA_LOSS:
|
|
171
|
-
raise ParseError(message) from e
|
|
172
|
-
raise GeneralError(message) from e
|
|
173
|
-
|
|
174
|
-
# Got a valid flag and valid type. Return it.
|
|
175
|
-
return FlagResolutionDetails(
|
|
176
|
-
value=response.value,
|
|
177
|
-
reason=response.reason,
|
|
178
|
-
variant=response.variant,
|
|
134
|
+
return self.resolver.resolve_object_details(
|
|
135
|
+
key, default_value, evaluation_context
|
|
179
136
|
)
|
|
180
|
-
|
|
181
|
-
def _convert_context(
|
|
182
|
-
self, evaluation_context: typing.Optional[EvaluationContext]
|
|
183
|
-
) -> Struct:
|
|
184
|
-
s = Struct()
|
|
185
|
-
if evaluation_context:
|
|
186
|
-
try:
|
|
187
|
-
s["targetingKey"] = evaluation_context.targeting_key
|
|
188
|
-
s.update(evaluation_context.attributes)
|
|
189
|
-
except ValueError as exc:
|
|
190
|
-
message = (
|
|
191
|
-
"could not serialize evaluation context to google.protobuf.Struct"
|
|
192
|
-
)
|
|
193
|
-
raise InvalidContextError(message) from exc
|
|
194
|
-
return s
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
from .grpc import GrpcResolver
|
|
9
|
+
from .in_process import InProcessResolver
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AbstractResolver(Protocol):
|
|
13
|
+
def shutdown(self) -> None: ...
|
|
14
|
+
|
|
15
|
+
def resolve_boolean_details(
|
|
16
|
+
self,
|
|
17
|
+
key: str,
|
|
18
|
+
default_value: bool,
|
|
19
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
20
|
+
) -> FlagResolutionDetails[bool]: ...
|
|
21
|
+
|
|
22
|
+
def resolve_string_details(
|
|
23
|
+
self,
|
|
24
|
+
key: str,
|
|
25
|
+
default_value: str,
|
|
26
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
27
|
+
) -> FlagResolutionDetails[str]: ...
|
|
28
|
+
|
|
29
|
+
def resolve_float_details(
|
|
30
|
+
self,
|
|
31
|
+
key: str,
|
|
32
|
+
default_value: float,
|
|
33
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
34
|
+
) -> FlagResolutionDetails[float]: ...
|
|
35
|
+
|
|
36
|
+
def resolve_integer_details(
|
|
37
|
+
self,
|
|
38
|
+
key: str,
|
|
39
|
+
default_value: int,
|
|
40
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
41
|
+
) -> FlagResolutionDetails[int]: ...
|
|
42
|
+
|
|
43
|
+
def resolve_object_details(
|
|
44
|
+
self,
|
|
45
|
+
key: str,
|
|
46
|
+
default_value: typing.Union[dict, list],
|
|
47
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
48
|
+
) -> FlagResolutionDetails[typing.Union[dict, list]]: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["AbstractResolver", "GrpcResolver", "InProcessResolver"]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import grpc
|
|
4
|
+
from google.protobuf.struct_pb2 import Struct
|
|
5
|
+
|
|
6
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
7
|
+
from openfeature.exception import (
|
|
8
|
+
FlagNotFoundError,
|
|
9
|
+
GeneralError,
|
|
10
|
+
InvalidContextError,
|
|
11
|
+
ParseError,
|
|
12
|
+
TypeMismatchError,
|
|
13
|
+
)
|
|
14
|
+
from openfeature.flag_evaluation import FlagResolutionDetails
|
|
15
|
+
|
|
16
|
+
from ..config import Config
|
|
17
|
+
from ..flag_type import FlagType
|
|
18
|
+
from ..proto.schema.v1 import schema_pb2, schema_pb2_grpc
|
|
19
|
+
|
|
20
|
+
T = typing.TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GrpcResolver:
|
|
24
|
+
def __init__(self, config: Config):
|
|
25
|
+
self.config = config
|
|
26
|
+
channel_factory = (
|
|
27
|
+
grpc.secure_channel if self.config.tls else grpc.insecure_channel
|
|
28
|
+
)
|
|
29
|
+
self.channel = channel_factory(f"{self.config.host}:{self.config.port}")
|
|
30
|
+
self.stub = schema_pb2_grpc.ServiceStub(self.channel)
|
|
31
|
+
|
|
32
|
+
def shutdown(self) -> None:
|
|
33
|
+
self.channel.close()
|
|
34
|
+
|
|
35
|
+
def resolve_boolean_details(
|
|
36
|
+
self,
|
|
37
|
+
key: str,
|
|
38
|
+
default_value: bool,
|
|
39
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
40
|
+
) -> FlagResolutionDetails[bool]:
|
|
41
|
+
return self._resolve(key, FlagType.BOOLEAN, default_value, evaluation_context)
|
|
42
|
+
|
|
43
|
+
def resolve_string_details(
|
|
44
|
+
self,
|
|
45
|
+
key: str,
|
|
46
|
+
default_value: str,
|
|
47
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
48
|
+
) -> FlagResolutionDetails[str]:
|
|
49
|
+
return self._resolve(key, FlagType.STRING, default_value, evaluation_context)
|
|
50
|
+
|
|
51
|
+
def resolve_float_details(
|
|
52
|
+
self,
|
|
53
|
+
key: str,
|
|
54
|
+
default_value: float,
|
|
55
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
56
|
+
) -> FlagResolutionDetails[float]:
|
|
57
|
+
return self._resolve(key, FlagType.FLOAT, default_value, evaluation_context)
|
|
58
|
+
|
|
59
|
+
def resolve_integer_details(
|
|
60
|
+
self,
|
|
61
|
+
key: str,
|
|
62
|
+
default_value: int,
|
|
63
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
64
|
+
) -> FlagResolutionDetails[int]:
|
|
65
|
+
return self._resolve(key, FlagType.INTEGER, default_value, evaluation_context)
|
|
66
|
+
|
|
67
|
+
def resolve_object_details(
|
|
68
|
+
self,
|
|
69
|
+
key: str,
|
|
70
|
+
default_value: typing.Union[dict, list],
|
|
71
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
72
|
+
) -> FlagResolutionDetails[typing.Union[dict, list]]:
|
|
73
|
+
return self._resolve(key, FlagType.OBJECT, default_value, evaluation_context)
|
|
74
|
+
|
|
75
|
+
def _resolve(
|
|
76
|
+
self,
|
|
77
|
+
flag_key: str,
|
|
78
|
+
flag_type: FlagType,
|
|
79
|
+
default_value: T,
|
|
80
|
+
evaluation_context: typing.Optional[EvaluationContext],
|
|
81
|
+
) -> FlagResolutionDetails[T]:
|
|
82
|
+
context = self._convert_context(evaluation_context)
|
|
83
|
+
call_args = {"timeout": self.config.timeout}
|
|
84
|
+
try:
|
|
85
|
+
if flag_type == FlagType.BOOLEAN:
|
|
86
|
+
request = schema_pb2.ResolveBooleanRequest( # type:ignore[attr-defined]
|
|
87
|
+
flag_key=flag_key, context=context
|
|
88
|
+
)
|
|
89
|
+
response = self.stub.ResolveBoolean(request, **call_args)
|
|
90
|
+
elif flag_type == FlagType.STRING:
|
|
91
|
+
request = schema_pb2.ResolveStringRequest( # type:ignore[attr-defined]
|
|
92
|
+
flag_key=flag_key, context=context
|
|
93
|
+
)
|
|
94
|
+
response = self.stub.ResolveString(request, **call_args)
|
|
95
|
+
elif flag_type == FlagType.OBJECT:
|
|
96
|
+
request = schema_pb2.ResolveObjectRequest( # type:ignore[attr-defined]
|
|
97
|
+
flag_key=flag_key, context=context
|
|
98
|
+
)
|
|
99
|
+
response = self.stub.ResolveObject(request, **call_args)
|
|
100
|
+
elif flag_type == FlagType.FLOAT:
|
|
101
|
+
request = schema_pb2.ResolveFloatRequest( # type:ignore[attr-defined]
|
|
102
|
+
flag_key=flag_key, context=context
|
|
103
|
+
)
|
|
104
|
+
response = self.stub.ResolveFloat(request, **call_args)
|
|
105
|
+
elif flag_type == FlagType.INTEGER:
|
|
106
|
+
request = schema_pb2.ResolveIntRequest( # type:ignore[attr-defined]
|
|
107
|
+
flag_key=flag_key, context=context
|
|
108
|
+
)
|
|
109
|
+
response = self.stub.ResolveInt(request, **call_args)
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError(f"Unknown flag type: {flag_type}")
|
|
112
|
+
|
|
113
|
+
except grpc.RpcError as e:
|
|
114
|
+
code = e.code()
|
|
115
|
+
message = f"received grpc status code {code}"
|
|
116
|
+
|
|
117
|
+
if code == grpc.StatusCode.NOT_FOUND:
|
|
118
|
+
raise FlagNotFoundError(message) from e
|
|
119
|
+
elif code == grpc.StatusCode.INVALID_ARGUMENT:
|
|
120
|
+
raise TypeMismatchError(message) from e
|
|
121
|
+
elif code == grpc.StatusCode.DATA_LOSS:
|
|
122
|
+
raise ParseError(message) from e
|
|
123
|
+
raise GeneralError(message) from e
|
|
124
|
+
|
|
125
|
+
# Got a valid flag and valid type. Return it.
|
|
126
|
+
return FlagResolutionDetails(
|
|
127
|
+
value=response.value,
|
|
128
|
+
reason=response.reason,
|
|
129
|
+
variant=response.variant,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _convert_context(
|
|
133
|
+
self, evaluation_context: typing.Optional[EvaluationContext]
|
|
134
|
+
) -> Struct:
|
|
135
|
+
s = Struct()
|
|
136
|
+
if evaluation_context:
|
|
137
|
+
try:
|
|
138
|
+
s["targetingKey"] = evaluation_context.targeting_key
|
|
139
|
+
s.update(evaluation_context.attributes)
|
|
140
|
+
except ValueError as exc:
|
|
141
|
+
message = (
|
|
142
|
+
"could not serialize evaluation context to google.protobuf.Struct"
|
|
143
|
+
)
|
|
144
|
+
raise InvalidContextError(message) from exc
|
|
145
|
+
return s
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from json_logic import builtins, jsonLogic # type: ignore[import-untyped]
|
|
5
|
+
|
|
6
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
7
|
+
from openfeature.exception import FlagNotFoundError, ParseError
|
|
8
|
+
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
|
|
9
|
+
from openfeature.provider.provider import AbstractProvider
|
|
10
|
+
|
|
11
|
+
from ..config import Config
|
|
12
|
+
from .process.custom_ops import ends_with, fractional, sem_ver, starts_with
|
|
13
|
+
from .process.file_watcher import FileWatcherFlagStore
|
|
14
|
+
|
|
15
|
+
T = typing.TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InProcessResolver:
|
|
19
|
+
OPERATORS: typing.ClassVar[dict] = {
|
|
20
|
+
**builtins.BUILTINS,
|
|
21
|
+
"fractional": fractional,
|
|
22
|
+
"starts_with": starts_with,
|
|
23
|
+
"ends_with": ends_with,
|
|
24
|
+
"sem_ver": sem_ver,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: Config, provider: AbstractProvider):
|
|
28
|
+
self.config = config
|
|
29
|
+
self.provider = provider
|
|
30
|
+
if not self.config.offline_flag_source_path:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"offline_flag_source_path must be provided when using in-process resolver"
|
|
33
|
+
)
|
|
34
|
+
self.flag_store = FileWatcherFlagStore(
|
|
35
|
+
self.config.offline_flag_source_path,
|
|
36
|
+
self.provider,
|
|
37
|
+
self.config.offline_poll_interval_seconds,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def shutdown(self) -> None:
|
|
41
|
+
self.flag_store.shutdown()
|
|
42
|
+
|
|
43
|
+
def resolve_boolean_details(
|
|
44
|
+
self,
|
|
45
|
+
key: str,
|
|
46
|
+
default_value: bool,
|
|
47
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
48
|
+
) -> FlagResolutionDetails[bool]:
|
|
49
|
+
return self._resolve(key, default_value, evaluation_context)
|
|
50
|
+
|
|
51
|
+
def resolve_string_details(
|
|
52
|
+
self,
|
|
53
|
+
key: str,
|
|
54
|
+
default_value: str,
|
|
55
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
56
|
+
) -> FlagResolutionDetails[str]:
|
|
57
|
+
return self._resolve(key, default_value, evaluation_context)
|
|
58
|
+
|
|
59
|
+
def resolve_float_details(
|
|
60
|
+
self,
|
|
61
|
+
key: str,
|
|
62
|
+
default_value: float,
|
|
63
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
64
|
+
) -> FlagResolutionDetails[float]:
|
|
65
|
+
return self._resolve(key, default_value, evaluation_context)
|
|
66
|
+
|
|
67
|
+
def resolve_integer_details(
|
|
68
|
+
self,
|
|
69
|
+
key: str,
|
|
70
|
+
default_value: int,
|
|
71
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
72
|
+
) -> FlagResolutionDetails[int]:
|
|
73
|
+
return self._resolve(key, default_value, evaluation_context)
|
|
74
|
+
|
|
75
|
+
def resolve_object_details(
|
|
76
|
+
self,
|
|
77
|
+
key: str,
|
|
78
|
+
default_value: typing.Union[dict, list],
|
|
79
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
80
|
+
) -> FlagResolutionDetails[typing.Union[dict, list]]:
|
|
81
|
+
return self._resolve(key, default_value, evaluation_context)
|
|
82
|
+
|
|
83
|
+
def _resolve(
|
|
84
|
+
self,
|
|
85
|
+
key: str,
|
|
86
|
+
default_value: T,
|
|
87
|
+
evaluation_context: typing.Optional[EvaluationContext] = None,
|
|
88
|
+
) -> FlagResolutionDetails[T]:
|
|
89
|
+
flag = self.flag_store.get_flag(key)
|
|
90
|
+
if not flag:
|
|
91
|
+
raise FlagNotFoundError(f"Flag with key {key} not present in flag store.")
|
|
92
|
+
|
|
93
|
+
if flag.state == "DISABLED":
|
|
94
|
+
return FlagResolutionDetails(default_value, reason=Reason.DISABLED)
|
|
95
|
+
|
|
96
|
+
if not flag.targeting:
|
|
97
|
+
variant, value = flag.default
|
|
98
|
+
return FlagResolutionDetails(value, variant=variant, reason=Reason.STATIC)
|
|
99
|
+
|
|
100
|
+
json_logic_context = evaluation_context.attributes if evaluation_context else {}
|
|
101
|
+
json_logic_context["$flagd"] = {"flagKey": key, "timestamp": int(time.time())}
|
|
102
|
+
json_logic_context["targetingKey"] = (
|
|
103
|
+
evaluation_context.targeting_key if evaluation_context else None
|
|
104
|
+
)
|
|
105
|
+
variant = jsonLogic(flag.targeting, json_logic_context, self.OPERATORS)
|
|
106
|
+
if variant is None:
|
|
107
|
+
variant, value = flag.default
|
|
108
|
+
return FlagResolutionDetails(value, variant=variant, reason=Reason.DEFAULT)
|
|
109
|
+
if not isinstance(variant, (str, bool)):
|
|
110
|
+
raise ParseError(
|
|
111
|
+
"Parsed JSONLogic targeting did not return a string or bool"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
variant, value = flag.get_variant(variant)
|
|
115
|
+
if not value:
|
|
116
|
+
raise ParseError(f"Resolved variant {variant} not in variants config.")
|
|
117
|
+
|
|
118
|
+
return FlagResolutionDetails(
|
|
119
|
+
value,
|
|
120
|
+
variant=variant,
|
|
121
|
+
reason=Reason.TARGETING_MATCH,
|
|
122
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import mmh3
|
|
5
|
+
import semver
|
|
6
|
+
|
|
7
|
+
JsonPrimitive = typing.Union[str, bool, float, int]
|
|
8
|
+
JsonLogicArg = typing.Union[JsonPrimitive, typing.Sequence[JsonPrimitive]]
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("openfeature.contrib")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
|
|
14
|
+
if not args:
|
|
15
|
+
logger.error("No arguments provided to fractional operator.")
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
bucket_by = None
|
|
19
|
+
if isinstance(args[0], str):
|
|
20
|
+
bucket_by = args[0]
|
|
21
|
+
args = args[1:]
|
|
22
|
+
else:
|
|
23
|
+
seed = data.get("$flagd", {}).get("flagKey", "")
|
|
24
|
+
targeting_key = data.get("targetingKey")
|
|
25
|
+
if not targeting_key:
|
|
26
|
+
logger.error("No targetingKey provided for fractional shorthand syntax.")
|
|
27
|
+
return None
|
|
28
|
+
bucket_by = seed + targeting_key
|
|
29
|
+
|
|
30
|
+
if not bucket_by:
|
|
31
|
+
logger.error("No hashKey value resolved")
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
|
|
35
|
+
bucket = int(hash_ratio * 100)
|
|
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]
|
|
47
|
+
|
|
48
|
+
range_end = 0
|
|
49
|
+
for variant, weight in variant_weights:
|
|
50
|
+
range_end += weight
|
|
51
|
+
if bucket < range_end:
|
|
52
|
+
return variant
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def starts_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
|
|
58
|
+
def f(s1: str, s2: str) -> bool:
|
|
59
|
+
return s1.startswith(s2)
|
|
60
|
+
|
|
61
|
+
return string_comp(f, data, *args)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ends_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
|
|
65
|
+
def f(s1: str, s2: str) -> bool:
|
|
66
|
+
return s1.endswith(s2)
|
|
67
|
+
|
|
68
|
+
return string_comp(f, data, *args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def string_comp(
|
|
72
|
+
comparator: typing.Callable[[str, str], bool], data: dict, *args: JsonLogicArg
|
|
73
|
+
) -> typing.Optional[bool]:
|
|
74
|
+
if not args:
|
|
75
|
+
logger.error("No arguments provided to string_comp operator.")
|
|
76
|
+
return None
|
|
77
|
+
if len(args) != 2:
|
|
78
|
+
logger.error("Exactly 2 args expected for string_comp operator.")
|
|
79
|
+
return None
|
|
80
|
+
arg1, arg2 = args
|
|
81
|
+
if not isinstance(arg1, str):
|
|
82
|
+
logger.debug(f"incorrect argument for first argument, expected string: {arg1}")
|
|
83
|
+
return False
|
|
84
|
+
if not isinstance(arg2, str):
|
|
85
|
+
logger.debug(f"incorrect argument for second argument, expected string: {arg2}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
return comparator(arg1, arg2)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def sem_ver(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]: # noqa: C901
|
|
92
|
+
if not args:
|
|
93
|
+
logger.error("No arguments provided to sem_ver operator.")
|
|
94
|
+
return None
|
|
95
|
+
if len(args) != 3:
|
|
96
|
+
logger.error("Exactly 3 args expected for sem_ver operator.")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
arg1, op, arg2 = args
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
v1 = semver.Version.parse(str(arg1))
|
|
103
|
+
v2 = semver.Version.parse(str(arg2))
|
|
104
|
+
except ValueError as e:
|
|
105
|
+
logger.exception(e)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if op == "=":
|
|
109
|
+
return v1 == v2
|
|
110
|
+
elif op == "!=":
|
|
111
|
+
return v1 != v2
|
|
112
|
+
elif op == "<":
|
|
113
|
+
return v1 < v2
|
|
114
|
+
elif op == "<=":
|
|
115
|
+
return v1 <= v2
|
|
116
|
+
elif op == ">":
|
|
117
|
+
return v1 > v2
|
|
118
|
+
elif op == ">=":
|
|
119
|
+
return v1 >= v2
|
|
120
|
+
elif op == "^":
|
|
121
|
+
return v1.major == v2.major
|
|
122
|
+
elif op == "~":
|
|
123
|
+
return v1.major == v2.major and v1.minor == v2.minor
|
|
124
|
+
else:
|
|
125
|
+
logger.error(f"Op not supported by sem_ver: {op}")
|
|
126
|
+
return None
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from openfeature.event import ProviderEventDetails
|
|
12
|
+
from openfeature.exception import ParseError
|
|
13
|
+
from openfeature.provider.provider import AbstractProvider
|
|
14
|
+
|
|
15
|
+
from .flags import Flag
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("openfeature.contrib")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileWatcherFlagStore:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
file_path: str,
|
|
24
|
+
provider: AbstractProvider,
|
|
25
|
+
poll_interval_seconds: float = 1.0,
|
|
26
|
+
):
|
|
27
|
+
self.file_path = file_path
|
|
28
|
+
self.provider = provider
|
|
29
|
+
self.poll_interval_seconds = poll_interval_seconds
|
|
30
|
+
|
|
31
|
+
self.last_modified = 0.0
|
|
32
|
+
self.flag_data: typing.Mapping[str, Flag] = {}
|
|
33
|
+
self.load_data()
|
|
34
|
+
self.thread = threading.Thread(target=self.refresh_file, daemon=True)
|
|
35
|
+
self.thread.start()
|
|
36
|
+
|
|
37
|
+
def shutdown(self) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def get_flag(self, key: str) -> typing.Optional[Flag]:
|
|
41
|
+
return self.flag_data.get(key)
|
|
42
|
+
|
|
43
|
+
def refresh_file(self) -> None:
|
|
44
|
+
while True:
|
|
45
|
+
time.sleep(self.poll_interval_seconds)
|
|
46
|
+
logger.debug("checking for new flag store contents from file")
|
|
47
|
+
last_modified = os.path.getmtime(self.file_path)
|
|
48
|
+
if last_modified > self.last_modified:
|
|
49
|
+
self.load_data(last_modified)
|
|
50
|
+
|
|
51
|
+
def load_data(self, modified_time: typing.Optional[float] = None) -> None:
|
|
52
|
+
try:
|
|
53
|
+
with open(self.file_path) as file:
|
|
54
|
+
if self.file_path.endswith(".yaml"):
|
|
55
|
+
data = yaml.safe_load(file)
|
|
56
|
+
else:
|
|
57
|
+
data = json.load(file)
|
|
58
|
+
|
|
59
|
+
self.flag_data = self.parse_flags(data)
|
|
60
|
+
logger.debug(f"{self.flag_data=}")
|
|
61
|
+
self.provider.emit_provider_configuration_changed(
|
|
62
|
+
ProviderEventDetails(flags_changed=list(self.flag_data.keys()))
|
|
63
|
+
)
|
|
64
|
+
self.last_modified = modified_time or os.path.getmtime(self.file_path)
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
logger.exception("Provided file path not valid")
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
logger.exception("Could not parse JSON flag data from file")
|
|
69
|
+
except yaml.error.YAMLError:
|
|
70
|
+
logger.exception("Could not parse YAML flag data from file")
|
|
71
|
+
except ParseError:
|
|
72
|
+
logger.exception("Could not parse flag data using flagd syntax")
|
|
73
|
+
except Exception:
|
|
74
|
+
logger.exception("Could not read flags from file")
|
|
75
|
+
|
|
76
|
+
def parse_flags(self, flags_data: dict) -> dict:
|
|
77
|
+
flags = flags_data.get("flags", {})
|
|
78
|
+
evaluators: typing.Optional[dict] = flags_data.get("$evaluators")
|
|
79
|
+
if evaluators:
|
|
80
|
+
transposed = json.dumps(flags)
|
|
81
|
+
for name, rule in evaluators.items():
|
|
82
|
+
transposed = re.sub(
|
|
83
|
+
rf"{{\s*\"\$ref\":\s*\"{name}\"\s*}}", json.dumps(rule), transposed
|
|
84
|
+
)
|
|
85
|
+
flags = json.loads(transposed)
|
|
86
|
+
|
|
87
|
+
if not isinstance(flags, dict):
|
|
88
|
+
raise ParseError("`flags` key of configuration must be a dictionary")
|
|
89
|
+
return {key: Flag.from_dict(key, data) for key, data in flags.items()}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from openfeature.exception import ParseError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Flag:
|
|
9
|
+
key: str
|
|
10
|
+
state: str
|
|
11
|
+
variants: typing.Mapping[str, typing.Any]
|
|
12
|
+
default_variant: typing.Union[bool, str]
|
|
13
|
+
targeting: typing.Optional[dict] = None
|
|
14
|
+
|
|
15
|
+
def __post_init__(self) -> None:
|
|
16
|
+
if not self.state or not isinstance(self.state, str):
|
|
17
|
+
raise ParseError("Incorrect 'state' value provided in flag config")
|
|
18
|
+
|
|
19
|
+
if not self.variants or not isinstance(self.variants, dict):
|
|
20
|
+
raise ParseError("Incorrect 'variants' value provided in flag config")
|
|
21
|
+
|
|
22
|
+
if not self.default_variant or not isinstance(
|
|
23
|
+
self.default_variant, (str, bool)
|
|
24
|
+
):
|
|
25
|
+
raise ParseError("Incorrect 'defaultVariant' value provided in flag config")
|
|
26
|
+
|
|
27
|
+
if self.targeting and not isinstance(self.targeting, dict):
|
|
28
|
+
raise ParseError("Incorrect 'targeting' value provided in flag config")
|
|
29
|
+
|
|
30
|
+
if self.default_variant not in self.variants:
|
|
31
|
+
raise ParseError("Default variant does not match set of variants")
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, key: str, data: dict) -> "Flag":
|
|
35
|
+
data["default_variant"] = data["defaultVariant"]
|
|
36
|
+
del data["defaultVariant"]
|
|
37
|
+
flag = cls(key=key, **data)
|
|
38
|
+
|
|
39
|
+
return flag
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def default(self) -> typing.Tuple[str, typing.Any]:
|
|
43
|
+
return self.get_variant(self.default_variant)
|
|
44
|
+
|
|
45
|
+
def get_variant(
|
|
46
|
+
self, variant_key: typing.Union[str, bool]
|
|
47
|
+
) -> typing.Tuple[str, typing.Any]:
|
|
48
|
+
if isinstance(variant_key, bool):
|
|
49
|
+
variant_key = str(variant_key).lower()
|
|
50
|
+
|
|
51
|
+
return variant_key, self.variants.get(variant_key)
|
{openfeature_provider_flagd-0.1.4.dist-info → openfeature_provider_flagd-0.1.5.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: openfeature-provider-flagd
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: OpenFeature provider for the flagd flag evaluation engine
|
|
5
5
|
Project-URL: Homepage, https://github.com/open-feature/python-sdk-contrib
|
|
6
6
|
Author-email: OpenFeature <openfeature-core@groups.io>
|
|
@@ -211,8 +211,12 @@ Classifier: Programming Language :: Python
|
|
|
211
211
|
Classifier: Programming Language :: Python :: 3
|
|
212
212
|
Requires-Python: >=3.8
|
|
213
213
|
Requires-Dist: grpcio>=1.60.0
|
|
214
|
+
Requires-Dist: mmh3>=4.1.0
|
|
214
215
|
Requires-Dist: openfeature-sdk>=0.4.0
|
|
216
|
+
Requires-Dist: panzi-json-logic>=1.0.1
|
|
215
217
|
Requires-Dist: protobuf>=4.25.2
|
|
218
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
219
|
+
Requires-Dist: semver<4,>=3
|
|
216
220
|
Description-Content-Type: text/markdown
|
|
217
221
|
|
|
218
222
|
# flagd Provider for OpenFeature
|
|
@@ -236,6 +240,19 @@ from openfeature.contrib.provider.flagd import FlagdProvider
|
|
|
236
240
|
api.set_provider(FlagdProvider())
|
|
237
241
|
```
|
|
238
242
|
|
|
243
|
+
To use in-process evaluation in offline mode with a file as source:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from openfeature import api
|
|
247
|
+
from openfeature.contrib.provider.flagd import FlagdProvider
|
|
248
|
+
from openfeature.contrib.provider.flagd.config import ResolverType
|
|
249
|
+
|
|
250
|
+
api.set_provider(FlagdProvider(
|
|
251
|
+
resolver_type=ResolverType.IN_PROCESS,
|
|
252
|
+
offline_flag_source_path="my-flag.json",
|
|
253
|
+
))
|
|
254
|
+
```
|
|
255
|
+
|
|
239
256
|
### Configuration options
|
|
240
257
|
|
|
241
258
|
The default options can be defined in the FlagdProvider constructor.
|
{openfeature_provider_flagd-0.1.4.dist-info → openfeature_provider_flagd-0.1.5.dist-info}/RECORD
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
openfeature/contrib/provider/flagd/__init__.py,sha256=WlrcPaCH31dEG1IvrvpeuhAaQ8Ni8LEzDpNM_x-qKOA,65
|
|
2
|
-
openfeature/contrib/provider/flagd/config.py,sha256=
|
|
2
|
+
openfeature/contrib/provider/flagd/config.py,sha256=DF_pAm1jYWIUTS7564YHI1mLBb1v3z7MstRgMwUHsmA,1921
|
|
3
3
|
openfeature/contrib/provider/flagd/flag_type.py,sha256=rZYfmqQEmtqVVTb8e-d8Wt8ZCnHtf7xPSmYxyU8w0R0,158
|
|
4
|
-
openfeature/contrib/provider/flagd/provider.py,sha256=
|
|
4
|
+
openfeature/contrib/provider/flagd/provider.py,sha256=i5_hJmy-EdbltQaPfcbCk6j9k1YkUnKxgubzKIQK3dQ,4614
|
|
5
5
|
openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2.py,sha256=W0Vgg8z7nEg_HTlAJkO6pu5nBcR8Ls3yoIykboavLRI,7704
|
|
6
6
|
openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2_grpc.py,sha256=GTKQXbUfRrquUG5o11N7ymhFnOuzh9_hcToZ9-cjOtA,13339
|
|
7
7
|
openfeature/contrib/provider/flagd/proto/flagd/sync/v1/sync_pb2.py,sha256=zUu_-V6DhHH5ZekWVsxKjGkUwkJrRrhqyXj8oX-Jo_g,3205
|
|
@@ -10,7 +10,13 @@ openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2.py,sha256=onIastnV
|
|
|
10
10
|
openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2_grpc.py,sha256=nllthJYfwhDBZHH_eGfSHesNbO6rJEZ0vXKyTFFy4AA,12393
|
|
11
11
|
openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2.py,sha256=uKBjB_lpfHN3r8wsjmqnQEBLtN9gw8zPmvWfWKsOYAE,2954
|
|
12
12
|
openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2_grpc.py,sha256=v5MSyyIOpE68EYWjsbdAq677gnD4jdNeLhMAB7EveNQ,4424
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
openfeature/contrib/provider/flagd/resolvers/__init__.py,sha256=kM5bt6cJCQgCsvJd1Tb2YD050X06wk8rK249JmNjoUc,1468
|
|
14
|
+
openfeature/contrib/provider/flagd/resolvers/grpc.py,sha256=9F-jX08Ynar5d2cgTV90lDQxgxIlxKg9KEIwueCU3u8,5460
|
|
15
|
+
openfeature/contrib/provider/flagd/resolvers/in_process.py,sha256=-RGKP9xC4Ic65-gBLvjMJJ-7EKDKsKnEu_0GdqGHaKk,4383
|
|
16
|
+
openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py,sha256=8vscgX-SA_lnjb8Tg4S_qrVspqeVwtyXc8F846tcdc8,3644
|
|
17
|
+
openfeature/contrib/provider/flagd/resolvers/process/file_watcher.py,sha256=0GMA-gs6PchT4tW_daV1LwFmCw21mVa_WawusW72xWA,3191
|
|
18
|
+
openfeature/contrib/provider/flagd/resolvers/process/flags.py,sha256=aE-i-ghpjoGejOwP3K4DotcdHYw_o393oxrMaBmpgko,1718
|
|
19
|
+
openfeature_provider_flagd-0.1.5.dist-info/METADATA,sha256=rWFt0M5Qb54eiiLaEPhhHVCzwY5PlxLYZwEToiMaCTA,15005
|
|
20
|
+
openfeature_provider_flagd-0.1.5.dist-info/WHEEL,sha256=as-1oFTWSeWBgyzh0O_qF439xqBe6AbBgt4MfYe5zwY,87
|
|
21
|
+
openfeature_provider_flagd-0.1.5.dist-info/licenses/LICENSE,sha256=h8jwqxShIeVkc8vOo9ynxGYW16f4fVPxLhZKZs0H5U8,11350
|
|
22
|
+
openfeature_provider_flagd-0.1.5.dist-info/RECORD,,
|
|
File without changes
|