spotify-confidence-sdk 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.
- confidence/__init__.py +6 -0
- confidence/_version.py +16 -0
- confidence/confidence.py +286 -0
- confidence/errors.py +127 -0
- confidence/flag_types.py +68 -0
- confidence/names.py +29 -0
- confidence/openfeature_provider.py +206 -0
- spotify_confidence_sdk-0.2.1.dist-info/LICENSE +202 -0
- spotify_confidence_sdk-0.2.1.dist-info/METADATA +285 -0
- spotify_confidence_sdk-0.2.1.dist-info/RECORD +16 -0
- spotify_confidence_sdk-0.2.1.dist-info/WHEEL +5 -0
- spotify_confidence_sdk-0.2.1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/test_names.py +30 -0
- tests/test_provider.py +318 -0
- tests/test_provider_parametrized.py +29 -0
confidence/__init__.py
ADDED
confidence/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.2.1'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 2, 1)
|
confidence/confidence.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import dataclasses
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import logging
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Dict,
|
|
9
|
+
List,
|
|
10
|
+
Optional,
|
|
11
|
+
Type,
|
|
12
|
+
Union,
|
|
13
|
+
get_args,
|
|
14
|
+
get_origin,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
from typing_extensions import TypeGuard
|
|
19
|
+
|
|
20
|
+
from confidence import __version__
|
|
21
|
+
from confidence.errors import (
|
|
22
|
+
FlagNotFoundError,
|
|
23
|
+
ParseError,
|
|
24
|
+
TypeMismatchError,
|
|
25
|
+
)
|
|
26
|
+
from .flag_types import FlagResolutionDetails, Reason
|
|
27
|
+
from .names import FlagName, VariantName
|
|
28
|
+
|
|
29
|
+
EU_RESOLVE_API_ENDPOINT = "https://resolver.eu.confidence.dev/v1"
|
|
30
|
+
US_RESOLVE_API_ENDPOINT = "https://resolver.us.confidence.dev/v1"
|
|
31
|
+
GLOBAL_RESOLVE_API_ENDPOINT = "https://resolver.confidence.dev/v1"
|
|
32
|
+
|
|
33
|
+
Primitive = Union[str, int, float, bool, None]
|
|
34
|
+
FieldType = Union[Primitive, List[Primitive], List["Object"], "Object"]
|
|
35
|
+
Object = Dict[str, FieldType]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_primitive(field_type: Type[Any]) -> TypeGuard[Type[Primitive]]:
|
|
39
|
+
return field_type in get_args(Primitive)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def primitive_matches(value: FieldType, value_type: Type[Primitive]) -> bool:
|
|
43
|
+
return (
|
|
44
|
+
value_type is None
|
|
45
|
+
or (value_type is int and isinstance(value, int))
|
|
46
|
+
or (value_type is float and isinstance(value, float))
|
|
47
|
+
or (value_type is str and isinstance(value, str))
|
|
48
|
+
or (value_type is bool and isinstance(value, bool))
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Region(Enum):
|
|
53
|
+
def endpoint(self) -> str:
|
|
54
|
+
return self.value
|
|
55
|
+
|
|
56
|
+
EU = EU_RESOLVE_API_ENDPOINT
|
|
57
|
+
US = US_RESOLVE_API_ENDPOINT
|
|
58
|
+
GLOBAL = GLOBAL_RESOLVE_API_ENDPOINT
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclasses.dataclass
|
|
62
|
+
class ResolveResult(object):
|
|
63
|
+
value: Optional[Object]
|
|
64
|
+
variant: Optional[str]
|
|
65
|
+
token: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Confidence:
|
|
69
|
+
context: Dict[str, FieldType] = {}
|
|
70
|
+
|
|
71
|
+
def put_context(self, key: str, value: FieldType) -> None:
|
|
72
|
+
self.context[key] = value
|
|
73
|
+
|
|
74
|
+
def with_context(self, context: Dict[str, FieldType]) -> "Confidence":
|
|
75
|
+
new_confidence = Confidence(
|
|
76
|
+
self._client_secret, self._region, self._apply_on_resolve
|
|
77
|
+
)
|
|
78
|
+
new_confidence.context = {**self.context, **context}
|
|
79
|
+
return new_confidence
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
client_secret: str,
|
|
84
|
+
region: Region = Region.GLOBAL,
|
|
85
|
+
apply_on_resolve: bool = True,
|
|
86
|
+
logger: logging.Logger = logging.getLogger("confidence_logger"),
|
|
87
|
+
):
|
|
88
|
+
self._client_secret = client_secret
|
|
89
|
+
self._region = region
|
|
90
|
+
self._api_endpoint = region.endpoint()
|
|
91
|
+
self._apply_on_resolve = apply_on_resolve
|
|
92
|
+
self.logger = logger
|
|
93
|
+
self._setup_logger(logger)
|
|
94
|
+
|
|
95
|
+
def resolve_boolean_details(
|
|
96
|
+
self, flag_key: str, default_value: bool
|
|
97
|
+
) -> FlagResolutionDetails[bool]:
|
|
98
|
+
return self._evaluate(flag_key, bool, default_value, self.context)
|
|
99
|
+
|
|
100
|
+
def resolve_float_details(
|
|
101
|
+
self, flag_key: str, default_value: float
|
|
102
|
+
) -> FlagResolutionDetails[float]:
|
|
103
|
+
return self._evaluate(flag_key, float, default_value, self.context)
|
|
104
|
+
|
|
105
|
+
def resolve_integer_details(
|
|
106
|
+
self, flag_key: str, default_value: int
|
|
107
|
+
) -> FlagResolutionDetails[int]:
|
|
108
|
+
return self._evaluate(flag_key, int, default_value, self.context)
|
|
109
|
+
|
|
110
|
+
def resolve_string_details(
|
|
111
|
+
self, flag_key: str, default_value: str
|
|
112
|
+
) -> FlagResolutionDetails[str]:
|
|
113
|
+
return self._evaluate(flag_key, str, default_value, self.context)
|
|
114
|
+
|
|
115
|
+
def resolve_object_details(
|
|
116
|
+
self, flag_key: str, default_value: Union[Object, List[Primitive]]
|
|
117
|
+
) -> FlagResolutionDetails[Union[Object, List[Primitive]]]:
|
|
118
|
+
return self._evaluate(flag_key, Object, default_value, self.context)
|
|
119
|
+
|
|
120
|
+
def _setup_logger(self, logger: logging.Logger) -> None:
|
|
121
|
+
if logger is not None:
|
|
122
|
+
logger.setLevel(logging.DEBUG)
|
|
123
|
+
formatter = logging.Formatter(
|
|
124
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
125
|
+
)
|
|
126
|
+
if not logger.hasHandlers():
|
|
127
|
+
ch = logging.StreamHandler()
|
|
128
|
+
ch.setFormatter(formatter)
|
|
129
|
+
logger.addHandler(ch)
|
|
130
|
+
|
|
131
|
+
#
|
|
132
|
+
# --- internals
|
|
133
|
+
#
|
|
134
|
+
|
|
135
|
+
def _evaluate(
|
|
136
|
+
self,
|
|
137
|
+
flag_key: str,
|
|
138
|
+
value_type: Type[FieldType],
|
|
139
|
+
default_value: FieldType,
|
|
140
|
+
context: Dict[str, FieldType],
|
|
141
|
+
) -> FlagResolutionDetails[Any]:
|
|
142
|
+
if "." in flag_key:
|
|
143
|
+
flag_id, value_path = flag_key.split(".", 1)
|
|
144
|
+
else:
|
|
145
|
+
flag_id = flag_key
|
|
146
|
+
value_path = None
|
|
147
|
+
result = self._resolve(FlagName(flag_id), context)
|
|
148
|
+
if result.variant is None or len(str(result.value)) == 0:
|
|
149
|
+
return FlagResolutionDetails(
|
|
150
|
+
value=default_value,
|
|
151
|
+
reason=Reason.DEFAULT,
|
|
152
|
+
flag_metadata={"flag_key": flag_key},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
variant_name = VariantName.parse(result.variant)
|
|
156
|
+
|
|
157
|
+
value = self._select(result, value_path, value_type, self.logger)
|
|
158
|
+
if value is None:
|
|
159
|
+
self.logger.debug(
|
|
160
|
+
f"Flag {flag_key} resolved to None. Returning default value."
|
|
161
|
+
)
|
|
162
|
+
value = default_value
|
|
163
|
+
|
|
164
|
+
return FlagResolutionDetails(
|
|
165
|
+
value=value,
|
|
166
|
+
variant=variant_name.variant,
|
|
167
|
+
reason=Reason.TARGETING_MATCH,
|
|
168
|
+
flag_metadata={"flag_key": flag_key},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# type-arg: ignore
|
|
172
|
+
def track(self, event_name: str, data: Dict[str, FieldType]) -> None:
|
|
173
|
+
asyncio.create_task(self._send_event(event_name, data))
|
|
174
|
+
|
|
175
|
+
async def track_async(self, event_name: str, data: Dict[str, FieldType]) -> None:
|
|
176
|
+
await self._send_event(event_name, data)
|
|
177
|
+
|
|
178
|
+
async def _send_event(self, event_name: str, data: Dict[str, FieldType]) -> None:
|
|
179
|
+
current_time = datetime.utcnow().isoformat() + "Z"
|
|
180
|
+
request_body = {
|
|
181
|
+
"clientSecret": self._client_secret,
|
|
182
|
+
"sendTime": current_time,
|
|
183
|
+
"events": [
|
|
184
|
+
{
|
|
185
|
+
"eventDefinition": f"eventDefinitions/{event_name}",
|
|
186
|
+
"payload": {"context": {**self.context}, **data},
|
|
187
|
+
"eventTime": current_time,
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"sdk": {"id": "SDK_ID_PYTHON_CONFIDENCE", "version": __version__},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
event_url = "https://events.confidence.dev/v1/events:publish"
|
|
194
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
195
|
+
response = requests.post(event_url, json=request_body, headers=headers)
|
|
196
|
+
response.raise_for_status()
|
|
197
|
+
json = response.json()
|
|
198
|
+
|
|
199
|
+
json_errors = json.get("errors")
|
|
200
|
+
if json_errors:
|
|
201
|
+
self.logger.warn("events emitted with errors:")
|
|
202
|
+
for error in json_errors:
|
|
203
|
+
self.logger.warn(error)
|
|
204
|
+
|
|
205
|
+
def _resolve(
|
|
206
|
+
self, flag_name: FlagName, context: Dict[str, FieldType]
|
|
207
|
+
) -> ResolveResult:
|
|
208
|
+
request_body = {
|
|
209
|
+
"clientSecret": self._client_secret,
|
|
210
|
+
"evaluationContext": context,
|
|
211
|
+
"apply": self._apply_on_resolve,
|
|
212
|
+
"flags": [str(flag_name)],
|
|
213
|
+
"sdk": {"id": "SDK_ID_PYTHON_CONFIDENCE", "version": __version__},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
resolve_url = f"{self._api_endpoint}/flags:resolve"
|
|
217
|
+
response = requests.post(resolve_url, json=request_body)
|
|
218
|
+
if response.status_code == 404:
|
|
219
|
+
self.logger.error(f"Flag {flag_name} not found")
|
|
220
|
+
raise FlagNotFoundError()
|
|
221
|
+
|
|
222
|
+
response.raise_for_status()
|
|
223
|
+
|
|
224
|
+
response_body = response.json()
|
|
225
|
+
|
|
226
|
+
resolved_flags = response_body["resolvedFlags"]
|
|
227
|
+
token = response_body["resolveToken"]
|
|
228
|
+
|
|
229
|
+
if len(resolved_flags) == 0:
|
|
230
|
+
self.logger.info(f"Flag {flag_name} not found")
|
|
231
|
+
return ResolveResult(None, None, token)
|
|
232
|
+
|
|
233
|
+
resolved_flag = resolved_flags[0]
|
|
234
|
+
variant = resolved_flag.get("variant")
|
|
235
|
+
return ResolveResult(
|
|
236
|
+
resolved_flag.get("value"), None if variant == "" else variant, token
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _select(
|
|
241
|
+
result: ResolveResult,
|
|
242
|
+
value_path: Optional[str],
|
|
243
|
+
value_type: Type[FieldType],
|
|
244
|
+
logger: logging.Logger,
|
|
245
|
+
) -> FieldType:
|
|
246
|
+
value: FieldType = result.value
|
|
247
|
+
|
|
248
|
+
if value_path is not None:
|
|
249
|
+
keys = value_path.split(".")
|
|
250
|
+
for key in keys:
|
|
251
|
+
if not isinstance(value, dict):
|
|
252
|
+
logger.debug(f"Value {value} is not a dict. Returning None.")
|
|
253
|
+
raise ParseError()
|
|
254
|
+
|
|
255
|
+
if key not in value:
|
|
256
|
+
logger.debug(
|
|
257
|
+
f"Key {key} not found in value {value}. Returning None."
|
|
258
|
+
)
|
|
259
|
+
raise ParseError()
|
|
260
|
+
|
|
261
|
+
value = value.get(key)
|
|
262
|
+
|
|
263
|
+
# skip type checking if the value was not specified
|
|
264
|
+
if value is None:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
if not Confidence.type_matches(value, value_type):
|
|
268
|
+
logger.debug(
|
|
269
|
+
f"Type of value {value} did not match expected type {value_type}."
|
|
270
|
+
)
|
|
271
|
+
raise TypeMismatchError("type of value did not match excepted type")
|
|
272
|
+
|
|
273
|
+
return value
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def type_matches(value: FieldType, value_type: Type[FieldType]) -> bool:
|
|
277
|
+
origin = get_origin(value_type)
|
|
278
|
+
|
|
279
|
+
if is_primitive(value_type):
|
|
280
|
+
return primitive_matches(value, value_type)
|
|
281
|
+
elif origin is list:
|
|
282
|
+
return isinstance(value, list)
|
|
283
|
+
elif origin is dict:
|
|
284
|
+
return isinstance(value, dict)
|
|
285
|
+
|
|
286
|
+
return False
|
confidence/errors.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ErrorCode(Enum):
|
|
6
|
+
NOT_READY = "NOT_READY"
|
|
7
|
+
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
|
|
8
|
+
PARSE_ERROR = "PARSE_ERROR"
|
|
9
|
+
TYPE_MISMATCH = "TYPE_MISMATCH"
|
|
10
|
+
TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
|
|
11
|
+
INVALID_CONTEXT = "INVALID_CONTEXT"
|
|
12
|
+
GENERAL = "GENERAL"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfidenceError(Exception):
|
|
16
|
+
"""
|
|
17
|
+
A generic confidence exception, this exception should not be raised. Instead
|
|
18
|
+
the more specific exceptions extending this one should be used.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, error_code: ErrorCode, error_message: typing.Optional[str] = None
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Constructor for the generic ConfidenceError.
|
|
26
|
+
@param error_message: an optional string message representing why the
|
|
27
|
+
error has been raised
|
|
28
|
+
@param error_code: the ErrorCode string enum value for the type of error
|
|
29
|
+
"""
|
|
30
|
+
self.error_message = error_message
|
|
31
|
+
self.error_code = error_code
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FlagNotFoundError(ConfidenceError):
|
|
35
|
+
"""
|
|
36
|
+
This exception should be raised when the provider cannot find a flag with the
|
|
37
|
+
key provided by the user.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, error_message: typing.Optional[str] = None):
|
|
41
|
+
"""
|
|
42
|
+
Constructor for the FlagNotFoundError. The error code for
|
|
43
|
+
this type of exception is ErrorCode.FLAG_NOT_FOUND.
|
|
44
|
+
@param error_message: an optional string message representing
|
|
45
|
+
why the error has been raised
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(ErrorCode.FLAG_NOT_FOUND, error_message)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GeneralError(ConfidenceError):
|
|
51
|
+
"""
|
|
52
|
+
This exception should be raised when the for an exception within the open
|
|
53
|
+
feature python sdk.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, error_message: typing.Optional[str] = None):
|
|
57
|
+
"""
|
|
58
|
+
Constructor for the GeneralError. The error code for this type of exception
|
|
59
|
+
is ErrorCode.GENERAL.
|
|
60
|
+
@param error_message: an optional string message representing why the error
|
|
61
|
+
has been raised
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(ErrorCode.GENERAL, error_message)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ParseError(ConfidenceError):
|
|
67
|
+
"""
|
|
68
|
+
This exception should be raised when the flag returned by the provider cannot
|
|
69
|
+
be parsed into a FlagEvaluationDetails object.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, error_message: typing.Optional[str] = None):
|
|
73
|
+
"""
|
|
74
|
+
Constructor for the ParseError. The error code for this type of exception
|
|
75
|
+
is ErrorCode.PARSE_ERROR.
|
|
76
|
+
@param error_message: an optional string message representing why the
|
|
77
|
+
error has been raised
|
|
78
|
+
"""
|
|
79
|
+
super().__init__(ErrorCode.PARSE_ERROR, error_message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TypeMismatchError(ConfidenceError):
|
|
83
|
+
"""
|
|
84
|
+
This exception should be raised when the flag returned by the provider does
|
|
85
|
+
not match the type requested by the user.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, error_message: typing.Optional[str] = None):
|
|
89
|
+
"""
|
|
90
|
+
Constructor for the TypeMismatchError. The error code for this type of
|
|
91
|
+
exception is ErrorCode.TYPE_MISMATCH.
|
|
92
|
+
@param error_message: an optional string message representing why the
|
|
93
|
+
error has been raised
|
|
94
|
+
"""
|
|
95
|
+
super().__init__(ErrorCode.TYPE_MISMATCH, error_message)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TargetingKeyMissingError(ConfidenceError):
|
|
99
|
+
"""
|
|
100
|
+
This exception should be raised when the provider requires a targeting key
|
|
101
|
+
but one was not provided in the evaluation context.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, error_message: typing.Optional[str] = None):
|
|
105
|
+
"""
|
|
106
|
+
Constructor for the TargetingKeyMissingError. The error code for this type of
|
|
107
|
+
exception is ErrorCode.TARGETING_KEY_MISSING.
|
|
108
|
+
@param error_message: a string message representing why the error has been
|
|
109
|
+
raised
|
|
110
|
+
"""
|
|
111
|
+
super().__init__(ErrorCode.TARGETING_KEY_MISSING, error_message)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class InvalidContextError(ConfidenceError):
|
|
115
|
+
"""
|
|
116
|
+
This exception should be raised when the evaluation context does not meet provider
|
|
117
|
+
requirements.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, error_message: typing.Optional[str]):
|
|
121
|
+
"""
|
|
122
|
+
Constructor for the InvalidContextError. The error code for this type of
|
|
123
|
+
exception is ErrorCode.INVALID_CONTEXT.
|
|
124
|
+
@param error_message: a string message representing why the error has been
|
|
125
|
+
raised
|
|
126
|
+
"""
|
|
127
|
+
super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
|
confidence/flag_types.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import typing
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from confidence.errors import ErrorCode
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 11):
|
|
10
|
+
# re-export needed for type checking
|
|
11
|
+
from enum import StrEnum as StrEnum # noqa: PLC0414
|
|
12
|
+
else:
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
class StrEnum(str, Enum):
|
|
16
|
+
"""
|
|
17
|
+
Backport StrEnum for Python <3.11
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FlagType(StrEnum):
|
|
24
|
+
BOOLEAN = "BOOLEAN"
|
|
25
|
+
STRING = "STRING"
|
|
26
|
+
OBJECT = "OBJECT"
|
|
27
|
+
FLOAT = "FLOAT"
|
|
28
|
+
INTEGER = "INTEGER"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Reason(StrEnum):
|
|
32
|
+
CACHED = "CACHED"
|
|
33
|
+
DEFAULT = "DEFAULT"
|
|
34
|
+
DISABLED = "DISABLED"
|
|
35
|
+
ERROR = "ERROR"
|
|
36
|
+
STATIC = "STATIC"
|
|
37
|
+
SPLIT = "SPLIT"
|
|
38
|
+
TARGETING_MATCH = "TARGETING_MATCH"
|
|
39
|
+
UNKNOWN = "UNKNOWN"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
FlagMetadata = typing.Mapping[str, typing.Any]
|
|
43
|
+
|
|
44
|
+
T_co = typing.TypeVar("T_co", covariant=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class FlagEvaluationDetails(typing.Generic[T_co]):
|
|
49
|
+
flag_key: str
|
|
50
|
+
value: T_co
|
|
51
|
+
variant: typing.Optional[str] = None
|
|
52
|
+
flag_metadata: FlagMetadata = field(default_factory=dict)
|
|
53
|
+
reason: typing.Optional[typing.Union[str, Reason]] = None
|
|
54
|
+
error_code: typing.Optional[ErrorCode] = None
|
|
55
|
+
error_message: typing.Optional[str] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
U_co = typing.TypeVar("U_co", covariant=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class FlagResolutionDetails(typing.Generic[U_co]):
|
|
63
|
+
value: U_co
|
|
64
|
+
error_code: typing.Optional[ErrorCode] = None
|
|
65
|
+
error_message: typing.Optional[str] = None
|
|
66
|
+
reason: typing.Optional[typing.Union[str, Reason]] = None
|
|
67
|
+
variant: typing.Optional[str] = None
|
|
68
|
+
flag_metadata: FlagMetadata = field(default_factory=dict)
|
confidence/names.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclasses.dataclass
|
|
5
|
+
class FlagName(object):
|
|
6
|
+
flag: str
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def parse(cls, resource_name: str) -> "FlagName":
|
|
10
|
+
components = resource_name.split("/", 2)
|
|
11
|
+
if components[0] != "flags":
|
|
12
|
+
raise ValueError("name error")
|
|
13
|
+
return cls(components[1])
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
return f"flags/{self.flag}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclasses.dataclass
|
|
20
|
+
class VariantName(object):
|
|
21
|
+
flag: str
|
|
22
|
+
variant: str
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def parse(cls, resource_name: str) -> "VariantName":
|
|
26
|
+
components = resource_name.split("/")
|
|
27
|
+
if components[0] != "flags" or components[2] != "variants":
|
|
28
|
+
raise ValueError("name error")
|
|
29
|
+
return cls(components[1], components[3])
|