Skip to content

Commit cc2bb45

Browse files
authored
Merge pull request #99 from splitio/improvement/inputSanitization
[SDKS: 29]: Python Input Validation
2 parents 42e4d76 + 0852444 commit cc2bb45

File tree

10 files changed

+1470
-83
lines changed

10 files changed

+1470
-83
lines changed

.github/pull_request_template.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Python SDK
2+
3+
## Tickets covered:
4+
* [SDKS-{TICKET}](https://splitio.atlassian.net/browse/SDKS-{TICKET})
5+
6+
## What did you accomplish?
7+
* Bullet 1
8+
* Bullet 2
9+
10+
## How to test new changes?
11+
*
12+
13+
## Extra Notes
14+
* Bullet 1
15+
* Bullet 2

splitio/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
unicode_literals
33

44
from .factories import get_factory # noqa
5-
from .clients import Key # noqa
5+
from .key import Key # noqa
66
from .version import __version__ # noqa
77

88
__all__ = ('api', 'brokers', 'cache', 'clients', 'matchers', 'segments',

splitio/clients.py

Lines changed: 141 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""A module for Split.io SDK API clients"""
1+
"""A module for Split.io SDK API clients."""
22
from __future__ import absolute_import, division, print_function, \
33
unicode_literals
44

@@ -10,19 +10,27 @@
1010
from splitio.metrics import SDK_GET_TREATMENT
1111
from splitio.splits import ConditionType
1212
from splitio.events import Event
13-
14-
15-
class Key(object):
16-
def __init__(self, matching_key, bucketing_key):
17-
"""Bucketing Key implementation"""
18-
self.matching_key = matching_key
19-
self.bucketing_key = bucketing_key
13+
from . import input_validator
14+
from splitio.key import Key
2015

2116

2217
class Client(object):
18+
"""Client class that uses a broker for storage."""
19+
2320
def __init__(self, broker, labels_enabled=True, impression_listener=None):
24-
"""Basic interface of a Client. Specific implementations need to override the
25-
get_split_fetcher method (and optionally the get_splitter method).
21+
"""
22+
Construct a Client instance.
23+
24+
:param broker: Broker that accepts/retrieves splits, segments, events, metrics & impressions
25+
:type broker: BaseBroker
26+
27+
:param labels_enabled: Whether to store labels on impressions
28+
:type labels_enabled: bool
29+
30+
:param impression_listener: impression listener implementation
31+
:type impression_listener: ImpressionListener
32+
33+
:rtype: Client
2634
"""
2735
self._logger = logging.getLogger(self.__class__.__name__)
2836
self._splitter = Splitter()
@@ -31,21 +39,10 @@ def __init__(self, broker, labels_enabled=True, impression_listener=None):
3139
self._destroyed = False
3240
self._impression_listener = impression_listener
3341

34-
@staticmethod
35-
def _get_keys(key):
36-
"""
37-
"""
38-
if isinstance(key, Key):
39-
matching_key = key.matching_key
40-
bucketing_key = key.bucketing_key
41-
else:
42-
matching_key = str(key)
43-
bucketing_key = None
44-
return matching_key, bucketing_key
45-
4642
def destroy(self):
4743
"""
4844
Disable the split-client and free all allocated resources.
45+
4946
Only applicable when using in-memory operation mode.
5047
"""
5148
self._destroyed = True
@@ -64,9 +61,11 @@ def _handle_custom_impression(self, impression, attributes):
6461

6562
def get_treatment(self, key, feature, attributes=None):
6663
"""
67-
Get the treatment for a feature and key, with an optional dictionary of attributes. This
68-
method never raises an exception. If there's a problem, the appropriate log message will
69-
be generated and the method will return the CONTROL treatment.
64+
Get the treatment for a feature and key, with an optional dictionary of attributes.
65+
66+
This method never raises an exception. If there's a problem, the appropriate log message
67+
will be generated and the method will return the CONTROL treatment.
68+
7069
:param key: The key for which to get the treatment
7170
:type key: str
7271
:param feature: The name of the feature for which to get the treatment
@@ -77,14 +76,19 @@ def get_treatment(self, key, feature, attributes=None):
7776
:rtype: str
7877
"""
7978
if self._destroyed:
80-
return CONTROL
81-
82-
if key is None or feature is None:
79+
self._logger.warning("Client has already been destroyed, returning CONTROL")
8380
return CONTROL
8481

8582
start = int(round(time.time() * 1000))
8683

87-
matching_key, bucketing_key = self._get_keys(key)
84+
matching_key, bucketing_key = input_validator.validate_key(key)
85+
feature = input_validator.validate_feature_name(feature)
86+
87+
if (matching_key is None and bucketing_key is None) or feature is None:
88+
impression = self._build_impression(matching_key, feature, CONTROL, Label.EXCEPTION,
89+
0, bucketing_key, start)
90+
self._record_stats(impression, start, SDK_GET_TREATMENT)
91+
return CONTROL
8892

8993
try:
9094
label = ''
@@ -123,49 +127,79 @@ def get_treatment(self, key, feature, attributes=None):
123127
self._handle_custom_impression(impression, attributes)
124128

125129
return _treatment
126-
except:
130+
except Exception: # pylint: disable=broad-except
127131
self._logger.exception('Exception caught getting treatment for feature')
128132

129133
try:
130-
impression = self._build_impression(matching_key, feature, CONTROL, Label.EXCEPTION,
131-
self._broker.get_change_number(), bucketing_key, start)
134+
impression = self._build_impression(
135+
matching_key,
136+
feature,
137+
CONTROL,
138+
Label.EXCEPTION,
139+
self._broker.get_change_number(), bucketing_key, start
140+
)
132141
self._record_stats(impression, start, SDK_GET_TREATMENT)
142+
except Exception: # pylint: disable=broad-except
143+
self._logger.exception(
144+
'Exception reporting impression into get_treatment exception block'
145+
)
133146

134147
self._handle_custom_impression(impression, attributes)
135-
except:
136-
self._logger.exception('Exception reporting impression into get_treatment exception block')
137148

138149
return CONTROL
139150

140-
def _build_impression(self, matching_key, feature_name, treatment, label, change_number, bucketing_key, time):
151+
def _build_impression(
152+
self, matching_key, feature_name, treatment, label,
153+
change_number, bucketing_key, imp_time
154+
):
155+
"""
156+
Build an impression.
141157
158+
TODO: REFACTOR THIS!
159+
"""
142160
if not self._labels_enabled:
143161
label = None
144162

145163
return Impression(
146164
matching_key=matching_key, feature_name=feature_name,
147165
treatment=treatment, label=label, change_number=change_number,
148-
bucketing_key=bucketing_key, time=time
166+
bucketing_key=bucketing_key, time=imp_time
149167
)
150168

151169
def _record_stats(self, impression, start, operation):
170+
"""
171+
Record impression and metrics.
172+
173+
:param impression: Generated impression
174+
:type impression: Impression
175+
176+
:param start: timestamp when get_treatment was called
177+
:type start: int
178+
179+
:param operation: operation performed.
180+
:type operation: string
181+
"""
152182
try:
153183
end = int(round(time.time() * 1000))
154184
self._broker.log_impression(impression)
155185
self._broker.log_operation_time(operation, end - start)
156-
except:
186+
except Exception: # pylint: disable=broad-except
157187
self._logger.exception('Exception caught recording impressions and metrics')
158188

159189
def _get_treatment_for_split(self, split, matching_key, bucketing_key, attributes=None):
160190
"""
161-
Internal method to get the treatment for a given Split and optional attributes. This
162-
method might raise exceptions and should never be used directly.
191+
Evaluate the user submitted data againt a feature and return the resulting treatment.
192+
193+
This method might raise exceptions and should never be used directly.
163194
:param split: The split for which to get the treatment
164195
:type split: Split
196+
165197
:param key: The key for which to get the treatment
166198
:type key: str
199+
167200
:param attributes: An optional dictionary of attributes
168201
:type attributes: dict
202+
169203
:return: The treatment for the key and split
170204
:rtype: str
171205
"""
@@ -207,32 +241,85 @@ def _get_treatment_for_split(self, split, matching_key, bucketing_key, attribute
207241

208242
def track(self, key, traffic_type, event_type, value=None):
209243
"""
210-
Track an event
244+
Track an event.
245+
246+
:param key: user key associated to the event
247+
:type key: string
248+
249+
:param traffic_type: traffic type name
250+
:type traffic_type: string
251+
252+
:param event_type: event type name
253+
:type event_type: string
254+
255+
:param value: (Optional) value associated to the event
256+
:type value: Number
257+
258+
:rtype: bool
211259
"""
212-
e = Event(
260+
key = input_validator.validate_track_key(key)
261+
event_type = input_validator.validate_event_type(event_type)
262+
traffic_type = input_validator.validate_traffic_type(traffic_type)
263+
value = input_validator.validate_value(value)
264+
265+
if key is None or event_type is None or traffic_type is None or value is None:
266+
return False
267+
268+
event = Event(
213269
key=key,
214270
trafficTypeName=traffic_type,
215271
eventTypeId=event_type,
216272
value=value,
217273
timestamp=int(time.time()*1000)
218274
)
219-
return self._broker.get_events_log().log_event(e)
275+
return self._broker.get_events_log().log_event(event)
220276

221277

222278
class MatcherClient(Client):
223279
"""
280+
Client to be used by matchers such as "Dependency Matcher".
281+
282+
TODO: Refactor This!
224283
"""
225-
def __init__(self, broker, splitter, logger):
284+
285+
def __init__(self, broker, splitter, logger): # pylint: disable=super-init-not-called
286+
"""
287+
Construct a MatcherClient instance.
288+
289+
:param broker: Broker where splits & segments will be fetched.
290+
:type broker: BaseBroker
291+
292+
:param splitter: splitter
293+
:type splitter: Splitter
294+
295+
:param logger: logger object
296+
:type logger: logging.Logger
297+
"""
226298
self._broker = broker
227299
self._splitter = splitter
228300
self._logger = logger
229301

230302
def get_treatment(self, key, feature, attributes=None):
231303
"""
304+
Evaluate a feature and return the appropriate traetment.
305+
306+
Will not generate impressions nor metrics
307+
308+
:param key: user key
309+
:type key: mixed
310+
311+
:param feature: feature name
312+
:type feature: str
313+
314+
:param attributes: (Optional) attributes associated with the user key
315+
:type attributes: dict
232316
"""
233-
if key is None or feature is None: return CONTROL
317+
matching_key, bucketing_key = input_validator.validate_key(key)
318+
feature = input_validator.validate_feature_name(feature)
319+
320+
if (matching_key is None and bucketing_key is None) or feature is None:
321+
return CONTROL
234322

235-
matching_key, bucketing_key = self._get_keys(key)
236323
try:
237324
# Fetching Split definition
238325
split = self._broker.fetch_feature(feature)
@@ -244,7 +331,8 @@ def get_treatment(self, key, feature, attributes=None):
244331
)
245332
return CONTROL
246333

247-
if split.killed: return split.default_treatment
334+
if split.killed:
335+
return split.default_treatment
248336

249337
treatment, _ = self._get_treatment_for_split(
250338
split,
@@ -253,8 +341,12 @@ def get_treatment(self, key, feature, attributes=None):
253341
attributes
254342
)
255343

256-
if treatment is None: return split.default_treatment
344+
if treatment is None:
345+
return split.default_treatment
346+
257347
return treatment
258-
except:
259-
self._logger.exception('Exception caught retrieving dependent feature. Returning CONTROL')
348+
except Exception: # pylint: disable=broad-except
349+
self._logger.exception(
350+
'Exception caught retrieving dependent feature. Returning CONTROL'
351+
)
260352
return CONTROL

0 commit comments

Comments
 (0)