diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..75357c20 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +# Python SDK + +## Tickets covered: +* [SDKS-{TICKET}](https://splitio.atlassian.net/browse/SDKS-{TICKET}) + +## What did you accomplish? +* Bullet 1 +* Bullet 2 + +## How to test new changes? +* + +## Extra Notes +* Bullet 1 +* Bullet 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4834eed4..72f8848f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ var/ *.egg-info/ .installed.cfg *.egg +venv/ .vscode # PyInstaller diff --git a/CHANGES.txt b/CHANGES.txt index 66006ee0..19ae9530 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +6.1.0 (Sep 25, 2018) + - Add custom impression listener feature. + - Input Sanitization for track, get_treatment and split. 6.0.0 (Aug 29, 2018) - Add support for redis sentinel - UWSGI performance boost (breaking change) diff --git a/Detailed-README.md b/Detailed-README.md index 71bb7c8e..0f4de2c2 100644 --- a/Detailed-README.md +++ b/Detailed-README.md @@ -32,6 +32,120 @@ The following snippet shows you how to create a basic client using the default c 'SOME_TREATMENT' ``` +## Logging +Split SDK uses logging module from Python. + +### Logging sample +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +## Cache +Split SDK depends on the popular [redis-py](https://github.com/andymccurdy/redis-py) library. + +### Cache Adapter +The redis-py library is supported as the python interface to the Redis key-value store. This library uses a connection pool to manage connections to a Redis server. For further information about how to configure the ```redis-py``` client, please take a look on [redis-py official docs](https://github.com/andymccurdy/redis-py) + +For ```redis``` and their dependencies such as ```jsonpickle``` you can use ```pip``` running the command ```pip install splitio_client[redis,cpphash]==5.5.0``` + +#### Provided redis-py connection - sample code +```python +#Default imports +from __future__ import print_function + +import sys + +from splitio import get_factory +from splitio.exceptions import TimeoutException + +# redis-py options +'''The options below, will be loaded as: +r = redis.StrictRedis(host='localhost', port=6379, db=0, prefix='') +''' +config = { + 'redisDb' : 0, + 'redisHost' : 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': '' +} + +# Create the Split Client instance. +try: + factory = get_factory('API_KEY', config=config) + split = factory.client() +except TimeoutException: + sys.exit() +``` + +#### Provided redis-py connection - sample code for Sentinel Support +```python +#Default imports +from __future__ import print_function + +import sys + +from splitio import get_factory +from splitio.exceptions import TimeoutException + +# redis-py options +''' +The options below, will be loaded as: +sentinel = Sentinel(redisSentinels, { db: redisDb, socket_timeout: redisSocketTimeout }) +master = sentinel.master_for(redisMasterService) +''' +config = { + 'redisDb': 0, + 'redisPrefix': '', + 'redisSentinels': [('IP', PORT), ('IP', PORT), ('IP', PORT)], + 'redisMasterService': 'SERVICE_MASTER_NAME', + 'redisSocketTimeout': 3 +} + +# Create the Split Client instance. +try: + factory = get_factory('API_KEY', config=config) + split = factory.client() +except TimeoutException: + sys.exit() +``` + +## Impression Listener +Split SDKs send impression data back to Split servers periodically and as a result of evaluating splits. In order to additionally send this information to a location of your choice, you could define and attach an Impression Listener. For that purpose, SDK's options have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `log_impression` method and it will receive data in the following schema: + +| Name | Type | Description | +| --- | --- | --- | +| impression | Impression | Impression object that has the feature_name, treatment result, label, etc. | +| attributes | Array | A list of attributes passed by the client. | +| instance-id | String | Corresponds to the IP of the machine where the SDK is running. | +| sdk-language-version | String | Indicates the version of the sdk. In this case the language will be python plus the version of it. | + +### Implementing custom Impression Listener +Below you could find an example of how implement a custom Impression Listener: +```python +# Import ImpressionListener interface +from splitio.impressions import ImpressionListener + +# Implementation Sample for a Custom Impression Listener +class CustomImpressionListener(ImpressionListener) +{ + def log_impression(self, data): + # Custom behavior +} +``` + +### Attaching custom Impression Listener +```python +factory = get_factory( + 'YOUR_API_KEY', + config={ + # ... + 'impressionListener': CustomImpressionListener() + }, + # ... +) +split = factory.client() + ## Additional information You can get more information on how to use this package in the included documentation. \ No newline at end of file diff --git a/splitio/__init__.py b/splitio/__init__.py index 94c1466a..0dcdeef2 100644 --- a/splitio/__init__.py +++ b/splitio/__init__.py @@ -2,7 +2,7 @@ unicode_literals from .factories import get_factory # noqa -from .clients import Key # noqa +from .key import Key # noqa from .version import __version__ # noqa __all__ = ('api', 'brokers', 'cache', 'clients', 'matchers', 'segments', diff --git a/splitio/brokers.py b/splitio/brokers.py index b5501bee..2d026d1f 100644 --- a/splitio/brokers.py +++ b/splitio/brokers.py @@ -57,12 +57,13 @@ class BaseBroker(object): __metaclass__ = abc.ABCMeta - def __init__(self): + def __init__(self, config=None): """ Class constructor, only sets up the logger """ self._logger = logging.getLogger(self.__class__.__name__) self._destroyed = False + self._config = config def fetch_feature(self, name): """ @@ -120,7 +121,7 @@ def destroy(self): pass class JSONFileBroker(BaseBroker): - def __init__(self, segment_changes_file_name, split_changes_file_name): + def __init__(self, config, segment_changes_file_name, split_changes_file_name): """ A Broker implementation that uses responses from the segmentChanges and splitChanges resources to provide access to splits. It is intended to be @@ -133,7 +134,7 @@ def __init__(self, segment_changes_file_name, split_changes_file_name): splitChanges response :type split_changes_file_name: str """ - super(JSONFileBroker, self).__init__() + super(JSONFileBroker, self).__init__(config) self._segment_changes_file_name = segment_changes_file_name self._split_changes_file_name = split_changes_file_name self._split_fetcher = self._build_split_fetcher() @@ -310,7 +311,6 @@ def _build_treatment_log(self): self._sdk_api, max_count=self._max_impressions_log_size, interval=self._impressions_interval, - listener=self._impression_listener ) return AsyncTreatmentLog(self_updating_treatment_log) @@ -388,7 +388,7 @@ class LocalhostEventStorage(object): def log(self, event): pass - def __init__(self, split_definition_file_name=None, auto_refresh_period=2): + def __init__(self, config, split_definition_file_name=None, auto_refresh_period=2): """ A broker implementation that builds its configuration from a split definition file. By default the definition is taken from $HOME/.split @@ -398,7 +398,7 @@ def __init__(self, split_definition_file_name=None, auto_refresh_period=2): :param auto_refresh_period: Number of seconds between split refresh calls :type auto_refresh_period: int """ - super(LocalhostBroker, self).__init__() + super(LocalhostBroker, self).__init__(config) if split_definition_file_name is None: self._split_definition_file_name = os.path.join( @@ -503,11 +503,11 @@ def destroy(self): class RedisBroker(BaseBroker): - def __init__(self, redis): + def __init__(self, redis, config): """A Broker implementation that uses Redis as its backend. :param redis: A redis broker :type redis: StrctRedis""" - super(RedisBroker, self).__init__() + super(RedisBroker, self).__init__(config) split_cache = RedisSplitCache(redis) split_fetcher = CacheBasedSplitFetcher(split_cache) @@ -571,7 +571,7 @@ def __init__(self, uwsgi, config=None): :param config: The configuration dictionary :type config: dict """ - super(UWSGIBroker, self).__init__() + super(UWSGIBroker, self).__init__(config) split_cache = UWSGISplitCache(uwsgi) split_fetcher = CacheBasedSplitFetcher(split_cache) @@ -712,7 +712,7 @@ def get_self_refreshing_broker(api_key, **kwargs): ) if api_key == 'localhost': - return LocalhostBroker(**kwargs) + return LocalhostBroker(config, **kwargs) return SelfRefreshingBroker( api_key, @@ -776,11 +776,11 @@ def get_redis_broker(api_key, **kwargs): api_key, config, _, _ = _init_config(api_key, **kwargs) if api_key == 'localhost': - return LocalhostBroker(**kwargs) + return LocalhostBroker(config, **kwargs) redis = get_redis(config) - redis_broker = RedisBroker(redis) + redis_broker = RedisBroker(redis, config) return redis_broker @@ -836,7 +836,7 @@ def get_uwsgi_broker(api_key, **kwargs): api_key, config, _, _ = _init_config(api_key, **kwargs) if api_key == 'localhost': - return LocalhostBroker(**kwargs) + return LocalhostBroker(config, **kwargs) uwsgi = get_uwsgi() uwsgi_broker = UWSGIBroker(uwsgi, config) diff --git a/splitio/clients.py b/splitio/clients.py index 01c51dcd..ffdfb2e9 100644 --- a/splitio/clients.py +++ b/splitio/clients.py @@ -1,4 +1,4 @@ -"""A module for Split.io SDK API clients""" +"""A module for Split.io SDK API clients.""" from __future__ import absolute_import, division, print_function, \ unicode_literals @@ -6,54 +6,71 @@ import time from splitio.treatments import CONTROL from splitio.splitters import Splitter -from splitio.impressions import Impression, Label +from splitio.impressions import Impression, Label, ImpressionListenerException from splitio.metrics import SDK_GET_TREATMENT from splitio.splits import ConditionType from splitio.events import Event - -class Key(object): - def __init__(self, matching_key, bucketing_key): - """Bucketing Key implementation""" - self.matching_key = matching_key - self.bucketing_key = bucketing_key +from . import input_validator +from splitio.key import Key class Client(object): - def __init__(self, broker, labels_enabled=True): - """Basic interface of a Client. Specific implementations need to override the - get_split_fetcher method (and optionally the get_splitter method). + """Client class that uses a broker for storage.""" + + def __init__(self, broker, labels_enabled=True, impression_listener=None): + """ + Construct a Client instance. + + :param broker: Broker that accepts/retrieves splits, segments, events, metrics & impressions + :type broker: BaseBroker + + :param labels_enabled: Whether to store labels on impressions + :type labels_enabled: bool + + :param impression_listener: impression listener implementation + :type impression_listener: ImpressionListener + + :rtype: Client """ self._logger = logging.getLogger(self.__class__.__name__) self._splitter = Splitter() self._broker = broker self._labels_enabled = labels_enabled self._destroyed = False - - @staticmethod - def _get_keys(key): - """ - """ - if isinstance(key, Key): - matching_key = key.matching_key - bucketing_key = key.bucketing_key - else: - matching_key = str(key) - bucketing_key = None - return matching_key, bucketing_key + self._impression_listener = impression_listener def destroy(self): """ Disable the split-client and free all allocated resources. + Only applicable when using in-memory operation mode. """ self._destroyed = True self._broker.destroy() + def _send_impression_to_listener(self, impression, attributes): + ''' + Sends impression result to custom listener. + + :param impression: Generated impression + :type impression: Impression + + :param attributes: An optional dictionary of attributes + :type attributes: dict + ''' + if self._impression_listener is not None: + try: + self._impression_listener.log_impression(impression, attributes) + except ImpressionListenerException as e: + self._logger.exception(e) + def get_treatment(self, key, feature, attributes=None): """ - Get the treatment for a feature and key, with an optional dictionary of attributes. This - method never raises an exception. If there's a problem, the appropriate log message will - be generated and the method will return the CONTROL treatment. + Get the treatment for a feature and key, with an optional dictionary of attributes. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + :param key: The key for which to get the treatment :type key: str :param feature: The name of the feature for which to get the treatment @@ -64,14 +81,19 @@ def get_treatment(self, key, feature, attributes=None): :rtype: str """ if self._destroyed: - return CONTROL - - if key is None or feature is None: + self._logger.warning("Client has already been destroyed, returning CONTROL") return CONTROL start = int(round(time.time() * 1000)) - matching_key, bucketing_key = self._get_keys(key) + matching_key, bucketing_key = input_validator.validate_key(key) + feature = input_validator.validate_feature_name(feature) + + if (matching_key is None and bucketing_key is None) or feature is None: + impression = self._build_impression(matching_key, feature, CONTROL, Label.EXCEPTION, + 0, bucketing_key, start) + self._record_stats(impression, start, SDK_GET_TREATMENT) + return CONTROL try: label = '' @@ -106,48 +128,80 @@ def get_treatment(self, key, feature, attributes=None): impression = self._build_impression(matching_key, feature, _treatment, label, _change_number, bucketing_key, start) self._record_stats(impression, start, SDK_GET_TREATMENT) + + self._send_impression_to_listener(impression, attributes) + return _treatment - except: + except Exception: # pylint: disable=broad-except self._logger.exception('Exception caught getting treatment for feature') try: - impression = self._build_impression(matching_key, feature, CONTROL, Label.EXCEPTION, - self._broker.get_change_number(), bucketing_key, start) + impression = self._build_impression( + matching_key, + feature, + CONTROL, + Label.EXCEPTION, + self._broker.get_change_number(), bucketing_key, start + ) self._record_stats(impression, start, SDK_GET_TREATMENT) - except: - self._logger.exception('Exception reporting impression into get_treatment exception block') + self._send_impression_to_listener(impression, attributes) + except Exception: # pylint: disable=broad-except + self._logger.exception( + 'Exception reporting impression into get_treatment exception block' + ) return CONTROL - def _build_impression(self, matching_key, feature_name, treatment, label, change_number, bucketing_key, time): - + def _build_impression( + self, matching_key, feature_name, treatment, label, + change_number, bucketing_key, imp_time + ): + """ + Build an impression. + """ if not self._labels_enabled: label = None return Impression( matching_key=matching_key, feature_name=feature_name, treatment=treatment, label=label, change_number=change_number, - bucketing_key=bucketing_key, time=time + bucketing_key=bucketing_key, time=imp_time ) def _record_stats(self, impression, start, operation): + """ + Record impression and metrics. + + :param impression: Generated impression + :type impression: Impression + + :param start: timestamp when get_treatment was called + :type start: int + + :param operation: operation performed. + :type operation: str + """ try: end = int(round(time.time() * 1000)) self._broker.log_impression(impression) self._broker.log_operation_time(operation, end - start) - except: + except Exception: # pylint: disable=broad-except self._logger.exception('Exception caught recording impressions and metrics') def _get_treatment_for_split(self, split, matching_key, bucketing_key, attributes=None): """ - Internal method to get the treatment for a given Split and optional attributes. This - method might raise exceptions and should never be used directly. + Evaluate the user submitted data againt a feature and return the resulting treatment. + + This method might raise exceptions and should never be used directly. :param split: The split for which to get the treatment :type split: Split + :param key: The key for which to get the treatment :type key: str + :param attributes: An optional dictionary of attributes :type attributes: dict + :return: The treatment for the key and split :rtype: str """ @@ -189,31 +243,85 @@ def _get_treatment_for_split(self, split, matching_key, bucketing_key, attribute def track(self, key, traffic_type, event_type, value=None): """ - Track an event + Track an event. + + :param key: user key associated to the event + :type key: str + + :param traffic_type: traffic type name + :type traffic_type: str + + :param event_type: event type name + :type event_type: str + + :param value: (Optional) value associated to the event + :type value: Number + + :rtype: bool """ - e = Event( + key = input_validator.validate_track_key(key) + event_type = input_validator.validate_event_type(event_type) + traffic_type = input_validator.validate_traffic_type(traffic_type) + value = input_validator.validate_value(value) + + if key is None or event_type is None or traffic_type is None or value is False: + return False + + event = Event( key=key, trafficTypeName=traffic_type, eventTypeId=event_type, value=value, timestamp=int(time.time()*1000) ) - return self._broker.get_events_log().log_event(e) + return self._broker.get_events_log().log_event(event) + class MatcherClient(Client): """ + Client to be used by matchers such as "Dependency Matcher". + + TODO: Refactor This! """ - def __init__(self, broker, splitter, logger): + + def __init__(self, broker, splitter, logger): # pylint: disable=super-init-not-called + """ + Construct a MatcherClient instance. + + :param broker: Broker where splits & segments will be fetched. + :type broker: BaseBroker + + :param splitter: splitter + :type splitter: Splitter + + :param logger: logger object + :type logger: logging.Logger + """ self._broker = broker self._splitter = splitter self._logger = logger def get_treatment(self, key, feature, attributes=None): """ + Evaluate a feature and return the appropriate traetment. + + Will not generate impressions nor metrics + + :param key: user key + :type key: mixed + + :param feature: feature name + :type feature: str + + :param attributes: (Optional) attributes associated with the user key + :type attributes: dict """ - if key is None or feature is None: return CONTROL + matching_key, bucketing_key = input_validator.validate_key(key) + feature = input_validator.validate_feature_name(feature) + + if (matching_key is None and bucketing_key is None) or feature is None: + return CONTROL - matching_key, bucketing_key = self._get_keys(key) try: # Fetching Split definition split = self._broker.fetch_feature(feature) @@ -225,7 +333,8 @@ def get_treatment(self, key, feature, attributes=None): ) return CONTROL - if split.killed: return split.default_treatment + if split.killed: + return split.default_treatment treatment, _ = self._get_treatment_for_split( split, @@ -234,8 +343,12 @@ def get_treatment(self, key, feature, attributes=None): attributes ) - if treatment is None: return split.default_treatment + if treatment is None: + return split.default_treatment + return treatment - except: - self._logger.exception('Exception caught retrieving dependent feature. Returning CONTROL') + except Exception: # pylint: disable=broad-except + self._logger.exception( + 'Exception caught retrieving dependent feature. Returning CONTROL' + ) return CONTROL diff --git a/splitio/factories.py b/splitio/factories.py index 76beabf0..42014934 100644 --- a/splitio/factories.py +++ b/splitio/factories.py @@ -5,6 +5,7 @@ from splitio.brokers import get_self_refreshing_broker, get_redis_broker, get_uwsgi_broker from splitio.managers import RedisSplitManager, SelfRefreshingSplitManager, \ LocalhostSplitManager, UWSGISplitManager +from splitio.impressions import ImpressionListenerWrapper import logging @@ -40,18 +41,21 @@ def __init__(self, api_key, **kwargs): config = kwargs['config'] labels_enabled = config.get('labelsEnabled', True) + + impression_listener = ImpressionListenerWrapper(config.get('impressionListener')) if 'impressionListener' in config else None # noqa: E501,E261 + if 'redisHost' in config or 'redisSentinels' in config: broker = get_redis_broker(api_key, **kwargs) - self._client = Client(broker, labels_enabled) + self._client = Client(broker, labels_enabled, impression_listener) self._manager = RedisSplitManager(broker) else: if 'uwsgiClient' in config and config['uwsgiClient']: broker = get_uwsgi_broker(api_key, **kwargs) - self._client = Client(broker, labels_enabled) + self._client = Client(broker, labels_enabled, impression_listener) self._manager = UWSGISplitManager(broker) else: broker = get_self_refreshing_broker(api_key, **kwargs) - self._client = Client(broker, labels_enabled) + self._client = Client(broker, labels_enabled, impression_listener) self._manager = SelfRefreshingSplitManager(broker) def client(self): # pragma: no cover diff --git a/splitio/impressions.py b/splitio/impressions.py index 2f19425a..2c727a71 100644 --- a/splitio/impressions.py +++ b/splitio/impressions.py @@ -4,12 +4,14 @@ import logging import six +import abc from threading import Thread from collections import namedtuple, defaultdict from concurrent.futures import ThreadPoolExecutor from copy import deepcopy from threading import RLock, Timer +from splitio.config import SDK_VERSION, DEFAULT_CONFIG Impression = namedtuple( @@ -56,24 +58,6 @@ def build_impressions_data(impressions): ] -def _notify_listener(listener, impressions_data): - """ - Execute custom callable provided by user with impressions as arguments - :param impressions_data: Impressions grouped by feature name. - :type impressions_data: list of dicts - """ - if six.callable(listener): - try: - t = Thread(target=listener, args=(impressions_data,)) - t.daemon = True - t.start() - except Exception: - logging.getLogger('Impressions-Listener').exception( - 'Exception caught when executing user provided impression ' - 'listener function.' - ) - - class Label(object): # Condition: Split Was Killed # Treatment: Default treatment @@ -268,7 +252,7 @@ def _log(self, impression): class SelfUpdatingTreatmentLog(InMemoryTreatmentLog): def __init__(self, api, interval=180, max_workers=5, max_count=-1, - ignore_impressions=False, listener=None): + ignore_impressions=False): """ An impressions implementation that sends the in impressions stored periodically to the Split.io back-end. @@ -282,9 +266,6 @@ def __init__(self, api, interval=180, max_workers=5, max_count=-1, :type max_count: int :param ignore_impressions: Whether to ignore log requests :type ignore_impressions: bool - :param listener: callback that will receive impressions bulk fur custom - user handling of impressions. - :type listener: callable """ super(SelfUpdatingTreatmentLog, self).__init__( max_count=max_count, @@ -294,7 +275,6 @@ def __init__(self, api, interval=180, max_workers=5, max_count=-1, self._interval = interval self._stopped = True self._thread_pool_executor = ThreadPoolExecutor(max_workers=max_workers) - self._listener = listener @property def stopped(self): @@ -335,7 +315,6 @@ def _update_evictions(self, feature_name, feature_impressions): if len(test_impressions_data) > 0: self._api.test_impressions(test_impressions_data) - _notify_listener(self._listener, test_impressions_data) except: self._logger.exception( 'Exception caught updating evicted impressions' @@ -355,7 +334,6 @@ def _update_impressions(self): if len(test_impressions_data) > 0: self._api.test_impressions(test_impressions_data) - _notify_listener(self._listener, test_impressions_data) except: self._logger.exception('Exception caught updating impressions') self._stopped = True @@ -458,3 +436,47 @@ def log(self, impression): self._logger.exception( 'Exception caught logging impression asynchronously' ) + + +class ImpressionListenerException(Exception): + ''' + Custom Exception for Impression Listener + ''' + pass + + +class ImpressionListenerWrapper(object): + """ + Wrapper in charge of building all the data that client would require in case + of adding some logic with the treatment and impression results. + """ + + impression_listener = None + + def __init__(self, impression_listener): + self.impression_listener = impression_listener + + def log_impression(self, impression, attributes=None): + data = {} + data['impression'] = impression + data['attributes'] = attributes + data['instance-id'] = DEFAULT_CONFIG['splitSdkMachineIp'] + data['sdk-language-version'] = SDK_VERSION + try: + self.impression_listener.log_impression(data) + except: + raise ImpressionListenerException('Exception caught in log_impression user\'s' + 'method is throwing exceptions') + + +class ImpressionListener(object): + """ + Abstract class defining the interface that concrete client must implement, + and including methods that use that interface to add client's logic for each + impression. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def log_impression(self, data): + pass diff --git a/splitio/input_validator.py b/splitio/input_validator.py new file mode 100644 index 00000000..b054fe65 --- /dev/null +++ b/splitio/input_validator.py @@ -0,0 +1,284 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from numbers import Number +import logging +import six +import re +from splitio.key import Key + +_LOGGER = logging.getLogger(__name__) + + +def _check_not_null(value, name, operation): + """ + Checks if value is null + + :param key: value to be checked + :type key: str + :param name: name to inform the error + :type feature: str + :param operation: operation to inform the error + :type operation: str + :return: The result of validation + :rtype: True|False + """ + if value is None: + _LOGGER.error('{}: {} cannot be None.'.format(operation, name)) + return False + return True + + +def _check_is_string(value, name, operation): + """ + Checks if value is not string + + :param key: value to be checked + :type key: str + :param name: name to inform the error + :type feature: str + :param operation: operation to inform the error + :type operation: str + :return: The result of validation + :rtype: True|False + """ + if isinstance(value, six.string_types) is False: + _LOGGER.error('{}: {} {} has to be of type string.'.format( + operation, name, value)) + return False + return True + + +def _check_string_not_empty(value, name, operation): + """ + Checks if value is an empty string + + :param key: value to be checked + :type key: str + :param name: name to inform the error + :type feature: str + :param operation: operation to inform the error + :type operation: str + :return: The result of validation + :rtype: True|False + """ + if value.strip() == "": + _LOGGER.error('{}: {} must not be empty.'.format(operation, name)) + return False + return True + + +def _check_string_matches(value, name, operation, pattern): + """ + Checks if value is adhere to a regular expression passed + + :param key: value to be checked + :type key: str + :param name: name to inform the error + :type feature: str + :param operation: operation to inform the error + :type operation: str + :param pattern: pattern that needs to adhere + :type pattern: str + :return: The result of validation + :rtype: True|False + """ + if not re.match(pattern, value): + _LOGGER.error('{}: {} must adhere to the regular expression {}.' + .format(operation, name, pattern)) + return False + return True + + +def _check_can_convert(value, name, operation, message): + """ + Checks if is a valid convertion. + + :param key: value to be checked + :type key: bool|number|array| + :param name: name to inform the error + :type feature: str + :param operation: operation to inform the error + :type operation: str + :param message: message to inform the error + :type message: str + :return: The result of validation + :rtype: True|False + """ + if isinstance(value, six.string_types): + return True + else: + if isinstance(value, bool) or (not isinstance(value, Number)): + _LOGGER.error('{}: {} {} {}'.format(operation, name, value, message)) + return False + _LOGGER.warning('{}: {} {} is not of type string, converting.' + .format(operation, name, value)) + return True + + +def _check_valid_matching_key(matching_key): + """ + Checks if matching_key is valid for get_treatment when is + sent as Key Object + + :param matching_key: matching_key to be checked + :type matching_key: str + :return: The result of validation + :rtype: True|False + """ + if matching_key is None: + _LOGGER.error('get_treatment: Key should be an object with bucketingKey and ' + 'matchingKey with valid string properties.') + return False + if isinstance(matching_key, six.string_types): + if not _check_string_not_empty(matching_key, 'matching_key', 'get_treatment'): + return False + else: + if not _check_can_convert(matching_key, 'matching_key', 'get_treatment', + 'has to be of type string.'): + return False + return True + + +def _check_valid_bucketing_key(bucketing_key): + """ + Checks if bucketing_key is valid for get_treatment when is + sent as Key Object + + :param bucketing_key: bucketing_key to be checked + :type bucketing_key: str + :return: The result of validation + :rtype: True|False + """ + if bucketing_key is None: + _LOGGER.warning('get_treatment: Key object should have bucketingKey set.') + return None + if not _check_can_convert(bucketing_key, 'bucketing_key', 'get_treatment', + 'has to be of type string.'): + return False + return str(bucketing_key) + + +def validate_key(key): + """ + Validate Key parameter for get_treatment, if is invalid at some point + the bucketing_key or matching_key it will return None + + :param key: user key + :type key: mixed + :return: The tuple key + :rtype: (matching_key,bucketing_key) + """ + matching_key_result = None + bucketing_key_result = None + if not _check_not_null(key, 'key', 'get_treatment'): + return None, None + if isinstance(key, Key): + if _check_valid_matching_key(key.matching_key): + matching_key_result = str(key.matching_key) + else: + return None, None + bucketing_key_result = _check_valid_bucketing_key(key.bucketing_key) + if bucketing_key_result is False: + return None, None + return matching_key_result, bucketing_key_result + else: + if _check_can_convert(key, 'key', 'get_treatment', + 'has to be of type string or object Key.'): + matching_key_result = str(key) + return matching_key_result, bucketing_key_result + + +def validate_feature_name(feature_name): + """ + Checks if feature_name is valid for get_treatment + + :param feature_name: feature_name to be checked + :type feature_name: str + :return: feature_name + :rtype: str|None + """ + if (not _check_not_null(feature_name, 'feature_name', 'get_treatment')) or \ + (not _check_is_string(feature_name, 'feature_name', 'get_treatment')): + return None + return feature_name + + +def validate_track_key(key): + """ + Checks if key is valid for track + + :param key: key to be checked + :type key: str + :return: key + :rtype: str|None + """ + if (not _check_not_null(key, 'key', 'track')) or \ + (not _check_can_convert(key, 'key', 'track', 'has to be of type string.')): + return None + return str(key) + + +def validate_traffic_type(traffic_type): + """ + Checks if traffic_type is valid for track + + :param traffic_type: traffic_type to be checked + :type traffic_type: str + :return: traffic_type + :rtype: str|None + """ + if (not _check_not_null(traffic_type, 'traffic_type', 'track')) or \ + (not _check_is_string(traffic_type, 'traffic_type', 'track')) or \ + (not _check_string_not_empty(traffic_type, 'traffic_type', 'track')): + return None + return traffic_type + + +def validate_event_type(event_type): + """ + Checks if event_type is valid for track + + :param event_type: event_type to be checked + :type event_type: str + :return: event_type + :rtype: str|None + """ + if (not _check_not_null(event_type, 'event_type', 'track')) or \ + (not _check_is_string(event_type, 'event_type', 'track')) or \ + (not _check_string_matches(event_type, 'event_type', 'track', + r'[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}')): + return None + return event_type + + +def validate_value(value): + """ + Checks if value is valid for track + + :param value: value to be checked + :type value: number + :return: value + :rtype: number|None + """ + if value is None: + return None + if (not isinstance(value, Number)) or isinstance(value, bool): + _LOGGER.error('track: value must be a number.') + return False + return value + + +def validate_manager_feature_name(feature_name): + """ + Checks if feature_name is valid for track + + :param feature_name: feature_name to be checked + :type feature_name: str + :return: feature_name + :rtype: str|None + """ + if (not _check_not_null(feature_name, 'feature_name', 'split')) or \ + (not _check_is_string(feature_name, 'feature_name', 'split')): + return None + return feature_name diff --git a/splitio/key.py b/splitio/key.py new file mode 100644 index 00000000..e50d43ba --- /dev/null +++ b/splitio/key.py @@ -0,0 +1,22 @@ +"""A module for Split.io SDK API clients.""" +from __future__ import absolute_import, division, print_function, \ + unicode_literals + + +class Key(object): + """Key class includes a matching key and bucketing key.""" + + def __init__(self, matching_key, bucketing_key): + """Construct a key object.""" + self._matching_key = matching_key + self._bucketing_key = bucketing_key + + @property + def matching_key(self): + """Return matching key.""" + return self._matching_key + + @property + def bucketing_key(self): + """Return bucketing key.""" + return self._bucketing_key diff --git a/splitio/managers.py b/splitio/managers.py index 5d7df4a2..96742529 100644 --- a/splitio/managers.py +++ b/splitio/managers.py @@ -3,10 +3,10 @@ import logging -from splitio.uwsgi import UWSGISplitCache from splitio.redis_support import RedisSplitCache -from splitio.splits import (CacheBasedSplitFetcher, SplitView) +from splitio.splits import SplitView from splitio.utils import bytes_to_string +from . import input_validator class SplitManager(object): @@ -60,7 +60,8 @@ def split_names(self): split_names = [] for split_name in splits: split_name = bytes_to_string(split_name) - split_names.append(split_name.replace(RedisSplitCache._KEY_TEMPLATE.format(suffix=''), '')) + split_names.append(split_name.replace + (RedisSplitCache._KEY_TEMPLATE.format(suffix=''), '')) return split_names @@ -80,7 +81,9 @@ def splits(self): # pragma: no cover for condition in split.conditions: for partition in condition.partitions: treatments.append(partition.treatment) - split_views.append(SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, treatments=list(set(treatments)), change_number=change_number)) + split_views.append(SplitView(name=split.name, traffic_type=split.traffic_type_name, + killed=split.killed, treatments=list(set(treatments)), + change_number=change_number)) return split_views @@ -89,6 +92,11 @@ def split(self, feature_name): # pragma: no cover :return: The SplitView instance. :rtype: SplitView """ + feature_name = input_validator.validate_manager_feature_name(feature_name) + + if feature_name is None: + return None + split = self._split_fetcher.fetch(feature_name) if split is None: @@ -102,8 +110,10 @@ def split(self, feature_name): # pragma: no cover for partition in condition.partitions: treatments.append(partition.treatment) - #Using sets to avoid duplicate entries - split_view = SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, treatments=list(set(treatments)), change_number=change_number) + # Using sets to avoid duplicate entries + split_view = SplitView(name=split.name, traffic_type=split.traffic_type_name, + killed=split.killed, treatments=list(set(treatments)), + change_number=change_number) return split_view @@ -148,7 +158,9 @@ def splits(self): # pragma: no cover for condition in split.conditions: for partition in condition.partitions: treatments.append(partition.treatment) - split_views.append(SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, treatments=list(set(treatments)), change_number=split.change_number)) + split_views.append(SplitView(name=split.name, traffic_type=split.traffic_type_name, + killed=split.killed, treatments=list(set(treatments)), + change_number=split.change_number)) return split_views @@ -157,6 +169,11 @@ def split(self, feature_name): # pragma: no cover :return: The SplitView instance. :rtype: SplitView """ + feature_name = input_validator.validate_manager_feature_name(feature_name) + + if feature_name is None: + return None + split = self._split_fetcher.fetch(feature_name) if split is None: @@ -168,8 +185,10 @@ def split(self, feature_name): # pragma: no cover for partition in condition.partitions: treatments.append(partition.treatment) - #Using sets on treatments to avoid duplicate entries - split_view = SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, treatments=list(set(treatments)), change_number=split.change_number) + # Using sets on treatments to avoid duplicate entries + split_view = SplitView(name=split.name, traffic_type=split.traffic_type_name, + killed=split.killed, treatments=list(set(treatments)), + change_number=split.change_number) return split_view @@ -210,17 +229,22 @@ def splits(self): # pragma: no cover for condition in split.conditions: for partition in condition.partitions: treatments.append(partition.treatment) - split_views.append(SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, - treatments=list(set(treatments)), change_number=change_number)) + split_views.append(SplitView(name=split.name, traffic_type=split.traffic_type_name, + killed=split.killed, treatments=list(set(treatments)), + change_number=change_number)) return split_views - def split(self, feature_name): """Get the splitView of feature_name. Subclasses need to override this method. :return: The SplitView instance. :rtype: SplitView """ + feature_name = input_validator.validate_manager_feature_name(feature_name) + + if feature_name is None: + return None + split = self._split_fetcher.fetch(feature_name) if split is None: @@ -235,8 +259,9 @@ def split(self, feature_name): treatments.append(partition.treatment) # Using sets to avoid duplicate entries - split_view = SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, - treatments=list(set(treatments)), change_number=change_number) + split_view = SplitView(name=split.name, traffic_type=split.traffic_type_name, + killed=split.killed, treatments=list(set(treatments)), + change_number=change_number) return split_view @@ -279,7 +304,6 @@ def splits(self): # pragma: no cover return split_views - def split(self, feature_name): """ Get the splitView of feature_name. Subclasses need to override this diff --git a/splitio/tasks.py b/splitio/tasks.py index 7d5377d4..efd9f600 100644 --- a/splitio/tasks.py +++ b/splitio/tasks.py @@ -12,7 +12,6 @@ from .splits import Status from .impressions import build_impressions_data -from .impressions import _notify_listener from . import asynctask from . import events @@ -135,7 +134,7 @@ def update_splits(split_cache, split_change_fetcher, split_parser): return added_features, removed_features -def report_impressions(impressions_cache, sdk_api, listener=None): +def report_impressions(impressions_cache, sdk_api): """ If the reporting process is enabled (through the impressions cache), this function collects the impressions from the cache and sends them to @@ -148,7 +147,6 @@ def report_impressions(impressions_cache, sdk_api, listener=None): impressions = impressions_cache.fetch_all_and_clear() test_impressions_data = build_impressions_data(impressions) - _notify_listener(listener, {'impressions': test_impressions_data}) _logger.debug('Impressions to send: %s' % test_impressions_data) diff --git a/splitio/tests/murmur3-custom-uuids.csv b/splitio/tests/murmur3-custom-uuids.csv new file mode 100644 index 00000000..6c7caee8 --- /dev/null +++ b/splitio/tests/murmur3-custom-uuids.csv @@ -0,0 +1,500 @@ +1798236110,211cc9c0-4154-0135-021e-0242ac112609,1029223426,27 +1798236110,0b867d50-17cf-0136-dcb0-0242ac115605,145158075,76 +1798236110,cfbd14f0-959a-0135-a37b-0242ac116708,2338571260,61 +1798236110,3be1b3c0-b697-0135-ceef-0242ac11510d,2174081703,4 +1798236110,b950dab0-2ffa-0136-07fb-0242ac11570b,1045037853,54 +1798236110,de32d3d0-ae16-0135-5b5e-0242ac11560a,1011102904,5 +1798236110,d83c4ef0-4904-0135-d199-0242ac11130b,211045901,2 +1798236110,a8f44bc0-70bd-0135-fab6-0242ac11560f,475163415,16 +1798236110,4e0d6cf0-2ae2-0136-98d8-0242ac115709,1195819048,49 +1798236110,455d0440-8251-0130-f786-123138068411,898059731,32 +1798236110,74c865f0-3013-0136-0860-0242ac11570b,1625083405,6 +1798236110,791687a0-2353-0136-60ff-0242ac115109,1496507406,7 +1798236110,998091b0-e1c3-0135-2865-0242ac11210d,3562428648,49 +1798236110,e609bdc0-1bd4-0136-5b7e-0242ac111506,3131027573,74 +1798236110,408810b0-b3de-0133-65e8-66636c1c99fd,612726717,18 +1798236110,2b9fe720-2852-0135-9765-0242ac113a06,3276053247,48 +1798236110,39203fb0-ee44-0135-ac0a-0242ac111c07,298838722,23 +1798236110,35098240-b015-0135-4060-0242ac112c0c,3163017301,2 +1798236110,0bac8bc0-1a75-0136-46fe-0242ac11510f,3780684808,9 +1798236110,2752ed10-a546-0135-4ad9-0242ac111508,128563500,1 +1798236110,3acf01f0-aa8c-0135-719b-0242ac115108,3435432402,3 +1798236110,f4545bb0-318a-0136-a1f8-0242ac11570b,4222469105,6 +1798236110,381eb640-f9f4-0135-523c-0242ac116709,510576969,70 +1798236110,64f59890-ff06-0135-e4fd-0242ac111c0a,1650092806,7 +1798236110,82644e30-a7c6-0135-458d-0242ac112c08,3088855707,8 +1798236110,55fdc3d0-e818-0135-32ba-0242ac11210d,2342123800,1 +1798236110,6931df30-4958-0135-8a4e-0242ac113a10,997003508,9 +1798236110,b1955a60-8fdc-0135-eaba-0242ac116705,1401557467,68 +1798236110,bca99e00-2c4b-0135-5593-0242ac115b0f,1796930506,7 +1798236110,c1c4db00-94fc-0135-f9e1-0242ac116706,1433705136,37 +1798236110,4055f5e0-e23a-0133-8639-2e1fbab1bd04,893175810,11 +1798236110,3b8e3f30-26c2-0136-7eb4-0242ac115605,4162533673,74 +1798236110,ee27a570-2319-0136-ac3c-0242ac11210f,1789436792,93 +1798236110,7e5497c0-ecc2-0135-839a-0242ac11150c,462718901,2 +1798236110,1f875e10-86b4-0135-7be5-0242ac116713,382942103,4 +1798236110,431decb0-8251-0130-f786-123138068411,1945717881,82 +1798236110,d1325fd0-714f-0135-0ad1-0242ac116707,3512495607,8 +1798236110,145385dc-a038-442e-999f-2fbe364e2b10,1017915614,15 +1798236110,3acb6f80-2901-0135-994f-0242ac113a06,4052935609,10 +1798236110,68b73310-01b9-0135-c4bf-0242ac113507,2245618437,38 +1798236110,a8c5c9d0-30e3-0136-637c-0242ac112c0e,2569518306,7 +1798236110,474a8370-2173-0136-a5dc-0242ac115605,1819252407,8 +1798236110,68c07650-31ea-0136-a2fb-0242ac11570b,2994338191,92 +1798236110,f4371450-f923-0130-10cb-261f49ef608c,1112177848,49 +1798236110,3ee87180-267b-0134-d663-4e216589424e,2249486103,4 +1798236110,36191b10-3457-0136-da12-0242ac11150a,3586239406,7 +1798236110,25b295d6-e1d5-4c82-ba20-61ee012fa433,716606169,70 +1798236110,bea5ee08-12c9-445e-9c25-dca32323abe1,896736372,73 +1798236110,e17d44b0-9d34-0134-303d-46c1d7cc6a65,1741734346,47 +1798236110,08fd5430-b82c-0135-f559-0242ac115606,2605628542,43 +1798236110,0fe68840-2602-0136-6712-0242ac116705,3695966102,3 +1798236110,41bf6bf0-02c7-0136-0f23-0242ac111c0a,1939719549,50 +1798236110,c4b0bad0-f964-0135-a0cd-0242ac116709,1752397609,10 +1798236110,00cfd620-34f4-0136-dbc1-0242ac11150a,2702369500,1 +1798236110,409b20c0-34c6-0136-3bc0-0242ac115607,3417104608,9 +1798236110,a965cb40-860a-0135-aea1-0242ac115107,687715528,29 +1798236110,276f0380-d398-0135-3122-0242ac11670d,2778293187,88 +1798236110,e3367470-fe03-0135-6f3b-0242ac112c0c,3933579898,99 +1798236110,66b20990-dc3e-0135-b630-0242ac11150e,2181039704,5 +1798236110,3319885f-dd37-4254-b2d9-4bb3e1385536,2704897942,43 +1798236110,c116d490-b120-0131-848d-2ae9f8eb7efd,2817565791,92 +1798236110,b6747020-1fcd-0136-a76e-0242ac11210f,109646505,6 +1798236110,e65ee9c0-1b0f-0136-d7e1-0242ac112108,1509213121,22 +1798236110,fba2dfc0-2976-0136-d908-0242ac112109,1339766980,81 +1798236110,1206f840-fee1-0135-4ccb-0242ac11150a,2765210802,3 +1798236110,3005d360-a177-0135-e498-0242ac115106,2794183107,8 +1798236110,40133b40-ff82-0135-f090-0242ac115709,1410159404,5 +1798236110,130b00d0-9d5c-0135-a677-0242ac115105,1595452188,89 +1798236110,cf0d0ee3-42ee-4487-a053-114c780d02d8,3639287801,2 +1798236110,0523f651-ebc6-405b-bbc9-26711944ce7b,511292670,71 +1798236110,95a04ec0-34c2-0136-db12-0242ac11150a,205060415,16 +1798236110,8a9da7a0-f488-0135-971c-0242ac11570b,594433339,40 +1798236110,d9f3d3b3-2872-485e-8d32-13803f8cf1d2,507342893,94 +1798236110,19c41bb0-0104-0135-3167-0242ac11350b,924889202,3 +1798236110,3390ef50-8b89-0135-a4c8-0242ac112c0a,2174670038,39 +1798236110,916449f0-f414-0135-2f60-0242ac116709,3691285305,6 +1798236110,e0abe680-be1c-0135-9a8a-0242ac115606,502122505,6 +1798236110,450cdf80-0843-0136-3147-0242ac115606,3398671499,100 +1798236110,892a43d0-892b-0135-81b0-0242ac116713,1834587505,6 +1798236110,745c4d9c-96e1-4b8d-88b0-367986d55332,3533867083,84 +1798236110,9cd4ed60-d75a-0135-8ac3-0242ac111c09,797547001,2 +1798236110,00a88bb0-dcfc-0134-503f-0242ac112c0a,2592345077,78 +1798236110,73dfd830-3429-0136-e07f-0242ac11210b,3209735903,4 +1798236110,36b0b550-0ef3-0136-cca2-0242ac112109,3993014503,4 +1798236110,062336b7-d02b-4507-b254-646bed28cf34,2368216781,82 +1798236110,9dbf8fd0-932e-0134-9840-46864cc1a8e0,3187165488,89 +1798236110,f4f73ba0-9fa0-0134-5fe6-22a9bec64a8c,2634722709,10 +1798236110,855a0460-2ed0-0136-fd44-0242ac115108,176022308,9 +1798236110,2a7218f0-1a65-0136-587d-0242ac111506,3215558115,16 +1798236110,6e4f3fb0-a15c-0135-d03f-0242ac116705,631949403,4 +1798236110,e9dae170-319e-0136-764f-0242ac115609,3213107219,20 +1798236110,d31b9610-2f79-0136-f7de-0242ac115607,2649111334,35 +1798236110,1340df00-312e-0136-7535-0242ac115609,2413056004,5 +1798236110,d83f59c0-7f6d-0135-6f10-0242ac112c08,3845773585,86 +1798236110,15510568-aeb0-4726-8b77-d3001041fe08,1414126216,17 +1798236110,3fc4cc80-b5ad-0135-45cc-0242ac11150b,408384118,19 +1798236110,622994e0-d8f0-0135-88d5-0242ac11570b,1805018801,2 +1798236110,baee5520-7666-0134-06e3-3eaed79935f0,2964531470,71 +1798236110,8bd2ffcf-80c7-4ff4-ac03-0e8b152517ce,707186809,10 +1798236110,a8dfa190-7ba7-0131-f9fa-22baa31ac126,4172595706,7 +1798236110,fd1be9a0-2646-0136-5544-0242ac11510c,3555035706,7 +1798236110,9a68e620-13c4-0136-1243-0242ac116705,2156080200,1 +1798236110,a6c76990-fa22-0135-a48a-0242ac112c0c,816733303,4 +1798236110,e22aa790-b5bf-0135-ffc4-0242ac116709,2229042906,7 +1798236110,11b721a0-0f59-0136-b683-0242ac11570b,4057157827,28 +1798236110,e4a4b130-b31f-0134-94d4-0242ac112c07,2737106662,63 +1798236110,04fc7d10-2d34-0135-f301-0242ac112610,354909002,3 +1798236110,ea855ed0-ff1f-0135-c162-0242ac115608,2256695109,10 +1798236110,ea5c4599-27b5-4de6-b121-24149efe7087,4046266842,43 +1798236110,1947c820-02e2-0136-5f5c-0242ac112c0c,1948627159,60 +1798236110,73525710-3f3d-0135-5838-0242ac113a0a,3910323650,51 +1798236110,75006400-2eb1-0136-fb9d-0242ac115108,2070083799,100 +1798236110,df1f9e10-ec8b-0135-0035-0242ac112c09,2407221541,42 +1798236110,913d3ac0-0009-0134-7940-6ab7b46b4e96,1849295006,7 +1798236110,d573d050-3422-0136-a505-0242ac11570b,3444608607,8 +1798236110,f7a1fb50-1ec0-0136-5904-0242ac115109,3339544318,19 +1798236110,6f469490-18f1-0136-07a9-0242ac116709,954592650,51 +1798236110,b67ea580-9734-0135-9f66-0242ac11150b,4238711700,1 +1798236110,48b8d360-d5df-0134-5d02-0242ac112c08,1796406606,7 +1798236110,e9feda80-ceaf-0134-6b67-0242ac112c09,3774787674,75 +1798236110,7adc96b0-2091-0136-5da3-0242ac115109,1853375475,76 +1798236110,f613e250-68a9-0135-e96f-0242ac116110,1961300806,7 +1798236110,0af49fd0-3174-0136-a1dd-0242ac11570b,2964589096,97 +1798236110,d3a6c660-2eb1-0135-0dc7-0242ac112610,1017163494,95 +1798236110,76cc9ce0-9a02-0134-3941-2e45891bd616,3217699103,4 +1798236110,d8184df0-1b09-0136-4807-0242ac11510f,405201004,5 +1798236110,a8dc04b0-d1cb-0135-be0d-0242ac11150e,1148820008,9 +1798236110,333233f0-f479-0135-b0f8-0242ac115607,3777409714,15 +1798236110,bab0a110-32ec-0133-7d35-42bbafba3712,1637096894,95 +1798236110,b97a4e60-b328-0135-d1e8-0242ac116709,1400618672,73 +1798236110,e1dba3e0-0ca9-0136-285d-0242ac112c0e,3448669610,11 +1798236110,919506e0-8e78-0135-a8cd-0242ac112c0a,3072542209,10 +1798236110,6155cc80-1f4a-0136-a633-0242ac11210f,1703471727,28 +1798236110,a0ab07b0-a4eb-0132-4c49-621444594b32,944215043,44 +1798236110,e5cfad80-a91a-0134-5bc0-3e421a69d19a,1619680808,9 +1798236110,209935f0-636e-0135-f34f-0242ac116708,2181691364,65 +1798236110,205e7dd0-2d39-0136-be12-0242ac111511,900494854,55 +1798236110,66009660-89a6-0135-e3cc-0242ac115608,982876600,1 +1798236110,bc49aef0-f8c3-0135-5a3d-0242ac111511,2343683209,10 +1798236110,56e53f70-f218-0135-e005-0242ac11570b,394022839,40 +1798236110,404f5d50-d1f4-0135-5060-0242ac112c0c,2491192200,1 +1798236110,f1f92850-e8f7-0135-ca06-0242ac11150e,3643354916,17 +1798236110,81de84d0-59cf-0135-066d-0242ac113a15,3043065188,89 +1798236110,9f896900-b5c9-0135-6658-0242ac112c0c,1256050101,2 +1798236110,45b43c00-f1c8-0135-b062-0242ac111c07,2388953122,23 +1798236110,a15fafe0-ecac-0135-82eb-0242ac114a09,3764454000,1 +1798236110,1a807550-8cce-0134-aae2-4e458d5f96c5,3025990309,10 +1798236110,e994c130-2e9e-0136-3035-0242ac112108,2955908406,7 +1798236110,8986bd10-aadb-0135-24b3-0242ac11570b,1872280339,40 +1798236110,04980cc0-282e-0135-0cde-0242ac11130b,1406449403,4 +1798236110,58deb98e-245d-4cf4-95f6-d285d8014494,42703188,89 +1798236110,fecd1620-2189-0136-d4ba-0242ac11150f,1312837017,18 +1798236110,b4d72230-b6f2-0134-9f6b-0242ac112c09,2339463901,2 +1798236110,12362fd0-8cc8-0134-8f56-6aad62a7000c,4101480705,6 +1798236110,d7e01010-189c-0136-9f31-0242ac115108,3045845501,2 +1798236110,cf6b8624-c5e7-4182-83cb-51ff70219b0f,23405333,34 +1798236110,81ec5a10-ea06-0133-893a-72e12eb45410,525677504,5 +1798236110,b25b15f0-2528-0136-5228-0242ac11510c,3917576316,17 +1798236110,1cf0e2c0-3441-0136-ed46-0242ac11670f,329577516,17 +1798236110,35258340-4a58-0135-d654-0242ac11130b,2165424932,33 +1798236110,3a1653c0-177a-0133-1200-76a694fdbaeb,1979148075,76 +1798236110,a1d7d2d0-23c2-0136-89b3-0242ac112c08,3657004834,35 +1798236110,28c17de3-3eb1-4548-bce6-117a309e28c4,2276176543,44 +1798236110,a9756a60-fd08-0134-664d-0242ac112c07,4294172909,10 +1798236110,a164bd50-690c-0133-1607-76df98c846f2,1975314953,54 +1798236110,84fb0970-c092-0135-a9e8-0242ac11150b,669467501,2 +1798236110,2205c560-2ce5-0134-bc96-7a0a2fea0b65,354325608,9 +1798236110,bae82c20-8427-0134-6347-5222656bf4dc,1925065805,6 +1798236110,2cdba030-081e-0136-1794-0242ac111c0a,3643541711,12 +1798236110,1f4f0b70-8555-0135-aceb-0242ac115107,3731153502,3 +1798236110,9c874810-4aee-0135-0db7-0242ac112609,2359681938,39 +1798236110,1ab5c640-d69f-0134-5eaf-0242ac112c08,1602399807,8 +1798236110,15e90060-e9b6-0135-b09b-0242ac116708,1273780701,2 +1798236110,1c616af0-1598-0136-3ae6-0242ac116705,3898582076,77 +1798236110,7c4ed500-343e-0136-ed33-0242ac11670f,3023989445,46 +1798236110,51f4fb60-ad43-0135-8710-0242ac111506,2661118004,5 +1798236110,354af010-a296-0135-1b7a-0242ac111511,2633406505,6 +1798236110,716cb110-2ac1-0136-38c6-0242ac114a07,1724929401,2 +1798236110,ac5de680-fd31-0135-6d34-0242ac112c0c,2866829846,47 +1798236110,f56c2810-196c-0136-9146-0242ac112c08,3822465609,10 +1798236110,fa8d48e0-ef15-0135-63ba-0242ac115108,2190950146,47 +1798236110,a7afed20-315f-0136-75eb-0242ac115609,1101774301,2 +1798236110,3a010ea0-34cb-0136-ef16-0242ac11670f,2158279607,8 +1798236110,79c46940-c5bb-0134-4c68-0242ac11350a,1434315476,77 +1798236110,e74e2190-1242-0136-d13b-0242ac112109,1557561052,53 +1798236110,9964cb50-a0b5-0135-ce15-0242ac116705,1069121500,1 +1798236110,6722f370-d25a-0135-5236-0242ac112c0c,2705421513,14 +1798236110,270ab990-0bf6-0135-1532-0242ac114706,3890666707,8 +1798236110,282e6b80-9adb-0134-63c5-4652aab06781,344830303,4 +1798236110,5dfb2500-0181-0136-d451-0242ac115106,77248782,83 +1798236110,03b9ddb0-1f03-0135-84e5-0242ac113a0c,2869562681,82 +1798236110,99731c50-9134-0133-cf4d-4e2150ffec00,2766439201,2 +1798236110,6c83d750-dc81-0133-fea6-0ac40204d6f2,3456505718,19 +1798236110,78d803f0-b09e-0133-29a5-1e242c829d8f,1660058537,38 +1798236110,8b186ca0-7392-0135-5ee6-0242ac112c0e,395868389,90 +1798236110,c2370f70-12c0-0136-ba5c-0242ac11570b,3247998608,9 +1798236110,01e4ffe0-35a2-0136-daf1-0242ac115109,173248125,26 +1798236110,32e99410-7ea3-0135-6c29-0242ac112c08,354253164,65 +1798236110,17b2a170-6bfc-0135-b5de-0242ac116107,1430021409,10 +1798236110,cb0397d0-248a-0136-5046-0242ac11510c,2329021765,66 +1798236110,f0f5a3d0-a76b-0135-468c-0242ac116711,1088184101,2 +1798236110,98d3a5b0-c3c6-0135-023b-0242ac112c0c,3247091308,9 +1798236110,ed0c24a0-2fa0-0136-900c-0242ac114a08,1338546406,7 +1798236110,930e9400-debb-0135-9e99-0242ac116708,3349951407,8 +1798236110,f38576b0-0be8-4703-910c-ec0a3e3e143d,1236204308,9 +1798236110,7d7f3890-ade0-0135-cbd4-0242ac116709,785458735,36 +1798236110,6125ea50-f9b4-0133-175b-462a2ead201c,1347984806,7 +1798236110,bb183640-88bf-0135-9cb5-0242ac112c0a,3657938709,10 +1798236110,b375f580-1027-0136-b7f0-0242ac11570b,2770652853,54 +1798236110,04978220-9d56-0135-d28a-0242ac116707,4131647208,9 +1798236110,913b9d70-6b45-0135-f8ac-0242ac116707,396280501,2 +1798236110,dbd86e20-3047-0136-4241-0242ac11560c,3359825771,72 +1798236110,c267a144-b7fa-4e15-84ce-28e727108cb9,773865892,93 +1798236110,0e632b10-85b8-0135-7790-0242ac116713,254798809,10 +1798236110,3e6daa80-0882-0136-7489-0242ac11150e,2978792509,10 +1798236110,45efe4a0-2dbb-0136-29b3-0242ac111c0c,3710888176,77 +1798236110,777abe10-8146-0133-1444-4a67901999ea,3994038802,3 +1798236110,fffd0f40-faf9-0135-f0ee-0242ac115709,1344354907,8 +1798236110,3162b6f4-a1d9-44d0-9f01-2dd96df991e8,4239514405,6 +1798236110,608fbe20-7c74-0135-cb29-0242ac115114,1420116510,11 +1798236110,11f9bda0-5195-0133-0e28-1249bcced4b4,2651112488,89 +1798236110,5ba254b0-8f32-0135-d43e-0242ac115706,399669329,30 +1798236110,51170620-42b3-0134-1dba-26bba13de470,1210598132,33 +1798236110,d550d3c0-3d15-0134-adfb-62afb4234fc9,1983428163,64 +1798236110,97ae1100-ae38-0132-e48b-6a622d8c7933,1505233207,8 +1798236110,62521d20-0d98-0136-d62e-0242ac115108,3312147866,67 +1798236110,ff4c6a80-bb18-0135-b62a-0242ac115109,3765230997,98 +1798236110,33d83530-6bbe-0135-f9b3-0242ac116707,11161566,67 +1798236110,999012c0-fac4-0134-8fda-0242ac110906,1878218105,6 +1798236110,aba330d0-a154-0135-86a5-0242ac111506,219321929,30 +1798236110,7805bfc0-ec96-0135-8531-0242ac116708,77891628,29 +1798236110,7c91ecb0-adeb-0135-87f6-0242ac111506,3763820809,10 +1798236110,987d71e0-d8f3-0135-96c2-0242ac116708,2230888848,49 +1798236110,a838d150-2eb2-0136-c0e1-0242ac111511,74497083,84 +1798236110,c2618a30-ff8f-0135-f0d2-0242ac115709,2628934201,2 +1798236110,c4ddebc0-852f-0135-75d9-0242ac116713,3524936527,28 +1798236110,d87915c0-ffb8-0135-bdf1-0242ac116709,3836402341,42 +1798236110,85e579b3-7c64-4195-ae8f-fd6dac1e360d,3608335204,5 +1798236110,595ff940-91ab-0135-0811-0242ac115105,1320025004,5 +1798236110,b0326e60-2f89-0136-32e7-0242ac11150d,3060838002,3 +1798236110,28f83570-952c-0135-fa38-0242ac116706,3437406855,56 +1798236110,90703c80-8c43-0135-a693-0242ac112c0a,1536715600,1 +1798236110,59dd6480-b806-0135-b247-0242ac115109,3677741001,2 +1798236110,3bb31553-5098-4dd7-ba06-452acb6ca14f,1411027871,72 +1798236110,a141a440-4ad8-0135-d77f-0242ac11130b,1473972209,10 +1798236110,57084300-24aa-0136-7953-0242ac115605,2658772254,55 +1798236110,7ed28700-a0de-0135-8503-0242ac111506,1844684655,56 +1798236110,27e56a61-6cff-4b75-b201-76613ff258d6,4266150372,73 +1798236110,b9c13b60-2496-0136-78ef-0242ac115605,1118877445,46 +1798236110,f6bc09b0-237e-0136-a7bd-0242ac115605,1880292209,10 +1798236110,794e70a0-0857-0136-8341-0242ac115606,1555654542,43 +1798236110,c1c8c5f0-8782-0135-9bf5-0242ac112c0a,218032008,9 +1798236110,73d1f4c5-7b25-4cc0-8b39-57887103ce48,2755457203,4 +1798236110,0462ec80-5a57-0135-3213-0242ac113d0e,848622084,85 +1798236110,a54985f0-2907-0136-81ca-0242ac115605,3972654009,10 +1798236110,13882c43-4f68-439e-8020-2e4d28827834,2234641388,89 +1798236110,74da5890-aca8-0135-be92-0242ac112c15,3610770234,35 +1798236110,b2c067f0-8c5f-0135-a6d0-0242ac112c0a,1686807411,12 +1798236110,8c5fb120-044b-0136-620e-0242ac11560d,431990953,54 +1798236110,fd862090-f38e-0135-2e65-0242ac116709,2659729309,10 +1798236110,29e08536-c589-42a1-89e8-b01109b2537a,1541744707,8 +1798236110,b26cf24e-cf55-4b77-92c2-4eb336d4690d,697868001,2 +1798236110,85988380-30f6-0136-5edb-0242ac11150a,2609108400,1 +1798236110,e83b7310-88f7-0130-f7ab-123138068411,2622614305,6 +1798236110,7b811e00-080c-0136-6719-0242ac112c0c,2539754450,51 +1798236110,2ff7f9f0-d218-0135-bf76-0242ac11150e,3119905708,9 +1798236110,0632c1eb-c80f-4bd0-8fbd-bff1145377ff,2207796608,9 +1798236110,51271d80-b68d-0135-f1a1-0242ac115606,3549516606,7 +1798236110,672376f0-a5f7-0135-0a2c-0242ac111510,2470357224,25 +1798236110,5d03aed0-536e-0135-3e44-0242ac11130b,844196277,78 +1798236110,bca2d250-baaf-0135-d50e-0242ac11510d,2434648073,74 +1798236110,103af63b-4004-42e4-ad56-6f8913e750e5,2580650208,9 +1798236110,ce0c5860-2171-0135-0e0d-0242ac115b0d,2433224502,3 +1798236110,09d27ee0-ee53-0135-0d08-0242ac116708,498736385,86 +1798236110,d0b06270-b8cc-0135-6d14-0242ac112c0c,432398298,99 +1798236110,88e54d72-de96-4f3f-92d4-50ac0ce34ff7,2520497661,62 +1798236110,9d4d9090-1d5c-0135-cdac-0242ac113a0b,868163406,7 +1798236110,ecd0b150-298e-0136-1a85-0242ac111508,1284099667,68 +1798236110,ad7762a0-3c5d-0135-139b-0242ac11130c,3402311518,19 +1798236110,d51ac4c0-98d4-0135-061a-0242ac116705,3953986477,78 +1798236110,f3599600-0e72-0136-2b46-0242ac112c0e,187950293,94 +1798236110,e3145a80-d5e9-0135-c5ca-0242ac11150e,3696130397,98 +1798236110,edf86aa0-e90f-0135-3590-0242ac11210d,4215332607,8 +1798236110,c7daea30-d426-0135-fa68-0242ac114a09,3612789916,17 +1798236110,dd9a7d00-21f4-0135-8840-0242ac113a0c,3182164203,4 +1798236110,84262770-4612-4155-8ff1-401014c3214d,1718236606,7 +1798236110,bf43ccb0-bb85-0135-dc29-0242ac111c0a,3946652,53 +1798236110,a23a6a90-1fcd-0136-a203-0242ac115605,3076741336,37 +1798236110,eb02afb0-2ac6-0136-38e9-0242ac112108,1193867802,3 +1798236110,7b73ad90-094d-0134-c4eb-4aa2292d3778,3004277607,8 +1798236110,d1f25240-3005-0136-414b-0242ac11560c,2906162467,68 +1798236110,84d5f7a7-f273-4015-aa10-be4074b5727d,3321505988,89 +1798236110,062eb5e0-1e45-0136-f4d8-0242ac115605,3689021267,68 +1798236110,8ee81d40-e91f-0135-aef6-0242ac116708,3155670609,10 +1798236110,a7ce4c30-e1ff-0132-623c-56a39da5840b,99097304,5 +1798236110,4c5d60c0-4ec2-0135-ffeb-0242ac11130b,1170143009,10 +1798236110,4dd19960-1c0c-0136-20e7-0242ac115109,2703992704,5 +1798236110,a6ac31f0-debb-0134-515b-0242ac112c0a,121453544,45 +1798236110,25df3a90-9a63-0135-4de7-0242ac111508,633454005,6 +1798236110,ebda9840-000f-0136-e827-0242ac111c0a,2825402981,82 +1798236110,bbf0fbd1-6e7a-48c0-9cec-0f5598f4ba04,2410967207,8 +1798236110,869e88b0-f93d-0135-0a60-0242ac11510b,2169008151,52 +1798236110,b1e97e20-ee50-0135-dbc5-0242ac11570b,2543460854,55 +1798236110,5ddbf420-0289-0136-b625-0242ac111c0a,449014032,33 +1798236110,791797d0-1e39-0136-8156-0242ac11670d,2520341007,8 +1798236110,c37c8ed0-a35c-0135-2e3d-0242ac112c10,3478142585,86 +1798236110,48cbbc00-34e2-0136-ef52-0242ac11670f,2036354907,8 +1798236110,ae6729d0-7aaf-0135-c671-0242ac115114,3894014305,6 +1798236110,5f783bb1-ce89-4ea4-bd04-3554cb26c1bf,2464967461,62 +1798236110,3e041ebd-37e5-40ce-a666-58c7dc1a67ed,525610508,9 +1798236110,1c576150-1f4c-0136-a635-0242ac11210f,2004954501,2 +1798236110,f1ca9490-d1f1-0135-504e-0242ac112c0c,1087170712,13 +1798236110,6c0320a0-83ea-0135-ca29-0242ac115607,2742145408,9 +1798236110,39ef0dd0-1e66-0136-a3c0-0242ac11210f,4274279107,8 +1798236110,209c9db0-6f9c-0135-9cdb-0242ac11560d,3135572634,35 +1798236110,4edcc9e0-10b6-0136-0e3c-0242ac116705,1834313093,94 +1798236110,d0033590-1f1b-0136-dd51-0242ac111c08,108536005,6 +1798236110,c7b36780-e3dc-0135-4607-0242ac114a09,942256802,3 +1798236110,319a5370-e9b5-0135-5124-0242ac114a09,446147202,3 +1798236110,1e344c30-bd6d-0135-1bae-0242ac116707,2040110600,1 +1798236110,921f8842-95e5-48cb-a075-8f0012035d9e,2410810803,4 +1798236110,5230a800-232a-0136-ac49-0242ac11210f,1954430700,1 +1798236110,8a4b7f50-1895-0136-d4a3-0242ac111507,868229601,2 +1798236110,222110b0-abb1-0135-9832-0242ac111506,2632555951,52 +1798236110,7e5d99f0-ecc3-0135-83a6-0242ac11150c,817205227,28 +1798236110,04300ee0-2378-0135-933e-0242ac112612,3573425310,11 +1798236110,f456d0b0-fa0d-0135-f877-0242ac111508,2414324380,81 +1798236110,336acff0-293b-0136-4908-0242ac115709,3403504820,21 +1798236110,f3fc3d00-ac67-0135-1716-0242ac112c13,1874829376,77 +1798236110,12695120-e4e7-0135-6125-0242ac111c09,1914093700,1 +1798236110,e08b7400-f260-0135-1216-0242ac116708,1691526800,1 +1798236110,a6426602-7b7d-4161-b35d-ae76062f8318,1940459690,91 +1798236110,70c3ec00-998a-0134-37e4-2e45891bd616,866165905,6 +1798236110,5d6600b0-08e5-0136-d206-0242ac112111,2600261267,68 +1798236110,3e466870-d5d3-0135-c8cd-0242ac111c08,3407762278,79 +1798236110,86ddfeb0-a060-0135-f188-0242ac116707,1060382163,64 +1798236110,583a1150-e816-0135-32a4-0242ac11210d,3501152706,7 +1798236110,28005fb0-e319-0135-a40e-0242ac116708,4198593952,53 +1798236110,e5aaadd0-dce5-0134-4fe6-0242ac112c0a,221858716,17 +1798236110,410a6c00-3a61-0134-1538-4e313517aaae,1906440309,10 +1798236110,9a0a0480-7f72-0134-dcb5-22ba6851a88e,706184015,16 +1798236110,94498010-6b7a-0134-22c5-3668c7782a1f,679281505,6 +1798236110,ed26db2b-75bb-4d48-88ff-281863942d04,3569312206,7 +1798236110,69f86bb0-efd9-0135-0f80-0242ac116708,3737076204,5 +1798236110,e1a06f60-fa41-0135-69fe-0242ac112c0c,3656843100,1 +1798236110,384c0760-74a8-0135-a481-0242ac11560d,1025197702,3 +1798236110,b5523f50-45d1-0134-88fd-5217bc62becc,1792144820,21 +1798236110,00f507cd-9d22-4807-8b55-768b5dfcd8d3,1099003900,1 +1798236110,90c84160-02b9-0136-5e96-0242ac11560d,847594304,5 +1798236110,a9120000-1b37-0135-1f20-0242ac114708,3859611099,100 +1798236110,3446ce40-dec8-0135-3efe-0242ac114a09,1338027109,10 +1798236110,259f3ed0-e1f1-0135-5bac-0242ac111c09,2225017309,10 +1798236110,8b446d50-2f80-0136-ef93-0242ac11570b,1367362508,9 +1798236110,66914500-bce8-0135-3b66-0242ac111c08,761936807,8 +1798236110,d64d8ec0-21ea-0135-9925-0242ac113d09,3085917490,91 +1798236110,378632a0-fd0b-0135-6ccc-0242ac112c0c,3578129954,55 +1798236110,efe4e970-1a73-0136-46f9-0242ac11510f,58537174,75 +1798236110,e0072520-09bd-0136-049a-0242ac115108,2589359400,1 +1798236110,e642af10-bfb0-0134-9a60-0242ac112c06,181292395,96 +1798236110,e58de5e2-5b4e-45d0-9ed4-4c25632c010f,3003679300,1 +1798236110,3d906410-dd44-0135-b842-0242ac11150e,262126707,8 +1798236110,7bb50ad0-c236-0135-aeed-0242ac116705,1054152826,27 +1798236110,43cff690-2840-0135-8967-0242ac113a0a,2726646179,80 +1798236110,13cf63c0-2ef5-0136-c66c-0242ac112c0e,1434945973,74 +1798236110,360efb30-aac5-0135-ba57-0242ac111506,2560167009,10 +1798236110,8a99aae0-ab6f-0135-a741-0242ac112c0a,2279830908,9 +1798236110,084516b0-34f9-0136-22c0-0242ac112c0e,3105129154,55 +1798236110,824f7730-6ca3-4bde-a56b-02e3f002318e,827801001,2 +1798236110,8094e760-8279-0134-e7f4-4ad89ed6c5ab,167514708,9 +1798236110,3c560390-26bb-0136-55d4-0242ac11510c,1813649208,9 +1798236110,ad397820-7e6c-0135-cc4c-0242ac115114,3653071991,92 +1798236110,624cc830-55d3-0135-5ec4-0242ac113a14,4217237709,10 +1798236110,24c41870-ff20-0135-d017-0242ac115106,540751780,81 +1798236110,a0ed67c0-e800-0135-c75b-0242ac11150e,2846743496,97 +1798236110,2a624d8e-f29f-45a6-8940-71fb6a365fba,1929504402,3 +1798236110,e4ea6ba0-95a0-0135-6048-0242ac116706,1529890752,53 +1798236110,3a1eb930-8f25-0135-aa04-0242ac112c0a,2446032804,5 +1798236110,9255e780-1402-0136-e578-0242ac115108,2634751139,40 +1798236110,39b42a10-5dd1-0135-be3c-0242ac111311,3423508850,51 +1798236110,3b5c8cf0-23d9-0136-769d-0242ac115605,142205807,8 +1798236110,00236a10-a6a8-0135-182a-0242ac111508,4046291384,85 +1798236110,cab0f160-ce1a-0135-4d65-0242ac112c0c,898730109,10 +1798236110,78cdfa40-a69b-0135-e2f3-0242ac115109,1963789658,59 +1798236110,50653fe0-029c-0136-d052-0242ac115607,1857189000,1 +1798236110,f9705f00-0895-0136-026a-0242ac115108,2805212008,9 +1798236110,6040ab00-0d16-0136-2886-0242ac112c0e,2043876401,2 +1798236110,4c3d6fd0-e388-0135-9754-0242ac11570b,1427671457,58 +1798236110,366164d0-ffbe-0135-f1a7-0242ac115709,1997925005,6 +1798236110,86e16fd0-c04b-0135-a98a-0242ac11150b,216372061,62 +1798236110,c8059005-04f5-4531-8af7-c07638ddf005,3553425018,19 +1798236110,cd4e0fe0-a1ae-0135-d30e-0242ac116706,861719501,2 +1798236110,1faa9ca0-2a1f-0136-9578-0242ac115709,175412307,8 +1798236110,10e7d4c0-1b15-0136-bb7f-0242ac111c06,1063845334,35 +1798236110,54f60270-de09-0135-3dd6-0242ac114a09,1868376050,51 +1798236110,713eb4f0-f8b2-0135-59fd-0242ac111511,3494424292,93 +1798236110,9ee86110-9fcc-0135-a249-0242ac112c0e,2965044203,4 +1798236110,fd4d3810-477c-0135-d384-0242ac113a07,3002916602,3 +1798236110,dea24ec0-293f-0136-1971-0242ac111508,3018221297,98 +1798236110,ba9956b0-95a4-0135-605c-0242ac116706,1371058423,24 +1798236110,56582b00-2cef-0134-bcad-7a0a2fea0b65,706094486,87 +1798236110,f5e6a9f0-1e59-0136-f53e-0242ac115605,1780454434,35 +1798236110,cdc09f40-7641-0135-0175-0242ac11560f,3870301704,5 +1798236110,e9d41090-4aa0-0134-de66-5eb08e3d5a68,3032115328,29 +1798236110,ad02efc0-2fc8-0136-7c4b-0242ac112109,1152675209,10 +1798236110,e7d4d230-e6d0-0135-2f96-0242ac11210d,1490295024,25 +1798236110,35689270-0ed0-0136-0aec-0242ac116705,1357558716,17 +1798236110,e5e79070-a38e-0134-425c-722ada57d912,626881206,7 +1798236110,2f244d90-9a34-0135-ba48-0242ac11150e,2288308420,21 +1798236110,7cee6d20-956f-0135-183c-0242ac112c08,1589024301,2 +1798236110,33a3b6d0-10d7-0135-fcad-0242ac112c09,1481460503,4 +1798236110,f8ba3a40-4644-0134-d892-5e9fd0b78137,3156818227,28 +1798236110,8ce181a0-583d-0135-6162-0242ac113a14,606045701,2 +1798236110,dc8228d0-a274-0134-8787-06f5bc152eb6,2687617706,7 +1798236110,58352d10-fdf6-0134-a48a-0242ac110909,443959384,85 +1798236110,125575a0-6b71-0134-15c0-3a287d122eaa,815401605,6 +1798236110,979d0620-e901-0135-ca40-0242ac11150e,2445264272,73 +1798236110,84571fe0-019f-0134-794d-7e3d0c184eeb,993675729,30 +1798236110,06e346bb-f6d6-47a7-bc86-5658309bdff3,3711305922,23 +1798236110,8c133410-9cbd-0135-e32d-0242ac111506,1485777807,8 +1798236110,ef820310-2219-0134-041d-2a27770e5969,1890092509,10 +1798236110,fe702fc0-d2a8-0135-c0e1-0242ac11150e,7554505,6 +1798236110,476d5920-3415-0135-f29d-0242ac11130c,1333326809,10 +1798236110,b7f041c0-25a0-0136-7c0a-0242ac115605,1028923956,57 +1798236110,c20e6f80-9892-0134-5224-2ef76f8fda6a,2965326749,50 +1798236110,0b782ec0-23c2-0136-4de9-0242ac11510c,249060501,2 +1798236110,946fe4d0-ecc9-0135-5f3c-0242ac115108,670037390,91 +1798236110,7dd644a0-892b-0135-81af-0242ac116713,2737721774,75 +1798236110,de72d853-5bed-4c8a-ac83-43f422002ebf,1518423701,2 +1798236110,fe830b6b-c4cf-403c-8bb0-4620b51df92b,2887291111,12 +1798236110,63076dd0-5739-44a2-b062-3dc628d8d642,2750565595,96 +1798236110,a389ac00-d1f3-0135-be76-0242ac11150e,1427634108,9 +1798236110,24f636a0-29bf-0136-bd19-0242ac111c09,4123494007,8 +1798236110,60aa51d0-877d-0135-e2ff-0242ac115608,2082867838,39 +1798236110,bf1a9710-6641-0134-7390-4ae03f7ca85d,2715790165,66 +1798236110,387befb0-1610-4807-9991-d3634f0122cd,1194084504,5 +1798236110,2428aa40-19c4-0136-b818-0242ac111c06,4202962907,8 +1798236110,5cc7da00-cbb6-0133-f65b-02dd53fd4665,3229968966,67 +1798236110,02b09370-b761-0135-035e-0242ac116709,3802660405,6 +1798236110,a72780b0-2931-0136-48d3-0242ac115709,1814569913,14 +1798236110,2da1a1d0-2087-0136-5d80-0242ac115109,2234529701,2 +1798236110,d1894cc0-5406-0135-c485-0242ac113d09,3058440974,75 +1798236110,f40fc520-47a5-0135-431d-0242ac115b0b,2676286455,56 +1798236110,3fdd96d0-d462-0135-c7b6-0242ac111c08,2598396901,2 +1798236110,bd83f510-25ec-0136-7cac-0242ac115605,137618190,91 +1798236110,ad8c8fc0-0803-0136-547f-0242ac115709,721086345,46 +1798236110,94f88da0-2854-0135-fc84-0242ac112611,2053984003,4 +1798236110,f623ca40-ece5-0135-d90c-0242ac11570b,2683614503,4 +1798236110,3cf5cce0-b81d-0135-04e6-0242ac116709,3607141208,9 +1798236110,bf432238-c7f1-4362-a31f-e9635684185f,1382492507,8 +1798236110,6181a9a0-5e83-0135-8bed-0242ac115b0a,3539692004,5 +1798236110,752f15c0-5d7d-0135-bca0-0242ac111311,1255483373,74 +1798236110,9d0c0fc0-a5c4-0135-4c10-0242ac111508,3873582309,10 +1798236110,5cb745f0-2ace-0136-2199-0242ac11510c,3868395271,72 +1798236110,fd137090-8798-0135-9c37-0242ac112c0a,2161637053,54 +1798236110,37591110-132e-0136-d27b-0242ac112109,954396404,5 +1798236110,b6586ec0-a854-0135-a178-0242ac116709,3488234884,85 +1798236110,a08bb840-c2ec-0134-9e15-0242ac112c06,3677878042,43 +1798236110,87fbf950-595f-0133-2f02-3a58e26560f5,490299695,96 +1798236110,b293df70-e2f6-0135-a3f4-0242ac116708,1232450901,2 +1798236110,cfa084f0-1a82-0136-d6d6-0242ac112108,1967517660,61 +1798236110,870168c0-e773-0135-649d-0242ac111c09,167481691,92 +1798236110,8687caf0-e74e-0135-4ad4-0242ac114a09,3746211911,12 +1798236110,c5de5c30-ba97-0134-3578-0242ac112c08,4093163301,2 +1798236110,c60b6d20-2c2d-0135-54d5-0242ac115b0f,580322434,35 +1798236110,5f0b93e0-3039-0136-30a6-0242ac112c0e,3521463556,57 +1798236110,b6047d30-1795-0135-1734-0242ac11350a,358615573,74 +1798236110,41a7f620-8ba9-0134-da19-2e2a6c07a4f5,427697034,35 +1798236110,95c0dbd0-c18c-0135-7ecf-0242ac115106,2196988225,26 +1798236110,7d7d9820-e4cd-0135-c2a9-0242ac11150e,2157770003,4 +1798236110,a3278be0-d9bf-0134-cd9d-0242ac11350c,2767090537,38 +1798236110,8e4053c0-1777-0136-d336-0242ac111507,3973093008,9 +1798236110,de07dc30-9e18-0134-c75b-7e014426ff95,813381921,22 +1798236110,0c0ff0d6-c1a0-453d-80f5-ec151f68bcba,1720901557,58 +1798236110,879f9720-34ca-0136-ef15-0242ac11670f,3149476303,4 +1798236110,8750a3c0-47d9-0135-43e3-0242ac115b0b,2837487401,2 +1798236110,6d31a440-b0e9-0135-cea4-0242ac116709,1378248301,2 +1798236110,1ca38680-defe-0135-9ef5-0242ac116708,1971574909,10 +1798236110,5ed0c6b0-ade6-0135-dd14-0242ac115108,756787970,71 +1798236110,a1271500-f1c7-0135-6644-0242ac115108,2991257105,6 +1798236110,c1676a10-f495-0135-290b-0242ac115607,906464404,5 +1798236110,9335e3de-44b4-42fc-b8ad-33f572130d6a,2960501408,9 +1798236110,db859be0-bd9e-0135-a3e5-0242ac112c0c,4216738014,15 +1798236110,79f524f0-1fc9-0136-a1db-0242ac115605,2214707007,8 +1798236110,b0aa7940-10bd-0135-fc33-0242ac112c09,4030345815,16 +1798236110,18614c90-c03a-0134-9bc9-0242ac112c06,2978212864,65 +1798236110,51508e50-f6b8-0134-a6eb-0242ac110906,1219801204,5 +1798236110,c8053e70-2bad-0136-513b-0242ac115609,957312001,2 +1798236110,d5daf080-c7cf-0135-8691-0242ac11560a,1274551815,16 +1798236110,39f67630-1e3d-0136-f4ab-0242ac115605,272121763,64 +1798236110,1379a040-84d5-0133-5976-0669f3e10ac4,715307100,1 +1798236110,8ea05131-e8e8-4d16-831a-4ecd813c8f36,3037708608,9 +1798236110,8bb5d5c0-2156-0136-d409-0242ac11150f,2472124400,1 +1798236110,f3dd65c0-18a3-0135-7d6f-0242ac11350b,2280517903,4 +1798236110,cce837c1-0851-472a-b0a7-19747eccb545,858466893,94 +1798236110,eed98c00-5928-0135-e027-0242ac115b08,3040457405,6 +1798236110,b0e73100-b5ee-0135-adf3-0242ac115109,1352580693,94 +1798236110,36588f20-2e6e-0136-fa2a-0242ac115108,2343794507,8 +1798236110,a19337a0-2605-0136-7cd8-0242ac115605,2696805403,4 +1798236110,6879ce80-194b-0136-5f5c-0242ac114a07,79031707,8 +1798236110,32943080-0904-0135-1eed-0242ac11350a,1071280577,78 diff --git a/splitio/tests/splitCustomImpressionListener.json b/splitio/tests/splitCustomImpressionListener.json new file mode 100644 index 00000000..0dd47956 --- /dev/null +++ b/splitio/tests/splitCustomImpressionListener.json @@ -0,0 +1,46 @@ +{ + "splits": [ + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "iltest", + "seed": -1329591480, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1325599980, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "valid" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ] + } + ] + } + ], + "since": -1, + "till": 1461957424937 + } \ No newline at end of file diff --git a/splitio/tests/test_clients.py b/splitio/tests/test_clients.py index 29891c10..9ddd218f 100644 --- a/splitio/tests/test_clients.py +++ b/splitio/tests/test_clients.py @@ -17,15 +17,15 @@ from splitio import get_factory from splitio.clients import Client -from splitio.brokers import JSONFileBroker, LocalhostBroker, RedisBroker, \ +from splitio.brokers import JSONFileBroker, RedisBroker, LocalhostBroker, \ UWSGIBroker, randomize_interval, SelfRefreshingBroker from splitio.exceptions import TimeoutException from splitio.config import DEFAULT_CONFIG, MAX_INTERVAL, SDK_API_BASE_URL, \ EVENTS_API_BASE_URL from splitio.treatments import CONTROL from splitio.tests.utils import MockUtilsMixin -from splitio.managers import LocalhostSplitManager, \ - SelfRefreshingSplitManager, UWSGISplitManager, RedisSplitManager +from splitio.managers import SelfRefreshingSplitManager, UWSGISplitManager, RedisSplitManager + class RandomizeIntervalTests(TestCase, MockUtilsMixin): def setUp(self): @@ -67,7 +67,6 @@ def test_returned_function_returns_max_result(self): class SelfRefreshingBrokerInitTests(TestCase, MockUtilsMixin): - def setUp(self): self.build_sdk_api_mock = self.patch('splitio.brokers.SelfRefreshingBroker._build_sdk_api') self.build_split_fetcher_mock = self.patch( @@ -192,7 +191,7 @@ def test_if_event_flag_is_set_an_exception_is_not_raised(self): """Test that if the event flag is set, a TimeoutException is not raised""" try: SelfRefreshingBroker(self.some_api_key, config={'ready': 10}) - except: + except Exception: self.fail('An unexpected exception was raised') @@ -263,7 +262,7 @@ def setUp(self): 'redisHost': 'localhost', 'redisPort': 6379, 'redisDb': 0, - 'redisPassword':None, + 'redisPassword': None, 'redisSocketTimeout': None, 'redisSocketConnectTimeout': None, 'redisSocketKeepalive': None, @@ -282,12 +281,10 @@ def setUp(self): 'redisSslCertReqs': None, 'redisSslCaCerts': None, 'redisMaxConnections': None, - 'eventsPushRate' : 60, + 'eventsPushRate': 60, 'eventsQueueSize': 500, } - - self.client = SelfRefreshingBroker(self.some_api_key) def test_if_config_is_none_uses_default(self): @@ -439,8 +436,7 @@ def test_calls_self_updating_treatment_log_constructor(self): self.self_updating_treatment_log_mock.assert_called_once_with( self.client._sdk_api, max_count=self.client._max_impressions_log_size, - interval=self.client._impressions_interval, - listener=None + interval=self.client._impressions_interval ) def test_calls_async_treatment_log_constructor(self): @@ -488,10 +484,10 @@ def test_returns_async_treatment_log(self): self.assertEqual(self.aync_metrics_mock.return_value, self.client._build_metrics()) - class JSONFileBrokerIntegrationTests(TestCase): @classmethod def setUpClass(cls): + cls.some_config = mock.MagicMock() cls.segment_changes_file_name = os.path.join( os.path.dirname(__file__), 'segmentChanges.json' @@ -500,7 +496,8 @@ def setUpClass(cls): os.path.dirname(__file__), 'splitChanges.json' ) - cls.client = Client(JSONFileBroker(cls.segment_changes_file_name, cls.split_changes_file_name)) + cls.client = Client(JSONFileBroker(cls.some_config, cls.segment_changes_file_name, + cls.split_changes_file_name)) cls.on_treatment = 'on' cls.off_treatment = 'off' cls.some_key = 'some_key' @@ -1165,8 +1162,9 @@ def setUp(self): 'splitio.tests.test_clients.LocalhostBroker._build_split_fetcher') self.open_mock = self.patch_builtin('open') + self.some_config = mock.MagicMock() self.threading_mock = self.patch('threading.Thread') - self.broker = LocalhostBroker() + self.broker = LocalhostBroker(self.some_config) def test_skips_comment_lines(self): """Test that _parse_split_file skips comment lines""" @@ -1207,14 +1205,14 @@ def test_raises_value_error_if_ioerror_is_raised(self): class LocalhostBrokerOffTheGrid(TestCase): - ''' + """ Tests for LocalhostEnvironmentClient. Auto update config behaviour - ''' + """ def test_auto_update_splits(self): - ''' + """ Verifies that the split file is automatically re-parsed as soon as it's modified - ''' + """ with tempfile.NamedTemporaryFile(mode='w') as split_file: split_file.write('a_test_split off\n') split_file.flush() @@ -1231,12 +1229,14 @@ def test_auto_update_splits(self): self.assertEqual(client.get_treatment('x', 'a_test_split'), 'on') client.destroy() + class TestClientDestroy(TestCase): """ """ def setUp(self): self.some_api_key = mock.MagicMock() + self.some_config = mock.MagicMock() def test_self_refreshing_destroy(self): broker = SelfRefreshingBroker(self.some_api_key) @@ -1248,7 +1248,7 @@ def test_self_refreshing_destroy(self): self.assertEqual(manager.split_names(), []) def test_redis_destroy(self): - broker = RedisBroker(self.some_api_key) + broker = RedisBroker(self.some_api_key, self.some_config) client = Client(broker) manager = RedisSplitManager(broker) client.destroy() @@ -1264,4 +1264,3 @@ def test_uwsgi_destroy(self): self.assertEqual(client.get_treatment('asd', 'asd'), CONTROL) self.assertEqual(manager.splits(), []) self.assertEqual(manager.split_names(), []) - diff --git a/splitio/tests/test_impression_listener.py b/splitio/tests/test_impression_listener.py new file mode 100644 index 00000000..8c8b50ba --- /dev/null +++ b/splitio/tests/test_impression_listener.py @@ -0,0 +1,196 @@ +try: + from unittest import mock +except ImportError: + # Python 2 + import mock + +from splitio.config import SDK_VERSION, DEFAULT_CONFIG + +from os.path import dirname, join +from json import load +from unittest import TestCase + +from splitio.clients import Client +from splitio.redis_support import (RedisSplitCache, get_redis) +from splitio.brokers import RedisBroker +from splitio.impressions import (Impression, ImpressionListener, ImpressionListenerWrapper, + ImpressionListenerException) +from splitio import get_factory + + +class ImpressionListenerClient(ImpressionListener): + def log_impression(self, data): + self._data_logged = data + + def get_impression(self): + return self._data_logged + + +class ImpressionListenerClientWithException(ImpressionListener): + def log_impression(self, data): + raise Exception('Simulate exception.') + + +class ImpressionListenerClientEmpty: + pass + + +class CustomImpressionListenerTestOnRedis(TestCase): + def setUp(self): + self._some_config = mock.MagicMock() + self._split_changes_file_name = join(dirname(__file__), + 'splitCustomImpressionListener.json') + + with open(self._split_changes_file_name) as f: + self._json = load(f) + split_definition = self._json['splits'][0] + split_name = split_definition['name'] + + self._redis = get_redis({'redisPrefix': 'customImpressionListenerTest'}) + + self._redis_split_cache = RedisSplitCache(self._redis) + self._redis_split_cache.add_split(split_name, split_definition) + self._client = Client(RedisBroker(self._redis, self._some_config)) + + self.some_feature = 'feature_0' + self.some_impression_0 = Impression(matching_key=mock.MagicMock(), + feature_name=self.some_feature, + treatment=mock.MagicMock(), + label=mock.MagicMock(), + change_number=mock.MagicMock(), + bucketing_key=mock.MagicMock(), + time=mock.MagicMock()) + + def test_client_raise_attribute_error(self): + client_1 = Client(RedisBroker(self._redis, self._some_config), + True, ImpressionListenerClientEmpty()) + + with self.assertRaises(AttributeError): + client_1._impression_listener.log_impression(self.some_impression_0) + + def test_send_data_to_client(self): + impression_client = ImpressionListenerClient() + impression_wrapper = ImpressionListenerWrapper(impression_client) + + impression_wrapper.log_impression(self.some_impression_0) + + self.assertIn('impression', impression_client._data_logged) + impression_logged = impression_client._data_logged['impression'] + self.assertIsInstance(impression_logged, Impression) + self.assertDictEqual({ + 'impression': { + 'keyName': self.some_impression_0.matching_key, + 'treatment': self.some_impression_0.treatment, + 'time': self.some_impression_0.time, + 'changeNumber': self.some_impression_0.change_number, + 'label': self.some_impression_0.label, + 'bucketingKey': self.some_impression_0.bucketing_key + } + }, { + 'impression': { + 'keyName': impression_logged.matching_key, + 'treatment': impression_logged.treatment, + 'time': impression_logged.time, + 'changeNumber': impression_logged.change_number, + 'label': impression_logged.label, + 'bucketingKey': impression_logged.bucketing_key + } + }) + + self.assertIn('instance-id', impression_client._data_logged) + self.assertEqual(impression_client._data_logged['instance-id'], + DEFAULT_CONFIG['splitSdkMachineIp']) + + self.assertIn('sdk-language-version', impression_client._data_logged) + self.assertEqual(impression_client._data_logged['sdk-language-version'], SDK_VERSION) + + self.assertIn('attributes', impression_client._data_logged) + + def test_client_throwing_exception_in_listener(self): + impressionListenerClient = ImpressionListenerClientWithException() + + config = { + 'ready': 180000, + 'impressionListener': impressionListenerClient, + 'redisDb': 0, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + + def test_client(self): + impressionListenerClient = ImpressionListenerClient() + + config = { + 'ready': 180000, + 'impressionListener': impressionListenerClient, + 'redisDb': 0, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_client_without_impression_listener(self): + config = { + 'ready': 180000, + 'redisDb': 0, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_client_when_impression_listener_is_none(self): + config = { + 'ready': 180000, + 'redisDb': 0, + 'impressionListener': None, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_client_with_empty_impression_listener(self): + config = { + 'ready': 180000, + 'redisDb': 0, + 'impressionListener': ImpressionListenerClientEmpty(), + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_throwing_exception_in_listener(self): + impression_exception = ImpressionListenerClientWithException() + + impression_wrapper = ImpressionListenerWrapper(impression_exception) + + with self.assertRaises(ImpressionListenerException): + impression_wrapper.log_impression(self.some_impression_0) diff --git a/splitio/tests/test_impressions.py b/splitio/tests/test_impressions.py index 4dffb6e2..ad7c2ec8 100644 --- a/splitio/tests/test_impressions.py +++ b/splitio/tests/test_impressions.py @@ -514,46 +514,3 @@ def test_log_doesnt_raise_exceptions_if_submit_does(self): # try: # except: # self.fail('Unexpected exception raised') - - -class TestImpressionListener(TestCase): - """ - Tests for impression listener in "in-memory" and "uwsgi-cache" operation - modes - """ - - def test_inmemory_impression_listener(self): - some_api = mock.MagicMock() - listener = mock.MagicMock() - treatment_log = SelfUpdatingTreatmentLog(some_api, listener=listener) - with mock.patch( - 'splitio.impressions.build_impressions_data', - return_value=[1, 2, 3] - ): - treatment_log._update_evictions('some_feature', []) - - listener.assert_called_once_with([1, 2, 3]) - - def test_uwsgi_impression_listener(self): - impressions_cache = mock.MagicMock() - impressions = { - 'testName': 'someTest', - 'keyImpressions': [1, 2, 3] - } - - impressions_cache.fetch_all_and_clear.return_value = { - 'someTest': [1, 2, 3] - } - some_api = mock.MagicMock() - listener = mock.MagicMock() - with mock.patch( - 'splitio.tasks.build_impressions_data', - return_value=impressions - ): - report_impressions( - impressions_cache, - some_api, - listener=listener - ) - - listener.assert_called_with({'impressions': impressions}) diff --git a/splitio/tests/test_input_validator.py b/splitio/tests/test_input_validator.py new file mode 100644 index 00000000..f4f5fc0f --- /dev/null +++ b/splitio/tests/test_input_validator.py @@ -0,0 +1,445 @@ +"""Unit tests for the input_validator module""" +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +try: + from unittest import mock +except ImportError: + # Python 2 + import mock + +from unittest import TestCase +from splitio.brokers import RedisBroker, SelfRefreshingBroker, UWSGIBroker +from splitio.clients import Client +from splitio.treatments import CONTROL +from splitio.redis_support import get_redis +from splitio.splits import Split +from splitio.impressions import Label +from splitio import input_validator +from splitio.managers import RedisSplitManager, SelfRefreshingSplitManager, UWSGISplitManager +from splitio.key import Key +from splitio.uwsgi import UWSGICacheEmulator + + +class TestInputSanitizationGetTreatment(TestCase): + + def setUp(self): + self.some_config = mock.MagicMock() + self.some_api_key = mock.MagicMock() + self.redis = get_redis({'redisPrefix': 'test'}) + self.client = Client(RedisBroker(self.redis, self.some_config)) + self.client._broker.fetch_feature = mock.MagicMock(return_value=Split( + "some_feature", + 0, + False, + "default_treatment", + "user", + "ACTIVE", + 123 + )) + + self.client._build_impression = mock.MagicMock() + + input_validator._LOGGER.error = mock.MagicMock() + self.logger_error = input_validator._LOGGER.error + input_validator._LOGGER.warning = mock.MagicMock() + self.logger_warning = input_validator._LOGGER.warning + + def test_get_treatment_with_null_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + None, "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: key cannot be None.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_treatment_with_number_key(self): + self.assertEqual("default_treatment", self.client.get_treatment( + 12345, "some_feature")) + self.logger_warning \ + .assert_called_once_with("get_treatment: key 12345 is not of type string, converting.") + + def test_get_treatment_with_bool_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + True, "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: key True has to be of type string " + "or object Key.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_treatment_with_array_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + [], "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: key [] has to be of type string " + "or object Key.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_treatment_with_null_feature_name(self): + self.assertEqual(CONTROL, self.client.get_treatment( + "some_key", None)) + self.logger_error \ + .assert_called_once_with("get_treatment: feature_name cannot be None.") + self.client._build_impression.assert_called_once_with( + "some_key", None, CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_treatment_with_numeric_feature_name(self): + self.assertEqual(CONTROL, self.client.get_treatment( + "some_key", 12345)) + self.logger_error \ + .assert_called_once_with("get_treatment: feature_name 12345 has to be of type string.") + self.client._build_impression.assert_called_once_with( + "some_key", None, CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_treatment_with_bool_feature_name(self): + self.assertEqual(CONTROL, self.client.get_treatment( + "some_key", True)) + self.logger_error \ + .assert_called_once_with("get_treatment: feature_name True has to be of type string.") + self.client._build_impression.assert_called_once_with( + "some_key", None, CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_treatment_with_array_feature_name(self): + self.assertEqual(CONTROL, self.client.get_treatment( + "some_key", [])) + self.logger_error \ + .assert_called_once_with("get_treatment: feature_name [] has to be of type string.") + self.client._build_impression.assert_called_once_with( + "some_key", None, CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_treatment_with_valid_inputs(self): + self.assertEqual("default_treatment", self.client.get_treatment( + "some_key", "some_feature")) + self.logger_error.assert_not_called() + self.logger_warning.assert_not_called() + + def test_get_tratment_with_null_matching_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + Key(None, "bucketing_key"), "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: Key should be an object with " + "bucketingKey and matchingKey with valid string properties.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_tratment_with_empty_matching_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + Key("", "bucketing_key"), "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: matching_key must not be empty.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_tratment_with_bool_matching_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + Key(True, "bucketing_key"), "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: matching_key True has to be of type string.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_tratment_with_array_matching_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + Key([], "bucketing_key"), "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: matching_key [] has to be of type string.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_tratment_with_numeric_matching_key(self): + self.assertEqual("default_treatment", self.client.get_treatment( + Key(12345, "bucketing_key"), "some_feature")) + self.logger_warning \ + .assert_called_once_with("get_treatment: matching_key 12345 is not of type string, " + "converting.") + + def test_get_tratment_with_null_bucketing_key(self): + self.assertEqual("default_treatment", self.client.get_treatment( + Key("matching_key", None), "some_feature")) + self.logger_warning \ + .assert_called_once_with("get_treatment: Key object should have bucketingKey set.") + + def test_get_tratment_with_bool_bucketing_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + Key("matching_key", True), "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: bucketing_key True has to be of type string.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_tratment_with_array_bucketing_key(self): + self.assertEqual(CONTROL, self.client.get_treatment( + Key("matching_key", []), "some_feature")) + self.logger_error \ + .assert_called_once_with("get_treatment: bucketing_key [] has to be of type string.") + self.client._build_impression.assert_called_once_with( + None, "some_feature", CONTROL, Label.EXCEPTION, 0, None, mock.ANY + ) + + def test_get_tratment_with_numeric_bucketing_key(self): + self.assertEqual("default_treatment", self.client.get_treatment( + Key("matching_key", 12345), "some_feature")) + self.logger_warning \ + .assert_called_once_with("get_treatment: bucketing_key 12345 is not of type string, " + "converting.") + + +class TestInputSanitizationTrack(TestCase): + + def setUp(self): + self.some_config = mock.MagicMock() + self.some_api_key = mock.MagicMock() + self.redis = get_redis({'redisPrefix': 'test'}) + self.client = Client(RedisBroker(self.redis, self.some_config)) + + input_validator._LOGGER.error = mock.MagicMock() + self.logger_error = input_validator._LOGGER.error + input_validator._LOGGER.warning = mock.MagicMock() + self.logger_warning = input_validator._LOGGER.warning + + def test_track_with_null_key(self): + self.assertEqual(False, self.client.track( + None, "traffic_type", "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: key cannot be None.") + + def test_track_with_numeric_key(self): + self.assertEqual(True, self.client.track( + 12345, "traffic_type", "event_type", 1)) + self.logger_warning \ + .assert_called_once_with("track: key 12345 is not of type string," + " converting.") + + def test_track_with_bool_key(self): + self.assertEqual(False, self.client.track( + True, "traffic_type", "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: key True has to be of type string.") + + def test_track_with_array_key(self): + self.assertEqual(False, self.client.track( + [], "traffic_type", "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: key [] has to be of type string.") + + def test_track_with_null_traffic_type(self): + self.assertEqual(False, self.client.track( + "some_key", None, "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: traffic_type cannot be None.") + + def test_track_with_bool_traffic_type(self): + self.assertEqual(False, self.client.track( + "some_key", True, "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: traffic_type True has to be of type string.") + + def test_track_with_array_traffic_type(self): + self.assertEqual(False, self.client.track( + "some_key", [], "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: traffic_type [] has to be of type string.") + + def test_track_with_numeric_traffic_type(self): + self.assertEqual(False, self.client.track( + "some_key", 12345, "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: traffic_type 12345 has to be of type string.") + + def test_track_with_empty_traffic_type(self): + self.assertEqual(False, self.client.track( + "some_key", "", "event_type", 1)) + self.logger_error \ + .assert_called_once_with("track: traffic_type must not be empty.") + + def test_track_with_null_event_type(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", None, 1)) + self.logger_error \ + .assert_called_once_with("track: event_type cannot be None.") + + def test_track_with_bool_event_type(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", True, 1)) + self.logger_error \ + .assert_called_once_with("track: event_type True has to be of type string.") + + def test_track_with_array_event_type(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", [], 1)) + self.logger_error \ + .assert_called_once_with("track: event_type [] has to be of type string.") + + def test_track_with_numeric_event_type(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", 12345, 1)) + self.logger_error \ + .assert_called_once_with("track: event_type 12345 has to be of type string.") + + def test_track_with_event_type_does_not_conform_reg_exp(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", "@@", 1)) + self.logger_error \ + .assert_called_once_with("track: event_type must adhere to the regular " + "expression [a-zA-Z0-9][-_\\.a-zA-Z0-9]{0,62}.") + + def test_track_with_null_value(self): + self.assertEqual(True, self.client.track( + "some_key", "traffic_type", "event_type", None)) + self.logger_error.assert_not_called() + self.logger_warning.assert_not_called() + + def test_track_with_string_value(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", "event_type", "test")) + self.logger_error \ + .assert_called_once_with("track: value must be a number.") + + def test_track_with_bool_value(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", "event_type", True)) + self.logger_error \ + .assert_called_once_with("track: value must be a number.") + + def test_track_with_array_value(self): + self.assertEqual(False, self.client.track( + "some_key", "traffic_type", "event_type", [])) + self.logger_error \ + .assert_called_once_with("track: value must be a number.") + + def test_track_with_int_value(self): + self.assertEqual(True, self.client.track( + "some_key", "traffic_type", "event_type", 1)) + self.logger_error.assert_not_called() + self.logger_warning.assert_not_called() + + def test_track_with_float_value(self): + self.assertEqual(True, self.client.track( + "some_key", "traffic_type", "event_type", 1.3)) + self.logger_error.assert_not_called() + self.logger_warning.assert_not_called() + + +class TestInputSanitizationRedisManager(TestCase): + + def setUp(self): + self.some_config = mock.MagicMock() + self.some_api_key = mock.MagicMock() + self.redis = get_redis({'redisPrefix': 'test'}) + self.client = Client(RedisBroker(self.redis, self.some_config)) + + self.manager = RedisSplitManager(self.client._broker) + + input_validator._LOGGER.error = mock.MagicMock() + self.logger_error = input_validator._LOGGER.error + + def test_manager_with_null_feature_name(self): + self.assertEqual(None, self.manager.split(None)) + self.logger_error \ + .assert_called_once_with("split: feature_name cannot be None.") + + def test_manager_with_bool_feature_name(self): + self.assertEqual(None, self.manager.split(True)) + self.logger_error \ + .assert_called_once_with("split: feature_name True has to be of type string.") + + def test_manager_with_array_feature_name(self): + self.assertEqual(None, self.manager.split([])) + self.logger_error \ + .assert_called_once_with("split: feature_name [] has to be of type string.") + + def test_manager_with_numeric_feature_name(self): + self.assertEqual(None, self.manager.split(12345)) + self.logger_error \ + .assert_called_once_with("split: feature_name 12345 has to be of type string.") + + def test_manager_with_valid_feature_name(self): + self.assertEqual(None, self.manager.split("valid_feature_name")) + self.logger_error.assert_not_called() + + +class TestInputSanitizationSelfRefreshingManager(TestCase): + + def setUp(self): + self.some_api_key = mock.MagicMock() + self.broker = SelfRefreshingBroker(self.some_api_key) + self.client = Client(self.broker) + self.manager = SelfRefreshingSplitManager(self.broker) + + input_validator._LOGGER.error = mock.MagicMock() + self.logger_error = input_validator._LOGGER.error + + def test_manager_with_null_feature_name(self): + self.assertEqual(None, self.manager.split(None)) + self.logger_error \ + .assert_called_once_with("split: feature_name cannot be None.") + + def test_manager_with_bool_feature_name(self): + self.assertEqual(None, self.manager.split(True)) + self.logger_error \ + .assert_called_once_with("split: feature_name True has to be of type string.") + + def test_manager_with_array_feature_name(self): + self.assertEqual(None, self.manager.split([])) + self.logger_error \ + .assert_called_once_with("split: feature_name [] has to be of type string.") + + def test_manager_with_numeric_feature_name(self): + self.assertEqual(None, self.manager.split(12345)) + self.logger_error \ + .assert_called_once_with("split: feature_name 12345 has to be of type string.") + + def test_manager_with_valid_feature_name(self): + self.assertEqual(None, self.manager.split("valid_feature_name")) + self.logger_error.assert_not_called() + + +class TestInputSanitizationUWSGIManager(TestCase): + + def setUp(self): + self.some_api_key = mock.MagicMock() + self.uwsgi = UWSGICacheEmulator() + self.broker = UWSGIBroker(self.uwsgi, {'eventsQueueSize': 30}) + self.client = Client(self.broker) + self.manager = UWSGISplitManager(self.broker) + + input_validator._LOGGER.error = mock.MagicMock() + self.logger_error = input_validator._LOGGER.error + + def test_manager_with_null_feature_name(self): + self.assertEqual(None, self.manager.split(None)) + self.logger_error \ + .assert_called_once_with("split: feature_name cannot be None.") + + def test_manager_with_bool_feature_name(self): + self.assertEqual(None, self.manager.split(True)) + self.logger_error \ + .assert_called_once_with("split: feature_name True has to be of type string.") + + def test_manager_with_array_feature_name(self): + self.assertEqual(None, self.manager.split([])) + self.logger_error \ + .assert_called_once_with("split: feature_name [] has to be of type string.") + + def test_manager_with_numeric_feature_name(self): + self.assertEqual(None, self.manager.split(12345)) + self.logger_error \ + .assert_called_once_with("split: feature_name 12345 has to be of type string.") + + def test_manager_with_valid_feature_name(self): + self.assertEqual(None, self.manager.split("valid_feature_name")) + self.logger_error.assert_not_called() diff --git a/splitio/tests/test_redis_cache.py b/splitio/tests/test_redis_cache.py index 78f841fa..3389300d 100644 --- a/splitio/tests/test_redis_cache.py +++ b/splitio/tests/test_redis_cache.py @@ -78,10 +78,12 @@ def sadd(self, name, *values): """ raise Exception('ReadOnlyError') + class RedisReadOnlyTest(TestCase, MockUtilsMixin): def setUp(self): + self._some_config = mock.MagicMock() self._split_changes_file_name = join(dirname(__file__), 'splitChangesReadOnly.json') - + with open(self._split_changes_file_name) as f: self._json = load(f) split_definition = self._json['splits'][0] @@ -92,7 +94,7 @@ def setUp(self): self._mocked_redis = ReadOnlyRedisMock(self._redis) self._redis_split_cache = RedisSplitCache(self._redis) self._redis_split_cache.add_split(split_name, split_definition) - self._client = Client(RedisBroker(self._mocked_redis)) + self._client = Client(RedisBroker(self._mocked_redis, self._some_config)) self._impression = mock.MagicMock() self._start = mock.MagicMock() @@ -101,4 +103,4 @@ def setUp(self): def test_redis_read_only_mode(self): self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1'), 'on') self.assertEqual(self._client.get_treatment('invalid', 'test_read_only_1'), 'off') - self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1_invalid'), 'control') \ No newline at end of file + self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1_invalid'), 'control') diff --git a/splitio/tests/test_splits.py b/splitio/tests/test_splits.py index f477e67f..229de2e4 100644 --- a/splitio/tests/test_splits.py +++ b/splitio/tests/test_splits.py @@ -1017,10 +1017,11 @@ class TrafficAllocationTests(TestCase): def setUp(self): ''' ''' + self.some_config = mock.MagicMock() redis = get_redis({}) segment_cache = RedisSegmentCache(redis) split_parser = RedisSplitParser(segment_cache) - self._client = Client(RedisBroker(redis)) + self._client = Client(RedisBroker(redis, self.some_config)) self._splitObjects = {} diff --git a/splitio/tests/test_splitters.py b/splitio/tests/test_splitters.py index 2c1d16fe..06426f7d 100644 --- a/splitio/tests/test_splitters.py +++ b/splitio/tests/test_splitters.py @@ -154,6 +154,16 @@ def test_murmur_with_non_alpha_numeric_sample_data(self): seed, key, hash_, bucket = line.split(',') self.assertEqual(int(hash_), hashfn(key, int(seed))) + def test_murmur_with_custom_uuids(self): + """ + Tests murmur32 hash against expected values using non alphanumeric values + """ + hashfn = _HASH_ALGORITHMS[HashAlgorithm.MURMUR] + with io.open(join(dirname(__file__), 'murmur3-custom-uuids.csv'), 'r', encoding='utf-8') as f: + for line in f: + seed, key, hash_, bucket = line.split(',') + self.assertEqual(int(hash_), hashfn(key, int(seed))) + class SplitterGetBucketUnitTests(TestCase): def setUp(self): diff --git a/splitio/uwsgi.py b/splitio/uwsgi.py index 47baaa2a..b9a0810e 100644 --- a/splitio/uwsgi.py +++ b/splitio/uwsgi.py @@ -173,8 +173,7 @@ def uwsgi_report_impressions(user_config): sdk_api = api_factory(config) report_impressions( impressions_cache, - sdk_api, - user_config.get('impression_listener')) + sdk_api) time.sleep(seconds) except: diff --git a/splitio/version.py b/splitio/version.py index 98d739c9..9f62d768 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '6.0.0' +__version__ = '6.1.0'