Skip to content

Development #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Feb 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
2.2.0
- Added uwsgi cache support
- Fixed HTTP status code exceptions
2.1.0
- Added enabled labels
- Added impressions by sdk and version including bucketing key
2.0.5
- Added SDK Factory Method with Manager API and Sdk Client
- Added Bucketing key support
- Added Bucketing key support
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
tests_require=tests_require,
extras_require={
'test': tests_require,
'redis': ['redis>=2.10.5', 'jsonpickle>=0.9.3']
'redis': ['redis>=2.10.5', 'jsonpickle>=0.9.3'],
'uwsgi': ['uwsgi>=2.0.0', 'jsonpickle>=0.9.3']
},
setup_requires=['nose'],
classifiers=[
Expand Down
14 changes: 12 additions & 2 deletions splitio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals

import requests
import logging

from splitio.config import SDK_API_BASE_URL, EVENTS_API_BASE_URL, SDK_VERSION

Expand Down Expand Up @@ -38,6 +39,7 @@ def __init__(self, api_key, sdk_api_base_url=None, events_api_base_url=None,
:param read_timeout: The HTTP read timeout. Default: 1000 (seconds)
:type read_timeout: float
"""
self._logger = logging.getLogger(self.__class__.__name__)
self._api_key = api_key
self._sdk_api_url_base = sdk_api_base_url if sdk_api_base_url is not None \
else SDK_API_BASE_URL
Expand Down Expand Up @@ -83,19 +85,27 @@ def _build_headers(self):

return headers

def _logHttpError(self, response):
if response.status_code < 200 or response.status_code >= 400 :
respJson = response.json()
if 'message' in respJson:
self._logger.error("HTTP Error (status: %s) connecting with split servers: %s" % (response.status_code, respJson['message']))
else:
self._logger.error("HTTP Error connecting with split servers")

def _get(self, url, params):
headers = self._build_headers()

response = requests.get(url, params=params, headers=headers, timeout=self._timeout)
response.raise_for_status()
self._logHttpError(response)

return response.json()

def _post(self, url, data):
headers = self._build_headers()

response = requests.post(url, json=data, headers=headers, timeout=self._timeout)
response.raise_for_status()
self._logHttpError(response)

return response.status_code

Expand Down
219 changes: 165 additions & 54 deletions splitio/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from splitio.config import DEFAULT_CONFIG, MAX_INTERVAL, parse_config_file
from splitio.treatments import CONTROL

from splitio.uwsgi import (UWSGISplitCache, UWSGIImpressionsCache, UWSGIMetricsCache, get_uwsgi)


class Key(object):
def __init__(self, matching_key, bucketing_key):
Expand Down Expand Up @@ -195,6 +197,59 @@ def random_interval():

return random_interval

class JSONFileClient(Client):
def __init__(self, segment_changes_file_name, split_changes_file_name):
"""
A Client implementation that uses responses from the segmentChanges and splitChanges
resources to provide access to splits. It is intended to be used on integration
tests only.

:param segment_changes_file_name: The name of the file with the segmentChanges response
:type segment_changes_file_name: str
:param split_changes_file_name: The name of the file with the splitChanges response
:type split_changes_file_name: str
"""
super(JSONFileClient, self).__init__()
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()
self._treatment_log = TreatmentLog()
self._metrics = Metrics()

def _build_split_fetcher(self):
"""
Build the json backed split fetcher
:return: The json backed split fetcher
:rtype: SelfRefreshingSplitFetcher
"""
segment_fetcher = JSONFileSegmentFetcher(self._segment_changes_file_name)
split_parser = SplitParser(segment_fetcher)
split_fetcher = JSONFileSplitFetcher(self._split_changes_file_name, split_parser)

return split_fetcher

def get_split_fetcher(self):
"""
Get the split fetcher implementation for the client.
:return: The split fetcher
:rtype: SplitFetcher
"""
return self._split_fetcher

def get_treatment_log(self):
"""Get the treatment log implementation.
:return: The treatment log implementation.
:rtype: TreatmentLog
"""
return self._treatment_log

def get_metrics(self):
"""Get the metrics implementation.
:return: The metrics implementation.
:rtype: Metrics
"""
return self._metrics


class SelfRefreshingClient(Client):
def __init__(self, api_key, config=None, sdk_api_base_url=None, events_api_base_url=None):
Expand Down Expand Up @@ -350,60 +405,6 @@ def get_metrics(self):
return self._metrics


class JSONFileClient(Client):
def __init__(self, segment_changes_file_name, split_changes_file_name):
"""
A Client implementation that uses responses from the segmentChanges and splitChanges
resources to provide access to splits. It is intended to be used on integration
tests only.

:param segment_changes_file_name: The name of the file with the segmentChanges response
:type segment_changes_file_name: str
:param split_changes_file_name: The name of the file with the splitChanges response
:type split_changes_file_name: str
"""
super(JSONFileClient, self).__init__()
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()
self._treatment_log = TreatmentLog()
self._metrics = Metrics()

def _build_split_fetcher(self):
"""
Build the json backed split fetcher
:return: The json backed split fetcher
:rtype: SelfRefreshingSplitFetcher
"""
segment_fetcher = JSONFileSegmentFetcher(self._segment_changes_file_name)
split_parser = SplitParser(segment_fetcher)
split_fetcher = JSONFileSplitFetcher(self._split_changes_file_name, split_parser)

return split_fetcher

def get_split_fetcher(self):
"""
Get the split fetcher implementation for the client.
:return: The split fetcher
:rtype: SplitFetcher
"""
return self._split_fetcher

def get_treatment_log(self):
"""Get the treatment log implementation.
:return: The treatment log implementation.
:rtype: TreatmentLog
"""
return self._treatment_log

def get_metrics(self):
"""Get the metrics implementation.
:return: The metrics implementation.
:rtype: Metrics
"""
return self._metrics


class LocalhostEnvironmentClient(Client):
_COMMENT_LINE_RE = compile('^#.*$')
_DEFINITION_LINE_RE = compile('^(?<![^#])(?P<feature>[\w_]+)\s+(?P<treatment>[\w_]+)$')
Expand Down Expand Up @@ -541,6 +542,62 @@ def get_metrics(self):
"""
return self._metrics

class UWSGIClient(Client):
def __init__(self, uwsgi, config=None):
"""
A Client implementation that consumes data from uwsgi cache framework. The config parameter
is a dictionary that allows you to control the behaviour of the client.

:param config: The configuration dictionary
:type config: dict
"""
labels_enabled = True
if config is not None and 'labelsEnabled' in config:
labels_enabled = config['labelsEnabled']

super(UWSGIClient, self).__init__(labels_enabled)

split_cache = UWSGISplitCache(uwsgi)
split_fetcher = CacheBasedSplitFetcher(split_cache)

impressions_cache = UWSGIImpressionsCache(uwsgi)
delegate_treatment_log = CacheBasedTreatmentLog(impressions_cache)
treatment_log = AsyncTreatmentLog(delegate_treatment_log)

metrics_cache = UWSGIMetricsCache(uwsgi)
delegate_metrics = CacheBasedMetrics(metrics_cache)
metrics = AsyncMetrics(delegate_metrics)

self._split_fetcher = split_fetcher
self._treatment_log = treatment_log
self._metrics = metrics


def get_split_fetcher(self):
"""
Get the split fetcher implementation for the client.
:return: The split fetcher
:rtype: SplitFetcher
"""
return self._split_fetcher

def get_treatment_log(self):
"""
Get the treatment log implementation for the client.
:return: The treatment log
:rtype: TreatmentLog
"""
return self._treatment_log

def get_metrics(self):
"""
Get the metrics implementation for the client.
:return: The metrics
:rtype: Metrics
"""
return self._metrics



def _init_config(api_key, **kwargs):
config = kwargs.pop('config', dict())
Expand Down Expand Up @@ -698,4 +755,58 @@ def get_redis_client(api_key, **kwargs):

return redis_client

def get_uwsgi_client(api_key, **kwargs):
"""
Builds a Split Client that that gets its information from a uWSGI cache instance. It also writes
impressions and metrics to the same instance.

In order for this work properly, you need to periodically call the spooler uwsgi_update_splits and
uwsgi_update_segments scripts. You also need to run the uwsgi_report_impressions and uwsgi_report_metrics scripts in
order to push the impressions and metrics onto the Split.io backend-

The config_file parameter is the name of a file that contains the client configuration. Here's
an example of a config file:

{
"apiKey": "some-api-key",
"sdkApiBaseUrl": "https://sdk.split.io/api",
"eventsApiBaseUrl": "https://events.split.io/api",
"featuresRefreshRate": 30,
"segmentsRefreshRate": 60,
"metricsRefreshRate": 60,
"impressionsRefreshRate": 60
}

If the api_key argument is 'localhost' a localhost environment client is built based on the
contents of a .split file in the user's home directory. The definition file has the following
syntax:

file: (comment | split_line)+
comment : '#' string*\n
split_line : feature_name ' ' treatment\n
feature_name : string
treatment : string

It is possible to change the location of the split file by using the split_definition_file_name
argument.

:param api_key: The API key provided by Split.io
:type api_key: str
:param config_file: Filename of the config file
:type config_file: str
:param sdk_api_base_url: An override for the default API base URL.
:type sdk_api_base_url: str
:param events_api_base_url: An override for the default events base URL.
:type events_api_base_url: str
:param split_definition_file_name: Name of the definition file (Optional)
:type split_definition_file_name: str
"""
api_key, config, _, _ = _init_config(api_key, **kwargs)

if api_key == 'localhost':
return LocalhostEnvironmentClient(**kwargs)

uwsgi = get_uwsgi()
uwsgi_client = UWSGIClient(uwsgi, config)

return uwsgi_client
13 changes: 9 additions & 4 deletions splitio/factories.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""A module for Split.io Factories"""
from __future__ import absolute_import, division, print_function, unicode_literals

from splitio.clients import get_client, get_redis_client
from splitio.managers import (RedisSplitManager, SelfRefreshingSplitManager, LocalhostSplitManager)
from splitio.clients import get_client, get_redis_client, get_uwsgi_client
from splitio.managers import (RedisSplitManager, SelfRefreshingSplitManager, LocalhostSplitManager, UWSGISplitManager)
from splitio.redis_support import get_redis
from splitio.uwsgi import get_uwsgi

import logging

Expand Down Expand Up @@ -41,8 +42,12 @@ def __init__(self, api_key, **kwargs):
redis = get_redis(config)
self._manager = RedisSplitManager(redis)
else:
self._client = get_client(api_key, **kwargs)
self._manager = SelfRefreshingSplitManager(self._client.get_split_fetcher())
if 'uwsgiClient' in config and config['uwsgiClient'] :
self._client = get_uwsgi_client(api_key, **kwargs)
self._manager = UWSGISplitManager(get_uwsgi())
else:
self._client = get_client(api_key, **kwargs)
self._manager = SelfRefreshingSplitManager(self._client.get_split_fetcher())



Expand Down
Loading