11"""Localhost client mocked components."""
22
3+ import itertools
34import logging
45import re
5- from splitio .models .splits import from_raw
6+
7+ from six import raise_from
8+ import yaml
9+
10+ from splitio .models import splits
611from splitio .storage import ImpressionStorage , EventStorage , TelemetryStorage
712from splitio .tasks import BaseSynchronizationTask
813from splitio .tasks .util import asynctask
914
10- _COMMENT_LINE_RE = re .compile ('^#.*$' )
11- _DEFINITION_LINE_RE = re .compile ('^(?<![^#])(?P<feature>[\w_-]+)\s+(?P<treatment>[\w_-]+)$' )
15+ _LEGACY_COMMENT_LINE_RE = re .compile (r '^#.*$' )
16+ _LEGACY_DEFINITION_LINE_RE = re .compile (r '^(?<![^#])(?P<feature>[\w_-]+)\s+(?P<treatment>[\w_-]+)$' )
1217
1318
1419_LOGGER = logging .getLogger (__name__ )
@@ -69,7 +74,7 @@ def pop_gauges(self, *_, **__): #pylint: disable=arguments-differ
6974class LocalhostSplitSynchronizationTask (BaseSynchronizationTask ):
7075 """Split synchronization task that periodically checks the file and updated the splits."""
7176
72- def __init__ (self , filename , storage , ready_event ):
77+ def __init__ (self , filename , storage , period , ready_event ):
7378 """
7479 Class constructor.
7580
@@ -83,22 +88,23 @@ def __init__(self, filename, storage, ready_event):
8388 self ._filename = filename
8489 self ._ready_event = ready_event
8590 self ._storage = storage
86- self ._task = asynctask .AsyncTask (self ._update_splits , 5 , self ._on_start )
91+ self ._period = period
92+ self ._task = asynctask .AsyncTask (self ._update_splits , period , self ._on_start )
8793
8894 def _on_start (self ):
8995 """Sync splits and set event if successful."""
9096 self ._update_splits ()
9197 self ._ready_event .set ()
9298
9399 @staticmethod
94- def _make_all_keys_based_split (split_name , treatment ):
100+ def _make_split (split_name , conditions , configs = None ):
95101 """
96102 Make a split with a single all_keys matcher.
97103
98104 :param split_name: Name of the split.
99105 :type split_name: str.
100106 """
101- return from_raw ({
107+ return splits . from_raw ({
102108 'changeNumber' : 123 ,
103109 'trafficTypeName' : 'user' ,
104110 'name' : split_name ,
@@ -107,30 +113,55 @@ def _make_all_keys_based_split(split_name, treatment):
107113 'seed' : 321654 ,
108114 'status' : 'ACTIVE' ,
109115 'killed' : False ,
110- 'defaultTreatment' : treatment ,
116+ 'defaultTreatment' : 'control' ,
111117 'algo' : 2 ,
112- 'conditions' : [
113- {
114- 'partitions' : [
115- {'treatment' : treatment , 'size' : 100 }
116- ],
117- 'contitionType' : 'WHITELIST' ,
118- 'label' : 'some_other_label' ,
119- 'matcherGroup' : {
120- 'matchers' : [
121- {
122- 'matcherType' : 'ALL_KEYS' ,
123- 'negate' : False ,
124- }
125- ],
126- 'combiner' : 'AND'
127- }
128- }
129- ]
118+ 'conditions' : conditions ,
119+ 'configurations' : configs
130120 })
131121
122+ @staticmethod
123+ def _make_all_keys_condition (treatment ):
124+ return {
125+ 'partitions' : [
126+ {'treatment' : treatment , 'size' : 100 }
127+ ],
128+ 'conditionType' : 'WHITELIST' ,
129+ 'label' : 'some_other_label' ,
130+ 'matcherGroup' : {
131+ 'matchers' : [
132+ {
133+ 'matcherType' : 'ALL_KEYS' ,
134+ 'negate' : False ,
135+ }
136+ ],
137+ 'combiner' : 'AND'
138+ }
139+ }
140+
141+ @staticmethod
142+ def _make_whitelist_condition (whitelist , treatment ):
143+ return {
144+ 'partitions' : [
145+ {'treatment' : treatment , 'size' : 100 }
146+ ],
147+ 'conditionType' : 'WHITELIST' ,
148+ 'label' : 'some_other_label' ,
149+ 'matcherGroup' : {
150+ 'matchers' : [
151+ {
152+ 'matcherType' : 'WHITELIST' ,
153+ 'negate' : False ,
154+ 'whitelistMatcherData' : {
155+ 'whitelist' : whitelist
156+ }
157+ }
158+ ],
159+ 'combiner' : 'AND'
160+ }
161+ }
162+
132163 @classmethod
133- def _read_splits_from_file (cls , filename ):
164+ def _read_splits_from_legacy_file (cls , filename ):
134165 """
135166 Parse a splits file and return a populated storage.
136167
@@ -140,53 +171,89 @@ def _read_splits_from_file(cls, filename):
140171 :return: Storage populataed with splits ready to be evaluated.
141172 :rtype: InMemorySplitStorage
142173 """
143- splits = {}
174+ to_return = {}
144175 try :
145176 with open (filename , 'r' ) as flo :
146177 for line in flo :
147- if line .strip () == '' :
148- continue
149-
150- comment_match = _COMMENT_LINE_RE .match (line )
151- if comment_match :
178+ if line .strip () == '' or _LEGACY_COMMENT_LINE_RE .match (line ):
152179 continue
153180
154- definition_match = _DEFINITION_LINE_RE .match (line )
155- if definition_match :
156- splits [definition_match .group ('feature' )] = cls ._make_all_keys_based_split (
157- definition_match .group ('feature' ),
158- definition_match .group ('treatment' )
181+ definition_match = _LEGACY_DEFINITION_LINE_RE .match (line )
182+ if not definition_match :
183+ _LOGGER .warning (
184+ 'Invalid line on localhost environment split '
185+ 'definition. Line = %s' ,
186+ line
159187 )
160188 continue
161189
162- _LOGGER .warning (
163- 'Invalid line on localhost environment split '
164- 'definition. Line = %s' ,
165- line
166- )
167- return splits
168- except IOError as e :
169- raise ValueError ("Error parsing split file" )
170- # TODO: ver raise from!
171- # raise_from(ValueError(
172- # 'There was a problem with '
173- # 'the splits definition file "{}"'.format(filename)),
174- # e
175- # )
190+ cond = cls ._make_all_keys_condition (definition_match .group ('treatment' ))
191+ splt = cls ._make_split (definition_match .group ('feature' ), [cond ])
192+ to_return [splt .name ] = splt
193+ return to_return
176194
195+ except IOError as exc :
196+ raise_from (
197+ ValueError ("Error parsing file %s. Make sure it's readable." % filename ),
198+ exc
199+ )
200+
201+ @classmethod
202+ def _read_splits_from_yaml_file (cls , filename ):
203+ """
204+ Parse a splits file and return a populated storage.
205+
206+ :param filename: Path of the file containing mocked splits & treatments.
207+ :type filename: str.
208+
209+ :return: Storage populataed with splits ready to be evaluated.
210+ :rtype: InMemorySplitStorage
211+ """
212+ try :
213+ with open (filename , 'r' ) as flo :
214+ parsed = yaml .load (flo .read (), Loader = yaml .FullLoader )
215+
216+ grouped_by_feature_name = itertools .groupby (
217+ sorted (parsed , key = lambda i : next (iter (i .keys ()))),
218+ lambda i : next (iter (i .keys ())))
219+
220+ to_return = {}
221+ for (split_name , statements ) in grouped_by_feature_name :
222+ configs = {}
223+ whitelist = []
224+ all_keys = []
225+ for statement in statements :
226+ data = next (iter (statement .values ())) # grab the first (and only) value.
227+ if 'keys' in data :
228+ keys = data ['keys' ] if isinstance (data ['keys' ], list ) else [data ['keys' ]]
229+ whitelist .append (cls ._make_whitelist_condition (keys , data ['treatment' ]))
230+ else :
231+ all_keys .append (cls ._make_all_keys_condition (data ['treatment' ]))
232+ if 'config' in data :
233+ configs [data ['treatment' ]] = data ['config' ]
234+ to_return [split_name ] = cls ._make_split (split_name , whitelist + all_keys , configs )
235+ return to_return
236+
237+ except IOError as exc :
238+ raise_from (
239+ ValueError ("Error parsing file %s. Make sure it's readable." % filename ),
240+ exc
241+ )
177242
178243 def _update_splits (self ):
179244 """Update splits in storage."""
180245 _LOGGER .info ('Synchronizing splits now.' )
181- splits = self ._read_splits_from_file (self ._filename )
182- to_delete = [name for name in self ._storage .get_split_names () if name not in splits .keys ()]
183- for split in splits .values ():
246+ if self ._filename .split ('.' )[- 1 ].lower () in ('yaml' , 'yml' ):
247+ fetched = self ._read_splits_from_yaml_file (self ._filename )
248+ else :
249+ fetched = self ._read_splits_from_legacy_file (self ._filename )
250+ to_delete = [name for name in self ._storage .get_split_names () if name not in fetched .keys ()]
251+ for split in fetched .values ():
184252 self ._storage .put (split )
185253
186254 for split in to_delete :
187255 self ._storage .remove (split )
188256
189-
190257 def is_running (self ):
191258 """Return whether the task is running."""
192259 return self ._task .running
@@ -195,13 +262,11 @@ def start(self):
195262 """Start split synchronization."""
196263 self ._task .start ()
197264
198- def stop (self , stop_event ):
265+ def stop (self , event = None ):
199266 """
200267 Stop task.
201268
202269 :param stop_event: Event top set when the task finishes.
203270 :type stop_event: threading.Event.
204271 """
205- self ._task .stop (stop_event )
206-
207-
272+ self ._task .stop (event )
0 commit comments