1
- # pylint: disable=invalid-name, too-few-public-methods
1
+ # pylint: disable=invalid-name, too-few-public-methods, use-a-generator
2
+ from typing import Optional , Union
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from dateutil .parser import parse , ParserError
6
+ import semver
2
7
from UnleashClient .utils import LOGGER , get_identifier
3
8
4
9
10
+ class ConstraintOperators (Enum ):
11
+ # Logical operators
12
+ IN = "IN"
13
+ NOT_IN = "NOT_IN"
14
+
15
+ # String operators
16
+ STR_ENDS_WITH = "STR_ENDS_WITH"
17
+ STR_STARTS_WITH = "STR_STARTS_WITH"
18
+ STR_CONTAINS = "STR_CONTAINS"
19
+
20
+ # Numeric oeprators
21
+ NUM_EQ = "NUM_EQ"
22
+ NUM_GT = "NUM_GT"
23
+ NUM_GTE = "NUM_GTE"
24
+ NUM_LT = "NUM_LT"
25
+ NUM_LTE = "NUM_LTE"
26
+
27
+ # Date operators
28
+ DATE_AFTER = "DATE_AFTER"
29
+ DATE_BEFORE = "DATE_BEFORE"
30
+
31
+ # Semver operators
32
+ SEMVER_EQ = "SEMVER_EQ"
33
+ SEMVER_GT = "SEMVER_GT"
34
+ SEMVER_LT = "SEMVER_LT"
35
+
36
+
5
37
class Constraint :
6
38
def __init__ (self , constraint_dict : dict ) -> None :
7
39
"""
8
40
Represents a constraint on a strategy
9
41
10
42
:param constraint_dict: From the strategy document.
11
43
"""
12
- self .context_name = constraint_dict ['contextName' ]
13
- self .operator = constraint_dict ['operator' ]
14
- self .values = constraint_dict ['values' ]
44
+ self .context_name : str = constraint_dict ['contextName' ]
45
+ self .operator : ConstraintOperators = ConstraintOperators (constraint_dict ['operator' ].upper ())
46
+ self .values = constraint_dict ['values' ] if 'values' in constraint_dict .keys () else []
47
+ self .value = constraint_dict ['value' ] if 'value' in constraint_dict .keys () else None
48
+
49
+ self .case_insensitive = constraint_dict ['caseInsensitive' ] if 'caseInsensitive' in constraint_dict .keys () else False
50
+ self .inverted = constraint_dict ['inverted' ] if 'inverted' in constraint_dict .keys () else False
51
+
52
+
53
+ # Methods to handle each operator type.
54
+ def check_list_operators (self , context_value : str ) -> bool :
55
+ return_value = False
56
+
57
+ if self .operator == ConstraintOperators .IN :
58
+ return_value = context_value in self .values
59
+ elif self .operator == ConstraintOperators .NOT_IN :
60
+ return_value = context_value not in self .values
61
+
62
+ return return_value
63
+
64
+ def check_string_operators (self , context_value : str ) -> bool :
65
+ if self .case_insensitive :
66
+ normalized_values = [x .upper () for x in self .values ]
67
+ normalized_context_value = context_value .upper ()
68
+ else :
69
+ normalized_values = self .values
70
+ normalized_context_value = context_value
71
+
72
+ return_value = False
73
+
74
+ if self .operator == ConstraintOperators .STR_CONTAINS :
75
+ return_value = any ([x in normalized_context_value for x in normalized_values ])
76
+ elif self .operator == ConstraintOperators .STR_ENDS_WITH :
77
+ return_value = any ([normalized_context_value .endswith (x ) for x in normalized_values ])
78
+ elif self .operator == ConstraintOperators .STR_STARTS_WITH :
79
+ return_value = any ([normalized_context_value .startswith (x ) for x in normalized_values ])
80
+
81
+ return return_value
82
+
83
+ def check_numeric_operators (self , context_value : Union [float , int ]) -> bool :
84
+ return_value = False
85
+ parsed_value = float (self .value )
86
+
87
+ if self .operator == ConstraintOperators .NUM_EQ :
88
+ return_value = context_value == parsed_value
89
+ elif self .operator == ConstraintOperators .NUM_GT :
90
+ return_value = context_value > parsed_value
91
+ elif self .operator == ConstraintOperators .NUM_GTE :
92
+ return_value = context_value >= parsed_value
93
+ elif self .operator == ConstraintOperators .NUM_LT :
94
+ return_value = context_value < parsed_value
95
+ elif self .operator == ConstraintOperators .NUM_LTE :
96
+ return_value = context_value <= parsed_value
97
+
98
+ return return_value
99
+
100
+
101
+ def check_date_operators (self , context_value : datetime ) -> bool :
102
+ return_value = False
103
+ parsing_exception = False
104
+
105
+ try :
106
+ parsed_date = parse (self .value , ignoretz = True )
107
+ except ParserError :
108
+ LOGGER .error (f"Unable to parse date: { self .value } " )
109
+ parsing_exception = True
110
+
111
+ if not parsing_exception :
112
+ if self .operator == ConstraintOperators .DATE_AFTER :
113
+ return_value = context_value > parsed_date
114
+ elif self .operator == ConstraintOperators .DATE_BEFORE :
115
+ return_value = context_value < parsed_date
116
+
117
+ return return_value
118
+
119
+
120
+ def check_semver_operators (self , context_value : str ) -> bool :
121
+ return_value = False
122
+ parsing_exception = False
123
+ target_version : Optional [semver .VersionInfo ] = None
124
+ context_version : Optional [semver .VersionInfo ] = None
125
+
126
+ try :
127
+ target_version = semver .VersionInfo .parse (self .value )
128
+ except ValueError :
129
+ LOGGER .error (f"Unable to parse server semver: { self .value } " )
130
+ parsing_exception = True
131
+
132
+ try :
133
+ context_version = semver .VersionInfo .parse (context_value )
134
+ except ValueError :
135
+ LOGGER .error (f"Unable to parse context semver: { context_value } " )
136
+ parsing_exception = True
137
+
138
+ if not parsing_exception :
139
+ if self .operator == ConstraintOperators .SEMVER_EQ :
140
+ return_value = context_version == target_version
141
+ elif self .operator == ConstraintOperators .SEMVER_GT :
142
+ return_value = context_version > target_version
143
+ elif self .operator == ConstraintOperators .SEMVER_LT :
144
+ return_value = context_version < target_version
145
+
146
+ return return_value
147
+
15
148
16
149
def apply (self , context : dict = None ) -> bool :
17
150
"""
@@ -23,14 +156,25 @@ def apply(self, context: dict = None) -> bool:
23
156
constraint_check = False
24
157
25
158
try :
26
- value = get_identifier (self .context_name , context )
159
+ context_value = get_identifier (self .context_name , context )
160
+
161
+ # Set currentTime if not specified
162
+ if self .context_name == "currentTime" and not context_value :
163
+ context_value = datetime .now ()
164
+
165
+ if context_value is not None :
166
+ if self .operator in [ConstraintOperators .IN , ConstraintOperators .NOT_IN ]:
167
+ constraint_check = self .check_list_operators (context_value = context_value )
168
+ elif self .operator in [ConstraintOperators .STR_CONTAINS , ConstraintOperators .STR_ENDS_WITH , ConstraintOperators .STR_STARTS_WITH ]:
169
+ constraint_check = self .check_string_operators (context_value = context_value )
170
+ elif self .operator in [ConstraintOperators .NUM_EQ , ConstraintOperators .NUM_GT , ConstraintOperators .NUM_GTE , ConstraintOperators .NUM_LT , ConstraintOperators .NUM_LTE ]:
171
+ constraint_check = self .check_numeric_operators (context_value = context_value )
172
+ elif self .operator in [ConstraintOperators .DATE_AFTER , ConstraintOperators .DATE_BEFORE ]:
173
+ constraint_check = self .check_date_operators (context_value = context_value )
174
+ elif self .operator in [ConstraintOperators .SEMVER_EQ , ConstraintOperators .SEMVER_GT , ConstraintOperators .SEMVER_LT ]:
175
+ constraint_check = self .check_semver_operators (context_value = context_value )
27
176
28
- if value :
29
- if self .operator .upper () == "IN" :
30
- constraint_check = value in self .values
31
- elif self .operator .upper () == "NOT_IN" :
32
- constraint_check = value not in self .values
33
177
except Exception as excep : # pylint: disable=broad-except
34
178
LOGGER .info ("Could not evaluate context %s! Error: %s" , self .context_name , excep )
35
179
36
- return constraint_check
180
+ return not constraint_check if self . inverted else constraint_check
0 commit comments