Skip to content

Commit 1aed3cb

Browse files
kp-catnoirbizarre
andauthored
Config v6 (#50)
* Expose Python 3.11 support * add a tox.ini config for easier local testing * Drop unsupported Python versions and update syntax accordingly * Add Flake8 linting to tox * Fix semver deprecations warnings * Export typing using PEP-561 py.typed * Add typing check and expose linting settings * Simplify github actions workflow - run test and coverage in a single pass - use official codecov action to upload coverage - add typing analysis - run flake8 once and rely on settings * Fix all lints * Update contributing guide and document `tox` usage * Ignore python:S4790 intentional Sonar errors * reverse commit: Drop unsupported Python versions and update syntax accordingly * update CONTRIBUTING.md * remove MyPy check * universal bdist_wheel * intro config v6 json format * add comparators * conditions check * dependent flag logging into the same log_entries * configclient get_config fixes * dependency loop check * testmatrix comparators_v6 * testmatrix segments * testmatrix dependent flag * testmatrix: AndOr * dependent flag logging * comments * TODO: percentage_rule_attribute * sdk key validation check * percentage_rule_attribute log * move sha256 calculation into a function * finalize no percentage_rule_attribute error handling * introduce typed value in override + test fixes * linter fixes * cleanup * github test fix * custom percentage attribute * IS NOT IN SEGMENT fix * operator updates * update tests * lint fixes * circular dependency test * new evaluation logging (WIP) * github action: python 2.7 support * fix user json key order on python 2.7 * Remove the u prefix from unicode strings on python 2.7 in the eval log tests * evaluation logging * lint fixes * fix tests * test_options_within_targeting_rule * lint fix * typo fix * handling the modified config json format * evaluation log test + generator + data * lint fix * Adjust evaluation and update evaluation tests * In case of local only flag overrides mode, we accept any SDK Key format * rename comparators * NOT STARTS WITH ANY OF (hashed), NOT ENDS WITH ANY OF (hashed) comparators * eval log tests: validation error handling * lint fixes * consistent trim logic during evaluation * log fix * test incorrect json * evaluation log update: hashed value + max 10 length lists * indentation fix * matched_evaluation_rule -> matched_targeting_rule, matched_evaluation_percentage_rule -> matched_percentage_rule * evallogging: list logging fix * update matrix tests + eval log tests * Fix list_truncation.txt * Turn off python 2.7 build * attr_value_from_datetime + attr_value_from_list * inline salt, segment for handling flag override * fix python 3.5 test * check if the prerequisite key exists * remove unnecessary served_value get * add `any of` tests to testmatrix_comparators_v6 * rename config members (comparision_rule -> user_condition, segment_rule -> segment_condition) * config descriptor * github-ci fix * fix python 2.7 tests * github ci: python 3.5 * Adjust config model and tests to config v6 schema changes * matrix test update + new cleartext comparators * unicode tests * segments_old matrix test + segment eval log fix * python 2.7 unicode support * fix evaluation log test on python <= 3.5 * type mismatched user attribute warning * python 3.12 support * lint fix + review fixes * python 2.7 fix * github ci: win test fix * review fixes + exceptions in prerequisite flag evaluation * lint fix * don't force users to pass user object attributes as strings * test: evaluation_details_matched_evaluation_rule_and_percentage_option * DefaultValue and SettingType mismatch warning * forced setting_type check * python 2.7 support (timezone, unicode/str handling) * Read cache upon each setting read (lazy cache fix) * fix coding convention * bump version 9.0.0 * ConfigService's refresh offline warning * comment updates * remove unsupported value error log --------- Co-authored-by: Axel H <[email protected]>
1 parent 241805f commit 1aed3cb

File tree

114 files changed

+3715
-721
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

114 files changed

+3715
-721
lines changed

.github/workflows/python-ci.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ on:
1414
jobs:
1515
test:
1616

17-
runs-on: ubuntu-20.04
17+
runs-on: ${{ matrix.os }}
1818
strategy:
1919
matrix:
20-
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
20+
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
21+
os: [ windows-latest, macos-latest, ubuntu-20.04 ]
2122

2223
steps:
2324
- uses: actions/checkout@v3
@@ -36,7 +37,7 @@ jobs:
3637
- name: Install dependencies
3738
run: |
3839
python -m pip install --upgrade pip
39-
pip install pytest pytest-cov mock flake8
40+
pip install pytest pytest-cov parameterized mock flake8
4041
pip install -r requirements.txt
4142
4243
- name: Lint with flake8

configcatclient/config.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
from enum import IntEnum
2+
3+
CONFIG_FILE_NAME = 'config_v6'
4+
SERIALIZATION_FORMAT_VERSION = 'v2'
5+
6+
# Config
7+
PREFERENCES = 'p'
8+
SEGMENTS = 's'
9+
FEATURE_FLAGS = 'f'
10+
11+
# Preferences
12+
BASE_URL = 'u'
13+
REDIRECT = 'r'
14+
SALT = 's'
15+
16+
# Segment
17+
SEGMENT_NAME = 'n' # The first 4 characters of the Segment's name
18+
SEGMENT_CONDITIONS = 'r' # The list of segment rule conditions (has a logical AND relation between the items).
19+
20+
# Segment Condition (User Condition)
21+
COMPARISON_ATTRIBUTE = 'a' # The attribute of the user object that should be used to evaluate this rule
22+
COMPARATOR = 'c'
23+
24+
# Feature flag (Evaluation Formula)
25+
SETTING_TYPE = 't' # 0 = bool, 1 = string, 2 = int, 3 = double
26+
PERCENTAGE_RULE_ATTRIBUTE = 'a' # Percentage rule evaluation hashes this attribute of the User object to calculate the buckets
27+
TARGETING_RULES = 'r' # Targeting Rules (Logically connected by OR)
28+
PERCENTAGE_OPTIONS = 'p' # Percentage Options without conditions
29+
VALUE = 'v'
30+
VARIATION_ID = 'i'
31+
INLINE_SALT = 'inline_salt'
32+
33+
# Targeting Rule (Evaluation Rule)
34+
CONDITIONS = 'c'
35+
SERVED_VALUE = 's' # Value and Variation ID
36+
TARGETING_RULE_PERCENTAGE_OPTIONS = 'p'
37+
38+
# Condition
39+
USER_CONDITION = 'u'
40+
SEGMENT_CONDITION = 's' # Segment targeting rule
41+
PREREQUISITE_FLAG_CONDITION = 'p' # Prerequisite flag targeting rule
42+
43+
# Segment Condition
44+
SEGMENT_INDEX = 's'
45+
SEGMENT_COMPARATOR = 'c'
46+
INLINE_SEGMENT = 'inline_segment'
47+
48+
# Prerequisite Flag Condition
49+
PREREQUISITE_FLAG_KEY = 'f'
50+
PREREQUISITE_COMPARATOR = 'c'
51+
52+
# Percentage Option
53+
PERCENTAGE = 'p'
54+
55+
# Value
56+
BOOL_VALUE = 'b'
57+
STRING_VALUE = 's'
58+
INT_VALUE = 'i'
59+
DOUBLE_VALUE = 'd'
60+
STRING_LIST_VALUE = 'l'
61+
UNSUPPORTED_VALUE = 'unsupported_value'
62+
63+
64+
def get_value(dictionary, setting_type):
65+
value_descriptor = dictionary.get(VALUE)
66+
if value_descriptor is None:
67+
raise ValueError('Value is missing')
68+
69+
if setting_type not in list(map(int, SettingType)):
70+
raise ValueError('Unsupported setting type')
71+
72+
expected_value_type, expected_py_type = SettingType.get_type_info(setting_type)
73+
if expected_value_type is None:
74+
raise ValueError('Unsupported setting type')
75+
76+
value = value_descriptor.get(expected_value_type)
77+
if value is None:
78+
raise ValueError('Setting value is not of the expected type %s' % expected_py_type)
79+
80+
return value
81+
82+
83+
def get_value_type(dictionary):
84+
value = dictionary.get(VALUE)
85+
if value is not None:
86+
if value.get(BOOL_VALUE) is not None:
87+
return bool
88+
if value.get(STRING_VALUE) is not None:
89+
return str
90+
if value.get(INT_VALUE) is not None:
91+
return int
92+
if value.get(DOUBLE_VALUE) is not None:
93+
return float
94+
95+
return None
96+
97+
98+
class SettingType(IntEnum):
99+
BOOL = 0
100+
STRING = 1
101+
INT = 2
102+
DOUBLE = 3
103+
104+
@staticmethod
105+
def get_type_info(setting_type):
106+
return setting_type_mapping.get(setting_type, (None, None))
107+
108+
@staticmethod
109+
def from_type(object_type):
110+
if object_type is bool:
111+
return SettingType.BOOL
112+
if object_type is str:
113+
return SettingType.STRING
114+
if object_type is int:
115+
return SettingType.INT
116+
if object_type is float:
117+
return SettingType.DOUBLE
118+
119+
return None
120+
121+
@staticmethod
122+
def to_type(setting_type):
123+
return SettingType.get_type_info(setting_type)[1]
124+
125+
@staticmethod
126+
def to_value_type(setting_type):
127+
return SettingType.get_type_info(setting_type)[0]
128+
129+
130+
setting_type_mapping = {
131+
SettingType.BOOL: (BOOL_VALUE, bool),
132+
SettingType.STRING: (STRING_VALUE, str),
133+
SettingType.INT: (INT_VALUE, int),
134+
SettingType.DOUBLE: (DOUBLE_VALUE, float)
135+
}
136+
137+
138+
class PrerequisiteComparator(IntEnum):
139+
EQUALS = 0
140+
NOT_EQUALS = 1
141+
142+
143+
class SegmentComparator(IntEnum):
144+
IS_IN = 0
145+
IS_NOT_IN = 1
146+
147+
148+
class Comparator(IntEnum):
149+
IS_ONE_OF = 0
150+
IS_NOT_ONE_OF = 1
151+
CONTAINS_ANY_OF = 2
152+
NOT_CONTAINS_ANY_OF = 3
153+
IS_ONE_OF_SEMVER = 4
154+
IS_NOT_ONE_OF_SEMVER = 5
155+
LESS_THAN_SEMVER = 6
156+
LESS_THAN_OR_EQUAL_SEMVER = 7
157+
GREATER_THAN_SEMVER = 8
158+
GREATER_THAN_OR_EQUAL_SEMVER = 9
159+
EQUALS_NUMBER = 10
160+
NOT_EQUALS_NUMBER = 11
161+
LESS_THAN_NUMBER = 12
162+
LESS_THAN_OR_EQUAL_NUMBER = 13
163+
GREATER_THAN_NUMBER = 14
164+
GREATER_THAN_OR_EQUAL_NUMBER = 15
165+
IS_ONE_OF_HASHED = 16
166+
IS_NOT_ONE_OF_HASHED = 17
167+
BEFORE_DATETIME = 18
168+
AFTER_DATETIME = 19
169+
EQUALS_HASHED = 20
170+
NOT_EQUALS_HASHED = 21
171+
STARTS_WITH_ANY_OF_HASHED = 22
172+
NOT_STARTS_WITH_ANY_OF_HASHED = 23
173+
ENDS_WITH_ANY_OF_HASHED = 24
174+
NOT_ENDS_WITH_ANY_OF_HASHED = 25
175+
ARRAY_CONTAINS_ANY_OF_HASHED = 26
176+
ARRAY_NOT_CONTAINS_ANY_OF_HASHED = 27
177+
EQUALS = 28
178+
NOT_EQUALS = 29
179+
STARTS_WITH_ANY_OF = 30
180+
NOT_STARTS_WITH_ANY_OF = 31
181+
ENDS_WITH_ANY_OF = 32
182+
NOT_ENDS_WITH_ANY_OF = 33
183+
ARRAY_CONTAINS_ANY_OF = 34
184+
ARRAY_NOT_CONTAINS_ANY_OF = 35
185+
186+
187+
COMPARATOR_TEXTS = [
188+
'IS ONE OF', # IS_ONE_OF
189+
'IS NOT ONE OF', # IS_NOT_ONE_OF
190+
'CONTAINS ANY OF', # CONTAINS_ANY_OF
191+
'NOT CONTAINS ANY OF', # NOT_CONTAINS_ANY_OF
192+
'IS ONE OF', # IS_ONE_OF_SEMVER
193+
'IS NOT ONE OF', # IS_NOT_ONE_OF_SEMVER
194+
'<', # LESS_THAN_SEMVER
195+
'<=', # LESS_THAN_OR_EQUAL_SEMVER
196+
'>', # GREATER_THAN_SEMVER
197+
'>=', # GREATER_THAN_OR_EQUAL_SEMVER
198+
'=', # EQUALS_NUMBER
199+
'!=', # NOT_EQUALS_NUMBER
200+
'<', # LESS_THAN_NUMBER
201+
'<=', # LESS_THAN_OR_EQUAL_NUMBER
202+
'>', # GREATER_THAN_NUMBER
203+
'>=', # GREATER_THAN_OR_EQUAL_NUMBER
204+
'IS ONE OF', # IS_ONE_OF_HASHED
205+
'IS NOT ONE OF', # IS_NOT_ONE_OF_HASHED
206+
'BEFORE', # BEFORE_DATETIME
207+
'AFTER', # AFTER_DATETIME
208+
'EQUALS', # EQUALS_HASHED
209+
'NOT EQUALS', # NOT_EQUALS_HASHED
210+
'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF_HASHED
211+
'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF_HASHED
212+
'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF_HASHED
213+
'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF_HASHED
214+
'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF_HASHED
215+
'ARRAY NOT CONTAINS ANY OF', # ARRAY_NOT_CONTAINS_ANY_OF_HASHED
216+
'EQUALS', # EQUALS
217+
'NOT EQUALS', # NOT_EQUALS
218+
'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF
219+
'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF
220+
'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF
221+
'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF
222+
'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF
223+
'ARRAY NOT CONTAINS ANY OF' # ARRAY_NOT_CONTAINS_ANY_OF
224+
]
225+
COMPARISON_VALUES = [
226+
STRING_LIST_VALUE, # IS_ONE_OF
227+
STRING_LIST_VALUE, # IS_NOT_ONE_OF
228+
STRING_LIST_VALUE, # CONTAINS_ANY_OF
229+
STRING_LIST_VALUE, # NOT_CONTAINS_ANY_OF
230+
STRING_LIST_VALUE, # IS_ONE_OF_SEMVER
231+
STRING_LIST_VALUE, # IS_NOT_ONE_OF_SEMVER
232+
STRING_VALUE, # LESS_THAN_SEMVER
233+
STRING_VALUE, # LESS_THAN_OR_EQUAL_SEMVER
234+
STRING_VALUE, # GREATER_THAN_SEMVER
235+
STRING_VALUE, # GREATER_THAN_OR_EQUAL_SEMVER
236+
DOUBLE_VALUE, # EQUALS_NUMBER
237+
DOUBLE_VALUE, # NOT_EQUALS_NUMBER
238+
DOUBLE_VALUE, # LESS_THAN_NUMBER
239+
DOUBLE_VALUE, # LESS_THAN_OR_EQUAL_NUMBER
240+
DOUBLE_VALUE, # GREATER_THAN_NUMBER
241+
DOUBLE_VALUE, # GREATER_THAN_OR_EQUAL_NUMBER
242+
STRING_LIST_VALUE, # IS_ONE_OF_HASHED
243+
STRING_LIST_VALUE, # IS_NOT_ONE_OF_HASHED
244+
DOUBLE_VALUE, # BEFORE_DATETIME
245+
DOUBLE_VALUE, # AFTER_DATETIME
246+
STRING_VALUE, # EQUALS_HASHED
247+
STRING_VALUE, # NOT_EQUALS_HASHED
248+
STRING_LIST_VALUE, # STARTS_WITH_ANY_OF_HASHED
249+
STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF_HASHED
250+
STRING_LIST_VALUE, # ENDS_WITH_ANY_OF_HASHED
251+
STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF_HASHED
252+
STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF_HASHED
253+
STRING_LIST_VALUE, # ARRAY_NOT_CONTAINS_ANY_OF_HASHED
254+
STRING_VALUE, # EQUALS
255+
STRING_VALUE, # NOT_EQUALS
256+
STRING_LIST_VALUE, # STARTS_WITH_ANY_OF
257+
STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF
258+
STRING_LIST_VALUE, # ENDS_WITH_ANY_OF
259+
STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF
260+
STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF
261+
STRING_LIST_VALUE # ARRAY_NOT_CONTAINS_ANY_OF
262+
]
263+
SEGMENT_COMPARATOR_TEXTS = ['IS IN SEGMENT', 'IS NOT IN SEGMENT']
264+
PREREQUISITE_COMPARATOR_TEXTS = ['EQUALS', 'DOES NOT EQUAL']
265+
266+
267+
def extend_config_with_inline_salt_and_segment(config):
268+
"""
269+
Adds the inline salt and segment to the config.
270+
When using flag overrides, the original salt and segment indexes may become invalid. Therefore, we copy the
271+
object references to the locations where they are referenced and use these references instead of the indexes.
272+
"""
273+
salt = config.get(PREFERENCES, {}).get(SALT, '')
274+
segments = config.get(SEGMENTS, [])
275+
settings = config.get(FEATURE_FLAGS, {})
276+
for setting in settings.values():
277+
if not isinstance(setting, dict):
278+
continue
279+
280+
# add salt
281+
setting[INLINE_SALT] = salt
282+
283+
# add segment to the segment conditions
284+
targeting_rules = setting.get(TARGETING_RULES, [])
285+
for targeting_rule in targeting_rules:
286+
conditions = targeting_rule.get(CONDITIONS, [])
287+
for condition in conditions:
288+
289+
segment_condition = condition.get(SEGMENT_CONDITION)
290+
if segment_condition:
291+
segment_index = segment_condition.get(SEGMENT_INDEX)
292+
segment = segments[segment_index]
293+
segment_condition[INLINE_SEGMENT] = segment

0 commit comments

Comments
 (0)