Skip to content

Commit

Permalink
use requests library as client (#26)
Browse files Browse the repository at this point in the history
* merged in #23 & #24 
* use requests as http client instead of directly using httplib
* use requests HTTPAdapter for retries
* version bump to 0.2.0
  • Loading branch information
jatinn committed Dec 28, 2016
1 parent 3dac02b commit fc0dd01
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 144 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.pyc
*.pyo
tests/server.pem
17 changes: 17 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
language: python

python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"

env:
- REQUESTS="requests" # latest
- REQUESTS="requests==2.5" # min version of requests library

install: pip install $REQUESTS

script: make test
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ upload:
python setup.py register
echo "*** Now upload the binary to PyPi *** (one second)" && sleep 3 && open dist & open "http://pypi.python.org/pypi?%3Aaction=pkg_edit&name=customerio" # python setup.py upload

run_tests:
test:
openssl req -new -newkey rsa:1024 -days 10 -nodes -x509 -subj "/C=CA/ST=Ontario/L=Toronto/O=Test/CN=127.0.0.1" -keyout ./tests/server.pem -out ./tests/server.pem
python -m unittest discover -v
101 changes: 51 additions & 50 deletions customerio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
from __future__ import division
import json
import base64
try:
from httplib import HTTPSConnection
except ImportError:
from http.client import HTTPSConnection

from datetime import datetime
import time
import warnings

from requests import Session
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

try:
from datetime import timezone
USE_PY3_TIMESTAMPS = True
except ImportError:
USE_PY3_TIMESTAMPS = False

import time

VERSION = (0, 1, 11, 'final', 0)

VERSION = (0, 2, 0, 'final', 0)

def get_version():
version = '%s.%s' % (VERSION[0], VERSION[1])
Expand All @@ -29,72 +26,77 @@ def get_version():
version = '%s %s %s' % (version, VERSION[3], VERSION[4])
return version

warnings.simplefilter("default")

class CustomerIOException(Exception):
pass


class CustomerIO(object):

def __init__(self, site_id=None, api_key=None, host=None, port=None, url_prefix=None, json_encoder=json.JSONEncoder, retries=3):
def __init__(self, site_id=None, api_key=None, host=None, port=None, url_prefix=None, json_encoder=None, retries=3, timeout=10, backoff_factor=0.02):
self.site_id = site_id
self.api_key = api_key
self.host = host or 'track.customer.io'
self.port = port or 443
self.url_prefix = url_prefix or '/api/v1'
self.json_encoder = json_encoder
self.retries = retries
self.timeout = timeout
self.backoff_factor = backoff_factor

if json_encoder is not None:
warnings.warn("With the switch to using requests library the `json_encoder` param is no longer used.", DeprecationWarning)

self.setup_base_url()
self.setup_connection()

def setup_base_url(self):
template = 'https://{host}:{port}/{prefix}'
if self.port == 443:
template = 'https://{host}/{prefix}'

if '://' in self.host:
self.host = self.host.split('://')[1]

self.base_url = template.format(
host=self.host.strip('/'),
port=self.port,
prefix=self.url_prefix.strip('/'))

def setup_connection(self):
self.http = HTTPSConnection(self.host, self.port)
self.http = Session()
# Retry request a number of times before raising an exception
# also define backoff_factor to delay each retry
self.http.mount('https://', HTTPAdapter(max_retries=Retry(
total=self.retries, backoff_factor=self.backoff_factor)))
self.http.auth = (self.site_id, self.api_key)

def get_customer_query_string(self, customer_id):
'''Generates a customer API path'''
return '%s/customers/%s' % (self.url_prefix, customer_id)
return '{base}/customers/{id}'.format(base=self.base_url, id=customer_id)

def get_event_query_string(self, customer_id):
'''Generates an event API path'''
return '%s/customers/%s/events' % (self.url_prefix, customer_id)
return '{base}/customers/{id}/events'.format(base=self.base_url, id=customer_id)

def send_request(self, method, query_string, data):
def send_request(self, method, url, data):
'''Dispatches the request and returns a response'''

data = json.dumps(self._sanitize(data), cls=self.json_encoder)
auth = "{site_id}:{api_key}".format(site_id=self.site_id, api_key=self.api_key).encode("utf-8")
basic_auth = base64.b64encode(auth)

headers = {
'Authorization': b" ".join([b"Basic", basic_auth]),
'Content-Type': 'application/json',
'Content-Length': len(data),
}

# Retry request a number of times before raising an exception
retry = 0
success = False
while not success:
try:
self.http.request(method, query_string, data, headers)
response = self.http.getresponse()
success = True
except Exception as e:
retry += 1
if retry > self.retries:
# Raise exception alerting user that the system might be
# experiencing an outage and refer them to system status page.
message = '''Failed to receive valid reponse after {count} retries.
try:
response = self.http.request(method, url=url, json=self._sanitize(data), timeout=self.timeout)
except Exception as e:
# Raise exception alerting user that the system might be
# experiencing an outage and refer them to system status page.
message = '''Failed to receive valid reponse after {count} retries.
Check system status at http://status.customer.io.
Last caught exception -- {klass}: {message}
'''.format(klass=type(e), message=e, count=self.retries)
raise CustomerIOException(message)
# Setup connection again in case connection was closed by the server
self.setup_connection()
'''.format(klass=type(e), message=e, count=self.retries)
raise CustomerIOException(message)

result_status = response.status
result_status = response.status_code
if result_status != 200:
raise CustomerIOException('%s: %s %s' % (result_status, query_string, data))
return response.read()
raise CustomerIOException('%s: %s %s' % (result_status, url, data))
return response.text

def identify(self, id, **kwargs):
'''Identify a single customer by their unique id, and optionally add attributes'''
Expand Down Expand Up @@ -142,7 +144,6 @@ def backfill(self, customer_id, name, timestamp, **data):

def delete(self, customer_id):
'''Delete a customer profile'''

url = self.get_customer_query_string(customer_id)
self.send_request('DELETE', url, {})

Expand All @@ -154,6 +155,6 @@ def _sanitize(self, data):

def _datetime_to_timestamp(self, dt):
if USE_PY3_TIMESTAMPS:
return dt.replace(tzinfo=timezone.utc).timestamp()
return int(dt.replace(tzinfo=timezone.utc).timestamp())
else:
return int(time.mktime(dt.timetuple()))
57 changes: 11 additions & 46 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,17 @@
import os
from distutils.core import setup
from setuptools import find_packages, setup


package_name = "customerio"
package_dir = "customerio"
package_description = "Customer.io Python bindings."


def fullsplit(path, result=None):
"""
Split a pathname into components (the opposite of os.path.join) in a
platform-neutral way.
"""
if result is None:
result = []
head, tail = os.path.split(path)
if head == '':
return [tail] + result
if head == path:
return result
return fullsplit(head, [tail] + result)

# Compile the list of packages available, because distutils doesn't have
# an easy way to do this.
packages, data_files = [], []
root_dir = os.path.dirname(__file__)
if root_dir != '':
os.chdir(root_dir)

for dirpath, dirnames, filenames in os.walk(package_dir):
# Ignore dirnames that start with '.'
for i, dirname in enumerate(dirnames):
if dirname.startswith('.'):
del dirnames[i]
if '__init__.py' in filenames:
packages.append('.'.join(fullsplit(dirpath)))
elif filenames:
data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])

version = __import__(package_dir).get_version()
from customerio import get_version


setup(
name=package_name,
version=version,
name="customerio",
version=get_version(),
author="Peaberry Software Inc.",
author_email="support@customerio.com",
license="BSD",
description=package_description,
packages=packages,
data_files=data_files,
description="Customer.io Python bindings.",
url="https://github.com/customerio/customerio-python",
packages=find_packages(),
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
Expand All @@ -59,5 +21,8 @@ def fullsplit(path, result=None):
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
]
'Programming Language :: Python :: 3.5',
],
install_requires=['requests>=2.5'],
test_suite="tests",
)
33 changes: 0 additions & 33 deletions tests/server.pem

This file was deleted.

18 changes: 15 additions & 3 deletions tests/server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
try:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
except ImportError:
from http.server import BaseHTTPRequestHandler, HTTPServer

from functools import wraps
from random import randint

Expand All @@ -25,21 +29,29 @@ class Handler(BaseHTTPRequestHandler):
The handler reads the post body and fails for the `fail_count` specified.
After sending specified number of bad responses will sent a valid response.
'''
def do_DELETE(self):
self.send_response(200)
self.end_headers()

def do_POST(self):
self.send_response(200)
self.end_headers()

def do_PUT(self):
global request_counts

# extract params
_id = self.path.split("/")[-1]
content_len = int(self.headers.getheader('content-length', 0))
params = json.loads(self.rfile.read(content_len))
content_len = int(self.headers.get('content-length', 0))
params = json.loads(self.rfile.read(content_len).decode('utf-8'))
fail_count = params.get('fail_count', 0)

# retrieve number of requests already served
processed = request_counts.get(_id, 0)
if processed > fail_count:
# return a valid response
self.send_response(200)
self.end_headers()
return

# increment number of requests and return invalid response
Expand Down
Loading

0 comments on commit fc0dd01

Please sign in to comment.