Skip to content

Commit de457ac

Browse files
committed
Merge branch 'main' into feature/remote-eval
# Conflicts: # growthbook/common_types.py # growthbook/growthbook.py # growthbook/growthbook_client.py
2 parents df41f8b + 8c6e57c commit de457ac

13 files changed

Lines changed: 591 additions & 118 deletions

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "1.4.10"
2+
".": "2.1.5"
33
}

CHANGELOG.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,66 @@
11
# Changelog
22

3+
## [2.1.5](https://github.com/growthbook/growthbook-python/compare/v2.1.4...v2.1.5) (2026-03-06)
4+
5+
6+
### Bug Fixes
7+
8+
* Add optional timeout for PoolManager ([#91](https://github.com/growthbook/growthbook-python/issues/91)) ([2fe21f6](https://github.com/growthbook/growthbook-python/commit/2fe21f692189d7a37712d445b4545571ff2d3039))
9+
10+
## [2.1.4](https://github.com/growthbook/growthbook-python/compare/v2.1.3...v2.1.4) (2026-02-23)
11+
12+
13+
### Bug Fixes
14+
15+
* Fixes for process hanging and shutdown errors - Merge pull request [#103](https://github.com/growthbook/growthbook-python/issues/103) from growthbook/pr102 ([c89a385](https://github.com/growthbook/growthbook-python/commit/c89a385b0cd0b0c4a776b7b81fc1ab3d27e40738))
16+
* parsing data for SSE in GrowthbookClient ([d390223](https://github.com/growthbook/growthbook-python/commit/d390223c0035d65d91a930391d4731321f4c2f15))
17+
* prevent SSE thread from blocking process exit and suppressing shutdown errors ([bddfb82](https://github.com/growthbook/growthbook-python/commit/bddfb82fce6284d4edf48b6135b51b362f82eab9))
18+
19+
## [2.1.3](https://github.com/growthbook/growthbook-python/compare/v2.1.2...v2.1.3) (2026-02-05)
20+
21+
22+
### Features
23+
24+
* Supporting Dict Subclasses in Evaluation - Merge pull request [#99](https://github.com/growthbook/growthbook-python/issues/99) from growthbook/feat/isInstanceTypeCheck ([8ed4d4e](https://github.com/growthbook/growthbook-python/commit/8ed4d4e1aaf5b79408d60b16f856d66146600f91))
25+
* Replaced all type(x) is T checks with isinstance(x, T).
26+
* Updated getType, getPath, compare, and operator functions to use these new checks.
27+
28+
## [2.1.2](https://github.com/growthbook/growthbook-python/compare/v2.1.1...v2.1.2) (2026-01-29)
29+
30+
31+
### Bug Fixes
32+
33+
* Disabled features not being removed from cache ([#93](https://github.com/growthbook/growthbook-python/issues/93)) ([eac9717](https://github.com/growthbook/growthbook-python/commit/eac971782f7776ff4261cc4ef9b7894b5735eb9d))
34+
35+
## [2.1.1](https://github.com/growthbook/growthbook-python/compare/v2.1.0...v2.1.1) (2026-01-27)
36+
37+
38+
### Features
39+
40+
* Add support for case-insensitive membership operators: `$ini`, `$nini`, `$alli`
41+
- `$ini`: Case-insensitive version of `$in` operator
42+
- `$nini`: Case-insensitive version of `$nin` operator
43+
- `$alli`: Case-insensitive version of `$all` operator ([0e26f7d](https://github.com/growthbook/growthbook-python/commit/0e26f7d55e2b4b5908a9e3dd0921c1ea1fa49f97))
44+
45+
## [2.1.0](https://github.com/growthbook/growthbook-python/compare/v2.0.0...v2.1.0) (2026-01-22)
46+
47+
48+
### Features
49+
50+
* Adds support for `regexi` and `$notRegexi` - Case insensitive regex ([b9fce8a](https://github.com/growthbook/growthbook-python/commit/b9fce8ab2e7c91e38a0f2cb7b1d2446d564e650b))
51+
- Adds support for `$notRegex`
52+
53+
## [2.0.0](https://github.com/growthbook/growthbook-python/compare/v1.4.10...v2.0.0) (2026-01-14)
54+
55+
56+
### ⚠ BREAKING CHANGES
57+
58+
* Fixes for Async wrapper execution and other enhancements
59+
60+
### Bug Fixes
61+
62+
* Fixes for Async wrapper execution and other enhancements ([e6a0eaf](https://github.com/growthbook/growthbook-python/commit/e6a0eaff7dcc391819ad92eeb94a4fd3aac7bdda))
63+
364
## [1.4.10](https://github.com/growthbook/growthbook-python/compare/v1.4.9...v1.4.10) (2025-12-19)
465

566

growthbook/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818
)
1919

2020
# x-release-please-start-version
21-
__version__ = "1.4.10"
21+
__version__ = "2.1.5"
2222
# x-release-please-end

growthbook/common_types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,9 @@ class Options:
485485
remote_eval: bool = False
486486
global_attributes: Dict[str, Any] = field(default_factory=dict)
487487
forced_features: Dict[str, Any] = field(default_factory=dict)
488+
http_connect_timeout: Optional[int] = None
489+
http_read_timeout: Optional[int] = None
490+
488491

489492
@dataclass
490493
class GlobalContext:

growthbook/core.py

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -49,42 +49,48 @@ def isOperatorObject(obj: Any) -> bool:
4949
return False
5050
return True
5151

52-
def getType(attributeValue) -> str:
53-
t = type(attributeValue)
52+
def _is_numeric(v: Any) -> bool:
53+
return isinstance(v, (int, float)) and not isinstance(v, bool)
5454

55+
def getType(attributeValue) -> str:
5556
if attributeValue is None:
5657
return "null"
57-
if t is int or t is float:
58+
if isinstance(attributeValue, bool):
59+
return "boolean"
60+
if _is_numeric(attributeValue):
5861
return "number"
59-
if t is str:
62+
if isinstance(attributeValue, str):
6063
return "string"
61-
if t is list or t is set:
64+
if isinstance(attributeValue, (list, set)):
6265
return "array"
63-
if t is dict:
66+
if isinstance(attributeValue, dict):
6467
return "object"
65-
if t is bool:
66-
return "boolean"
6768
return "unknown"
6869

6970
def getPath(attributes, path):
7071
current = attributes
7172
for segment in path.split("."):
72-
if type(current) is dict and segment in current:
73+
if isinstance(current, dict) and segment in current:
7374
current = current[segment]
7475
else:
7576
return None
7677
return current
7778

78-
def evalConditionValue(conditionValue, attributeValue, savedGroups) -> bool:
79-
if type(conditionValue) is dict and isOperatorObject(conditionValue):
79+
def evalConditionValue(conditionValue, attributeValue, savedGroups, insensitive: bool = False) -> bool:
80+
if isinstance(conditionValue, dict) and isOperatorObject(conditionValue):
8081
for key, value in conditionValue.items():
8182
if not evalOperatorCondition(key, attributeValue, value, savedGroups):
8283
return False
8384
return True
85+
86+
# Simple equality comparison with optional case-insensitivity
87+
if insensitive and isinstance(conditionValue, str) and isinstance(attributeValue, str):
88+
return conditionValue.lower() == attributeValue.lower()
89+
8490
return bool(conditionValue == attributeValue)
8591

8692
def elemMatch(condition, attributeValue, savedGroups) -> bool:
87-
if not type(attributeValue) is list:
93+
if not isinstance(attributeValue, list):
8894
return False
8995

9096
for item in attributeValue:
@@ -98,13 +104,13 @@ def elemMatch(condition, attributeValue, savedGroups) -> bool:
98104
return False
99105

100106
def compare(val1, val2) -> int:
101-
if (type(val1) is int or type(val1) is float) and not (type(val2) is int or type(val2) is float):
107+
if _is_numeric(val1) and not _is_numeric(val2):
102108
if (val2 is None):
103109
val2 = 0
104110
else:
105111
val2 = float(val2)
106112

107-
if (type(val2) is int or type(val2) is float) and not (type(val1) is int or type(val1) is float):
113+
if _is_numeric(val2) and not _is_numeric(val1):
108114
if (val1 is None):
109115
val1 = 0
110116
else:
@@ -160,13 +166,13 @@ def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups)
160166
elif operator == "$vgte":
161167
return paddedVersionString(attributeValue) >= paddedVersionString(conditionValue)
162168
elif operator == "$inGroup":
163-
if not type(conditionValue) is str:
169+
if not isinstance(conditionValue, str):
164170
return False
165171
if not conditionValue in savedGroups:
166172
return False
167173
return isIn(savedGroups[conditionValue] or [], attributeValue)
168174
elif operator == "$notInGroup":
169-
if not type(conditionValue) is str:
175+
if not isinstance(conditionValue, str):
170176
return False
171177
if not conditionValue in savedGroups:
172178
return True
@@ -177,31 +183,54 @@ def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups)
177183
return bool(r.search(attributeValue))
178184
except Exception:
179185
return False
186+
elif operator == "$regexi":
187+
try:
188+
r = re.compile(conditionValue, re.IGNORECASE)
189+
return bool(r.search(attributeValue))
190+
except Exception:
191+
return False
192+
elif operator == "$notRegex":
193+
try:
194+
r = re.compile(conditionValue)
195+
return not bool(r.search(attributeValue))
196+
except Exception:
197+
return False
198+
elif operator == "$notRegexi":
199+
try:
200+
r = re.compile(conditionValue, re.IGNORECASE)
201+
return not bool(r.search(attributeValue))
202+
except Exception:
203+
return False
180204
elif operator == "$in":
181-
if not type(conditionValue) is list:
205+
if not isinstance(conditionValue, list):
182206
return False
183207
return isIn(conditionValue, attributeValue)
184208
elif operator == "$nin":
185-
if not type(conditionValue) is list:
209+
if not isinstance(conditionValue, list):
186210
return False
187211
return not isIn(conditionValue, attributeValue)
212+
elif operator == "$ini":
213+
if not isinstance(conditionValue, list):
214+
return False
215+
return isIn(conditionValue, attributeValue, insensitive=True)
216+
elif operator == "$nini":
217+
if not isinstance(conditionValue, list):
218+
return False
219+
return not isIn(conditionValue, attributeValue, insensitive=True)
188220
elif operator == "$elemMatch":
189221
return elemMatch(conditionValue, attributeValue, savedGroups)
190222
elif operator == "$size":
191-
if not (type(attributeValue) is list):
223+
if not isinstance(attributeValue, list):
192224
return False
193225
return evalConditionValue(conditionValue, len(attributeValue), savedGroups)
194226
elif operator == "$all":
195-
if not (type(attributeValue) is list):
227+
if not isinstance(conditionValue, list):
196228
return False
197-
for cond in conditionValue:
198-
passing = False
199-
for attr in attributeValue:
200-
if evalConditionValue(cond, attr, savedGroups):
201-
passing = True
202-
if not passing:
203-
return False
204-
return True
229+
return isInAll(conditionValue, attributeValue, savedGroups, insensitive=False)
230+
elif operator == "$alli":
231+
if not isinstance(conditionValue, list):
232+
return False
233+
return isInAll(conditionValue, attributeValue, savedGroups, insensitive=True)
205234
elif operator == "$exists":
206235
if not conditionValue:
207236
return attributeValue is None
@@ -214,10 +243,10 @@ def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups)
214243

215244
def paddedVersionString(input) -> str:
216245
# If input is a number, convert to a string
217-
if type(input) is int or type(input) is float:
246+
if _is_numeric(input):
218247
input = str(input)
219248

220-
if not input or type(input) is not str:
249+
if not input or not isinstance(input, str):
221250
input = "0"
222251

223252
# Remove build info and leading `v` if any
@@ -235,11 +264,41 @@ def paddedVersionString(input) -> str:
235264
return "-".join([v.rjust(5, " ") if re.match(r"^[0-9]+$", v) else v for v in parts])
236265

237266

238-
def isIn(conditionValue, attributeValue) -> bool:
239-
if type(attributeValue) is list:
267+
def isIn(conditionValue, attributeValue, insensitive: bool = False) -> bool:
268+
if insensitive:
269+
# Helper function to case-fold values (lowercase for strings)
270+
def case_fold(val):
271+
return val.lower() if isinstance(val, str) else val
272+
273+
# Do an intersection if attribute is an array (insensitive)
274+
if isinstance(attributeValue, list):
275+
return any(
276+
case_fold(el) == case_fold(exp)
277+
for el in attributeValue
278+
for exp in conditionValue
279+
)
280+
return any(case_fold(attributeValue) == case_fold(exp) for exp in conditionValue)
281+
282+
# Case-sensitive behavior (original)
283+
if isinstance(attributeValue, list):
240284
return bool(set(conditionValue) & set(attributeValue))
241285
return attributeValue in conditionValue
242286

287+
def isInAll(conditionValue, attributeValue, savedGroups, insensitive: bool = False) -> bool:
288+
"""Check if attributeValue (array) contains all elements in conditionValue"""
289+
if not isinstance(attributeValue, list):
290+
return False
291+
292+
for cond in conditionValue:
293+
passing = False
294+
for attr in attributeValue:
295+
if evalConditionValue(cond, attr, savedGroups, insensitive):
296+
passing = True
297+
break
298+
if not passing:
299+
return False
300+
return True
301+
243302
def _getOrigHashValue(
244303
eval_context: EvaluationContext,
245304
attr: Optional[str] = "id",

0 commit comments

Comments
 (0)