Commit ed7d0bcd by Clinton Blackburn

Removed the client package

It is now available at https://github.com/edx/edx-analytics-api-client.

Change-Id: Ia2b9a66a8c5a5aee79410e23dc83723cc62fb38f
parent 77dc4626
ROOT = $(shell echo "$$PWD") ROOT = $(shell echo "$$PWD")
COVERAGE = $(ROOT)/build/coverage COVERAGE = $(ROOT)/build/coverage
PACKAGES = analyticsdata analyticsdataclient PACKAGES = analyticsdata
DATABASES = default analytics DATABASES = default analytics
validate: test.requirements test quality validate: test.requirements test quality
...@@ -13,7 +13,7 @@ clean: ...@@ -13,7 +13,7 @@ clean:
find . -name '*.pyc' -delete find . -name '*.pyc' -delete
coverage erase coverage erase
test.app: clean test: clean
. ./.test_env && ./manage.py test --settings=analyticsdataserver.settings.test \ . ./.test_env && ./manage.py test --settings=analyticsdataserver.settings.test \
--with-coverage --cover-inclusive --cover-branches \ --with-coverage --cover-inclusive --cover-branches \
--cover-html --cover-html-dir=$(COVERAGE)/html/ \ --cover-html --cover-html-dir=$(COVERAGE)/html/ \
...@@ -21,15 +21,6 @@ test.app: clean ...@@ -21,15 +21,6 @@ test.app: clean
--cover-package=analyticsdata \ --cover-package=analyticsdata \
analyticsdata/ analyticsdata/
test.client:
nosetests --with-coverage --cover-inclusive --cover-branches \
--cover-html --cover-html-dir=$(COVERAGE)/html/ \
--cover-xml --cover-xml-file=$(COVERAGE)/coverage.xml \
--cover-package=analyticsdataclient \
analyticsdataclient/
test: test.app test.client
diff.report: diff.report:
diff-cover $(COVERAGE)/coverage.xml --html-report $(COVERAGE)/diff_cover.html diff-cover $(COVERAGE)/coverage.xml --html-report $(COVERAGE)/diff_cover.html
diff-quality --violations=pep8 --html-report $(COVERAGE)/diff_quality_pep8.html diff-quality --violations=pep8 --html-report $(COVERAGE)/diff_quality_pep8.html
......
edx-analytics-data-api edX Analytics API Server
====================== ======================
See https://edx-wiki.atlassian.net/wiki/display/AN/Analytics+Data+API for more details. See https://edx-wiki.atlassian.net/wiki/display/AN/Analytics+Data+API for more details. For the API *client* visit
https://github.com/edx/edx-analytics-api-client.
Running Tests Running Tests
------------- -------------
......
import logging
import requests
import requests.exceptions
log = logging.getLogger(__name__)
class Client(object):
"""A client capable of retrieving the requested resources."""
DEFAULT_TIMEOUT = 0.1 # In seconds
DEFAULT_VERSION = 'v0'
def __init__(self, version=DEFAULT_VERSION):
"""
Initialize the Client.
Arguments:
version (str): When breaking changes are made to either the resource addresses or their returned data, this
value will change. Multiple clients can be made which adhere to each version.
"""
self.version = version
def get(self, resource, timeout=None):
"""
Retrieve the data for a resource.
Arguments:
resource (str): Path in the form of slash separated strings.
timeout (float): Continue to attempt to retrieve a resource for this many seconds before giving up and
raising an error.
Returns: A structure consisting of simple python types (dict, list, int, str etc).
Raises: ClientError if the resource cannot be retrieved for any reason.
"""
raise NotImplementedError
def has_resource(self, resource, timeout=None):
"""
Check if a resource exists.
Arguments:
resource (str): Path in the form of slash separated strings.
timeout (float): Continue to attempt to retrieve a resource for this many seconds before giving up and
raising an error.
Returns: True iff the resource exists.
"""
raise NotImplementedError
class RestClient(Client):
"""Retrieve resources from a remote REST API."""
DEFAULT_AUTH_TOKEN = ''
DEFAULT_BASE_URL = 'http://localhost:9090'
def __init__(self, base_url=DEFAULT_BASE_URL, auth_token=DEFAULT_AUTH_TOKEN):
"""
Initialize the RestClient.
Arguments:
base_url (str): A URL containing the scheme, netloc and port of the remote service.
auth_token (str): The token that should be used to authenticate requests made to the remote service.
"""
super(RestClient, self).__init__()
self.base_url = '{0}/api/{1}'.format(base_url, self.version)
self.auth_token = auth_token
def get(self, resource, timeout=None):
"""
Retrieve the data for a resource.
Inherited from `Client`.
Arguments:
resource (str): Path in the form of slash separated strings.
timeout (float): Continue to attempt to retrieve a resource for this many seconds before giving up and
raising an error.
Returns: A structure consisting of simple python types (dict, list, int, str etc).
Raises: ClientError if the resource cannot be retrieved for any reason.
"""
response = self._request(resource, timeout=timeout)
try:
return response.json()
except ValueError:
message = 'Unable to decode JSON response'
log.exception(message)
raise ClientError(message)
def has_resource(self, resource, timeout=None):
"""
Check if the server responds with a 200 OK status code when the resource is requested.
Inherited from `Client`.
Arguments:
resource (str): Path in the form of slash separated strings.
timeout (float): Continue to attempt to retrieve a resource for this many seconds before giving up and
raising an error.
Returns: True iff the resource exists.
"""
try:
self._request(resource, timeout=timeout)
return True
except ClientError:
return False
def _request(self, resource, timeout=None):
if timeout is None:
timeout = self.DEFAULT_TIMEOUT
headers = {
'Accept': 'application/json',
}
if self.auth_token:
headers['Authorization'] = 'Token ' + self.auth_token
try:
response = requests.get('{0}/{1}'.format(self.base_url, resource), headers=headers, timeout=timeout)
if response.status_code != requests.codes.ok: # pylint: disable=no-member
message = 'Resource "{0}" returned status code {1}'.format(resource, response.status_code)
log.error(message)
raise ClientError(message)
return response
except requests.exceptions.RequestException:
message = 'Unable to retrieve resource'
log.exception(message)
raise ClientError('{0} "{1}"'.format(message, resource))
# TODO: Provide more detailed errors as necessary.
class ClientError(Exception):
"""An error occurred that prevented the client from performing the requested operation."""
pass
class Course(object):
"""Course scoped analytics."""
# TODO: Should we have an acceptance test that runs the hadoop job to populate the database, serves the data with
# the API server and uses the client to retrieve it and validate the various transports?
def __init__(self, client, course_key):
"""
Initialize the CourseUserActivity.
Arguments:
client (analyticsdataclient.client.Client): The client to use to access remote resources.
course_key (mixed): An object that when passed to unicode() returns the unique identifier for the course as
it is represented in the data pipeline results.
"""
self.client = client
self.course_key = course_key
@property
def recent_active_user_count(self):
"""A count of users who have recently interacted with the course in any way."""
# TODO: should we return something more structured than a python dict?
return self.client.get('courses/{0}/recent_activity'.format(unicode(self.course_key)))
@property
def recent_problem_activity_count(self):
"""A count of users who have recently attempted a problem."""
# TODO: Can we avoid passing around strings like "ATTEMPTED_PROBLEM" in the data pipeline and the client?
return self.client.get(
'courses/{0}/recent_activity?label=ATTEMPTED_PROBLEM'.format(unicode(self.course_key)))
from analyticsdataclient.client import ClientError
class Status(object):
"""
Query the status of the connection between the client and the remote service.
Arguments:
client (analyticsdataclient.client.Client): The client to use to access remote resources.
"""
def __init__(self, client):
"""
Initialize the Status.
Arguments:
client (analyticsdataclient.client.Client): The client to use to access remote resources.
"""
self.client = client
@property
def alive(self):
"""
A very fast shallow check to see if the service is functioning.
Returns: True iff the remote server responds to requests.
"""
return self.client.has_resource('status')
@property
def authenticated(self):
"""
Validate the client credentials.
Returns: True iff the client is successfully authenticated.
"""
return self.client.has_resource('authenticated')
@property
def healthy(self):
"""
A slow deep health check of the remote service.
Returns: True iff the remote service is reasonably confident that further operations will succeed.
"""
try:
health = self.client.get('health')
except ClientError:
return False
try:
return health['overall_status'] == 'OK'
except KeyError:
return False
from analyticsdataclient.client import Client, ClientError
class InMemoryClient(Client):
"""Serves resources that have previously been set and stored in memory."""
def __init__(self):
"""Initialize the fake client."""
super(InMemoryClient, self).__init__()
self.resources = {}
def has_resource(self, resource, timeout=None):
"""Return True iff the resource has been previously set."""
try:
self.get(resource, timeout=timeout)
return True
except ClientError:
return False
def get(self, resource, timeout=None):
"""Return the resource from memory."""
try:
return self.resources[resource]
except KeyError:
raise ClientError('Unable to find requested resource')
import json
from unittest import TestCase
import httpretty
from analyticsdataclient.client import RestClient, ClientError
class RestClientTest(TestCase):
BASE_URI = 'http://localhost:9091'
VERSIONED_BASE_URI = BASE_URI + '/api/v0'
TEST_ENDPOINT = 'test'
TEST_URI = VERSIONED_BASE_URI + '/' + TEST_ENDPOINT
def setUp(self):
httpretty.enable()
self.client = RestClient(base_url=self.BASE_URI)
def tearDown(self):
httpretty.disable()
def test_has_resource(self):
httpretty.register_uri(httpretty.GET, self.TEST_URI, body='')
self.assertEquals(self.client.has_resource(self.TEST_ENDPOINT), True)
def test_missing_resource(self):
httpretty.register_uri(httpretty.GET, self.TEST_URI, body='', status=404)
self.assertEquals(self.client.has_resource(self.TEST_ENDPOINT), False)
def test_failed_authentication(self):
self.client = RestClient(base_url=self.BASE_URI, auth_token='atoken')
httpretty.register_uri(httpretty.GET, self.TEST_URI, body='', status=401)
self.assertEquals(self.client.has_resource(self.TEST_ENDPOINT), False)
self.assertEquals(httpretty.last_request().headers['Authorization'], 'Token atoken')
def test_get(self):
data = {
'foo': 'bar'
}
httpretty.register_uri(httpretty.GET, self.TEST_URI, body=json.dumps(data))
self.assertEquals(self.client.get(self.TEST_ENDPOINT), data)
def test_get_invalid_json(self):
data = {
'foo': 'bar'
}
httpretty.register_uri(httpretty.GET, self.TEST_URI, body=json.dumps(data)[:6])
with self.assertRaises(ClientError):
self.client.get(self.TEST_ENDPOINT)
from unittest import TestCase
from analyticsdataclient.status import Status
from analyticsdataclient.tests import InMemoryClient
class StatusTest(TestCase):
def setUp(self):
self.client = InMemoryClient()
self.status = Status(self.client)
def test_alive(self):
self.assertEquals(self.status.alive, False)
self.client.resources['status'] = ''
self.assertEquals(self.status.alive, True)
def test_authenticated(self):
self.assertEquals(self.status.authenticated, False)
self.client.resources['authenticated'] = ''
self.assertEquals(self.status.authenticated, True)
def test_healthy(self):
self.client.resources['health'] = {
'overall_status': 'OK',
'detailed_status': {
'database_connection': 'OK'
}
}
self.assertEquals(self.status.healthy, True)
def test_not_healthy(self):
self.client.resources['health'] = {
'overall_status': 'UNAVAILABLE',
'detailed_status': {
'database_connection': 'UNAVAILABLE'
}
}
self.assertEquals(self.status.healthy, False)
def test_invalid_health_value(self):
self.client.resources['health'] = {}
self.assertEquals(self.status.healthy, False)
from setuptools import setup
setup(
name="edx-analytics-data-client",
version="0.1.0",
packages=[
'analyticsdataclient'
],
install_requires=[
"requests==2.3.0",
],
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment