blob: bf3eab43c3b76e5992d537cb3a9193d2864f6a36 [file] [log] [blame]
"""
Copyright 2013 Appurify, Inc
All rights reserved
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
"""
import os
import sys
import time
import math
import platform
import requests
import logging
from . import constants
logging.basicConfig(level=logging.WARNING, format='[%(asctime)s] [%(process)d] %(message)s')
def log(msg, level=None): # pragma: no cover
"""simple logging facility"""
logging.log(level if level else logging.WARNING, msg)
class AppurifyHttpClientError(Exception):
pass
class AppurifyHttpClient(object):
def __init__(self, method, resource, payload=None, files=None, headers=None):
self.method_name = method
self.method = getattr(requests, self.method_name)
self.resource = resource
self.url = self.url(self.resource)
self.payload = payload
self.files = files
if headers:
assert type(headers) == dict
else:
headers = dict()
self.headers = headers
self.headers['User-Agent'] = self.user_agent()
self.retry_count = 0
@staticmethod
def url(resource): # pragma: no cover
"""(defaults: https://live.appurify.com:443/resource/)
Url can be overridden by specifying following environment variables
APPURIFY_API_PROTO (default: https)
APPURIFY_API_HOST (default: live.appurify.com)
APPURIFY_API_PORT (default: 443)
Clients and Customers MUST not override this unless instructed by Appurify devs
"""
return '/'.join(['%s://%s:%s/resource' % (AppurifyHttpClient.proto(), AppurifyHttpClient.host(), AppurifyHttpClient.port()), resource]) + '/'
@staticmethod
def proto():
return os.environ.get('APPURIFY_API_PROTO', constants.API_PROTO)
@staticmethod
def host():
return os.environ.get('APPURIFY_API_HOST', constants.API_HOST)
@staticmethod
def port():
return os.environ.get('APPURIFY_API_PORT', str(constants.API_PORT))
@staticmethod
def user_agent(): # pragma: no cover
"""returns string representation of user-agent"""
implementation = platform.python_implementation()
if implementation == 'CPython':
version = platform.python_version()
elif implementation == 'PyPy':
version = '%s.%s.%s' % (sys.pypy_version_info.major, sys.pypy_version_info.minor, sys.pypy_version_info.micro)
elif implementation == 'Jython':
version = platform.python_version()
elif implementation == 'IronPython':
version = platform.python_version()
else:
version = 'Unknown'
try:
system = platform.system()
release = platform.release()
except IOError:
system = 'Unknown'
release = 'Unknown'
return " ".join([
'appurify-client/%s' % constants.__version__,
'python-requests/%s' % requests.__version__,
'%s/%s' % (implementation, version),
'%s/%s' % (system, release)
])
@staticmethod
def retry_on_failure():
return int(os.environ.get('APPURIFY_API_RETRY_ON_FAILURE', constants.API_RETRY_ON_FAILURE))
@staticmethod
def max_retry():
return int(os.environ.get('APPURIFY_API_MAX_RETRY', constants.API_MAX_RETRY))
@staticmethod
def retry_delay():
return int(os.environ.get('APPURIFY_API_RETRY_DELAY', constants.API_RETRY_DELAY))
@staticmethod
def api_status():
"""returns api service status from aws status page."""
url = '%s/%s.txt' % (constants.API_STATUS_BASE_URL, AppurifyHttpClient.host().split('.')[0])
print url
r = requests.get(url)
if r.status_code == 200:
return int(r.text.strip())
else:
return constants.API_STATUS_DOWN
@staticmethod
def wait_for_api_service():
"""waits endlessly until api service is up.
.. todo::
- support timeouts
"""
n = 1
while AppurifyHttpClient.api_status() == constants.API_STATUS_DOWN:
# restrict max sleep to 2^6 ~ 1 min
delay = int(math.pow(2, n % 7))
log('Service is down, will retry in %s seconds...' % delay)
time.sleep(delay)
n += 1
log('API service is back up, resuming...')
return True
def is_api_response(self, response):
"""detects if response came from api backend or from higher up in the stack."""
return 'x-api-server-hostname' in response.headers
def kwargs(self):
"""returns kwargs for configured requests method."""
kwargs = dict()
key = 'params' if self.method_name == 'get' else 'data'
kwargs[key] = self.payload
kwargs['headers'] = self.headers
kwargs['verify'] = False
if self.files:
kwargs['files'] = self.files
return kwargs
def start(self):
self.retry_count += 1
log("HTTP %s %s" % (self.method_name.upper(), self.url))
try:
response = self.method(self.url, **self.kwargs())
if self.is_api_response(response):
# received response from api backend
return response
else:
# received response from higher up the stack
log('Received unexpected response from API, waiting for service to resume...')
self.wait_for_api_service()
exc = AppurifyHttpClientError('API failure with response %s, code %s' % (response.text, response.status_code))
return self.retry_or_raise(exc)
except requests.exceptions.ConnectionError as e:
# either no internet connectivity / dns failures
# or lb is not responding/down
log('Connection to API server failed, waiting for service to resume...')
self.wait_for_api_service()
return self.retry_or_raise(AppurifyHttpClientError('API failure with reason %s' % str(e)))
def retry_or_raise(self, exc):
if self.retry_on_failure() and self.retry_count < self.max_retry():
time.sleep(int(math.pow(2, self.retry_count)))
return self.start()
raise exc
def get(resource, params): # pragma: no cover
"""make a HTTP GET request on API endpoint"""
client = AppurifyHttpClient('get', resource, params)
return client.start()
def post(resource, data, files=None): # pragma: no cover
"""make a HTTP POST request on API endpoint"""
client = AppurifyHttpClient('post', resource, data, files=files)
return client.start()
def wget(url, path, verify=True): # pragma: no cover
"""Download a file to specified path"""
with open(path, 'wb') as f:
f.write(requests.get(url, verify=verify).content)