PKcOG kxr go_http/metrics.py""" Experimental client for Vumi Go's metrics API. TODO: * Factor out common API-level code, such as auth. * Implement more of the API as the server side grows. """ import json import requests class MetricsApiClient(object): """ Client for Vumi Go's metrics API. :param str auth_token: An OAuth 2 access token. NOTE: This will be replaced by a proper authentication system at some point. :param str api_url: The full URL of the HTTP API. Defaults to ``https://go.vumi.org/api/v1/go``. :type session: :class:`requests.Session` :param session: Requests session to use for HTTP requests. Defaults to a new session. """ def __init__(self, auth_token, api_url=None, session=None): self.auth_token = auth_token if api_url is None: api_url = "https://go.vumi.org/api/v1/go" self.api_url = api_url.rstrip('/') if session is None: session = requests.Session() self.session = session def _api_request(self, method, api_collection, data=None): url = "%s/%s" % (self.api_url, api_collection) headers = { "Content-Type": "application/json; charset=utf-8", "Authorization": "Bearer %s" % (self.auth_token,), } if method is "GET" and data is not None: r = self.session.request(method, url, params=data, headers=headers) else: if data is not None: data = json.dumps(data) r = self.session.request(method, url, data=data, headers=headers) r.raise_for_status() return r.json() def get_metric(self, metric, start, interval, nulls): """ Get a metric. :param str metric: Metric name. :param str start: How far back to get metrics from (e.g. `-30d`). :param str interval: Bucket size for grouping (e.g. `1d`). :param str nulls: How nulls should be handled (e.g. `omit`). """ payload = { "m": metric, "from": start, "interval": interval, "nulls": nulls } return self._api_request("GET", "metrics/", payload) def fire(self, metrics): """ Fire metrics. :param dict metrics: A mapping of metric names to floating point metric values. When metrics are fired they must specify an aggregator. The aggregation method is determined by the suffix of the metric name. For example, ``foo.last`` fires a metric that uses the ``last`` aggregation method. If a metric name does not end in a valid aggregator name, firing the set of metrics will fail. The available aggregators are: :Average: ``avg``. Aggregates by averaging the values in each time period. :Sum: ``sum``. Aggregates by summing all the values in each time period. :Maximum: ``max``. Aggregates by choosing the maximum value in each time period. :Minimum: ``min``. Aggregates by choosing the minimum value in each time period. :Last: ``last``. Aggregates by choosing the last value in each time period. Note that metrics can also be fired via an HTTP conversation API. See :meth:`go_http.send.HttpApiSender.fire_metric`. """ return self._api_request("POST", "metrics/", metrics) PKcOGTTgo_http/contacts.py""" Experimental client for Vumi Go's contacts API. TODO: * Factor out common API-level code, such as auth. * Implement more of the API as the server side grows. """ import json import requests from go_http.exceptions import PagedException class ContactsApiClient(object): """ Client for Vumi Go's contacts API. :param str auth_token: An OAuth 2 access token. NOTE: This will be replaced by a proper authentication system at some point. :param str api_url: The full URL of the HTTP API. Defaults to ``https://go.vumi.org/api/v1/go``. :type session: :class:`requests.Session` :param session: Requests session to use for HTTP requests. Defaults to a new session. """ def __init__(self, auth_token, api_url=None, session=None): self.auth_token = auth_token if api_url is None: api_url = "https://go.vumi.org/api/v1/go" self.api_url = api_url.rstrip('/') if session is None: session = requests.Session() self.session = session def _api_request( self, method, api_collection, api_path, data=None, params=None): url = "%s/%s/%s" % (self.api_url, api_collection, api_path) headers = { "Content-Type": "application/json; charset=utf-8", "Authorization": "Bearer %s" % (self.auth_token,), } if data is not None: data = json.dumps(data) r = self.session.request( method, url, data=data, headers=headers, params=params) r.raise_for_status() return r.json() def contacts(self, start_cursor=None): """ Retrieve all contacts. This uses the API's paginated contact download. :param start_cursor: An optional parameter that declares the cursor to start fetching the contacts from. :returns: An iterator over all contacts. """ if start_cursor: page = self._api_request( "GET", "contacts", "?cursor=%s" % start_cursor) else: page = self._api_request("GET", "contacts", "") while True: for contact in page['data']: yield contact if page['cursor'] is None: break try: page = self._api_request( "GET", "contacts", "?cursor=%s" % page['cursor']) except Exception as err: raise PagedException(page['cursor'], err) def create_contact(self, contact_data): """ Create a contact. :param dict contact_data: Data for new contact. """ return self._api_request("POST", "contacts", "", contact_data) def _contact_by_key(self, contact_key): return self._api_request("GET", "contacts", contact_key) def _contact_by_field(self, field, value): contact = self._api_request( "GET", "contacts", "", params={'query': '%s=%s' % (field, value)}) return contact.get('data')[0] def get_contact(self, *args, **kw): """ Get a contact. May either be called as ``.get_contact(contact_key)`` to get a contact from its key, or ``.get_contact(field=value)``, to get the contact from an address field ``field`` having a value ``value``. Contact key example: contact = api.get_contact('abcdef123456') Field/value example: contact = api.get_contact(msisdn='+12345') :param str contact_key: Key for the contact to get. :param str field: ``field`` is the address field that is searched on (e.g. ``msisdn`` , ``twitter_handle``). The value of ``field`` is the value to search for (e.g. ``+12345``, `@foobar``). """ if not kw and len(args) == 1: return self._contact_by_key(args[0]) elif len(kw) == 1 and not args: field, value = kw.items()[0] return self._contact_by_field(field, value) raise ValueError( "get_contact may either be called as .get_contact(contact_key) or" " .get_contact(field=value)") def update_contact(self, contact_key, update_data): """ Update a contact. :param str contact_key: Key for the contact to update. :param dict update_data: Fields to modify. """ return self._api_request("PUT", "contacts", contact_key, update_data) def delete_contact(self, contact_key): """ Delete a contact. :param str contact_key: Key for the contact to delete. """ return self._api_request("DELETE", "contacts", contact_key) def create_group(self, group_data): """ Create a group. :param dict group_data: Data for new group. """ return self._api_request("POST", "groups", "", group_data) def get_group(self, group_key): """ Get a group :param str group_key: Key for the group to get """ return self._api_request("GET", "groups", group_key) def update_group(self, group_key, update_data): """ Update a group. :param str group_key Key for the group to update. :param str update_data Fields to modify. """ return self._api_request("PUT", "groups", group_key, update_data) def delete_group(self, group_key): """ Delete a group. :param str group_key Key for the group to delete. """ return self._api_request("DELETE", "groups", group_key) def group_contacts(self, group_key, start_cursor=None): """ Retrieve all group contacts. This uses the API's paginated contact download. :param str group_key Key for the group to query. :param start_cursor: An optional parameter that declares the cursor to start fetching the contacts from. :returns: An iterator over all group contacts. """ if start_cursor: page = self._api_request( "GET", "groups/%s" % group_key, "contacts?cursor=%s" % start_cursor) else: page = self._api_request( "GET", "groups/%s" % group_key, "contacts") while True: for contact in page['data']: yield contact if page['cursor'] is None: break try: page = self._api_request( "GET", "groups/%s" % group_key, "contacts?cursor=%s" % page['cursor']) except Exception as err: raise PagedException(page['cursor'], err) PKJkGl)go_http/__init__.py"""Vumi Go HTTP API client library.""" from .send import HttpApiSender, LoggingSender __version__ = "0.3.0" __all__ = [ 'HttpApiSender', 'LoggingSender', ] PKJkGuZZgo_http/send.py""" Simple utilities for sending messages via Vumi Go' HTTP API. """ import json import logging import pprint import uuid import requests from requests.exceptions import HTTPError from go_http.exceptions import UserOptedOutException class HttpApiSender(object): """ A helper for sending text messages and firing metrics via Vumi Go's HTTP API. :param str account_key: The unique id of the account to send to. You can find this at the bottom of the Account > Details page in Vumi Go. :param str conversation_key: The unique id of the conversation to send to. This is the UUID at the end of the conversation URL. :param str conversation_token: The secret authentication token entered in the conversation config. :param str api_url: The full URL of the HTTP API. Defaults to ``https://go.vumi.org/api/v1/go/http_api_nostream``. :type session: :class:`requests.Session` :param session: Requests session to use for HTTP requests. Defaults to a new session. """ def __init__(self, account_key, conversation_key, conversation_token, api_url=None, session=None): self.account_key = account_key self.conversation_key = conversation_key self.conversation_token = conversation_token if api_url is None: api_url = "https://go.vumi.org/api/v1/go/http_api_nostream" self.api_url = api_url if session is None: session = requests.Session() self.session = session def _api_request(self, suffix, py_data): url = "%s/%s/%s" % (self.api_url, self.conversation_key, suffix) headers = {'content-type': 'application/json; charset=utf-8'} auth = (self.account_key, self.conversation_token) data = json.dumps(py_data) r = self.session.put(url, auth=auth, data=data, headers=headers) r.raise_for_status() return r.json() def _raw_send(self, data): try: return self._api_request('messages.json', data) except HTTPError as e: try: response = e.response.json() except ValueError: # Some HTTP responses are not decodable raise e if (e.response.status_code != 400 or 'opted out' not in response.get('reason', '') or response.get('success')): raise e raise UserOptedOutException( data.get("to_addr"), data.get("content"), response.get('reason')) def send_text(self, to_addr, content, session_event=None): """ Send a text message to an address. :param str to_addr: Address to send to. :param str content: Text to send. :param str session_event: The session event for session-based messaging channels (e.g. USSD). May be one of 'new', 'resume' or 'close'. Optional. """ data = { "to_addr": to_addr, "content": content, } if session_event is not None: data["session_event"] = session_event return self._raw_send(data) def send_voice(self, to_addr, content, speech_url=None, wait_for=None, session_event=None): """ Send a voice message to an address. :param str to_addr: Address to send to. :param str content: Text to send. If ``speech_url`` is not provided, a text-to-speech engine is used to generate a voice message from this text. If ``speech_url`` is provided, this text is ignored by voice messaging channels, but may still be used by non-voice channels that process the message. :param str speech_url: A URL to a voice file containing the voice message to sent. If not given, a voice message is generated from ``content`` using a text-to-speech engine. Optional. :param str wait_for: By default the Vumi voice connections send a response to a message as soon as a key is pressed by the person the call is with. The ``wait_for`` option allows specifying a character to wait for before a response is returned. For example, without ``wait_for`` pressing '123#' on the phone would result in four response messages containing '1', '2', '3' and '#' respectively. If ``wait_for='#'`` was set, pressing '123#' would result in a single response message containing '123'. Optional. :param str session_event: The session event. May be one of 'new', 'resume' or 'close'. 'new' initiates a new call. 'resume' continues an existing call. 'close' ends an existing call. The default of ``None`` is equivalent to 'resume'. """ data = { "to_addr": to_addr, "content": content, } if session_event is not None: data["session_event"] = session_event voice = {} if speech_url is not None: voice["speech_url"] = speech_url if wait_for is not None: voice["wait_for"] = wait_for if voice: data["helper_metadata"] = {"voice": voice} return self._raw_send(data) def fire_metric(self, metric, value, agg="last"): """ Fire a value for a metric. :param str metric: Name of the metric to fire. :param float value: Value for the metric. :param str agg: Aggregation type. Defaults to ``'last'``. Other allowed values are ``'sum'``, ``'avg'``, ``'max'`` and ``'min'``. Note that metrics can also be fired via the metrics API. See :meth:`go_http.metrics.MetricsApiClient.fire`. """ data = [ [ metric, value, agg ] ] return self._api_request('metrics.json', data) class LoggingSender(HttpApiSender): """ A helper for pretending to sending text messages and fire metrics by instead logging them via Python's logging module. :param str logger: The name of the logger to use. :param int level: The level to log at. Defaults to ``logging.INFO``. """ def __init__(self, logger, level=logging.INFO): self._logger = logging.getLogger(logger) self._level = level def _api_request(self, suffix, py_data): if suffix == "messages.json": return self._handle_messages(py_data) elif suffix == "metrics.json": return self._handle_metrics(py_data) else: raise ValueError("XXX") def _handle_messages(self, data): data["message_id"] = uuid.uuid4().hex msg = "Message: %r sent to %r" % (data['content'], data['to_addr']) if data.get("session_event"): msg += " [session_event: %s]" % data["session_event"] if data.get("helper_metadata"): for key, value in sorted(data["helper_metadata"].items()): msg += " [%s: %s]" % (key, pprint.pformat(value)) self._logger.log(self._level, msg) return data def _handle_metrics(self, data): for metric, value, agg in data: assert agg in ["last", "sum", "avg", "max", "min"] self._logger.log( self._level, "Metric: %r [%s] -> %g" % ( metric, agg, float(value), )) return { "success": True, "reason": "Metrics published", } PKJkGrX,,go_http/optouts.py""" Client for Vumi Go's opt out API. """ import json import urllib import requests class OptOutsApiClient(object): """ Client for Vumi Go's opt out API. :param str auth_token: An OAuth 2 access token. :param str api_url: The full URL of the HTTP API. Defaults to ``https://go.vumi.org/api/v1/go``. :type session: :class:`requests.Session` :param session: Requests session to use for HTTP requests. Defaults to a new session. """ def __init__(self, auth_token, api_url=None, session=None): self.auth_token = auth_token if api_url is None: api_url = "https://go.vumi.org/api/v1/go" self.api_url = api_url.rstrip('/') if session is None: session = requests.Session() self.session = session def _api_request(self, method, path, data=None, none_for_statuses=()): url = "%s/%s" % (self.api_url, urllib.quote(path)) headers = { "Content-Type": "application/json; charset=utf-8", "Authorization": "Bearer %s" % (self.auth_token,), } if method is "GET" and data is not None: r = self.session.request(method, url, params=data, headers=headers) else: if data is not None: data = json.dumps(data) r = self.session.request(method, url, data=data, headers=headers) if r.status_code in none_for_statuses: return None r.raise_for_status() return r.json() def get_optout(self, address_type, address): """ Retrieve an opt out record. :param str address_type: Type of address, e.g. `msisdn`. :param str address: The address to retrieve an opt out for, e.g. `+271235678`. :return: The opt out record (a dict) or `None` if the API returned a 404 HTTP response. Example:: >>> client.get_optout('msisdn', '+12345') { u'created_at': u'2015-11-10 20:33:03.742409', u'message': None, u'user_account': u'fxxxeee', } """ uri = "optouts/%s/%s" % (address_type, address) result = self._api_request("GET", uri, none_for_statuses=(404,)) if result is None: return None return result["opt_out"] def set_optout(self, address_type, address): """ Register an address as having opted out. :param str address_type: Type of address, e.g. `msisdn`. :param str address: The address to store an opt out for, e.g. `+271235678`. :return: The created opt out record (a dict). Example:: >>> client.set_optout('msisdn', '+12345') { u'created_at': u'2015-11-10 20:33:03.742409', u'message': None, u'user_account': u'fxxxeee', } """ uri = "optouts/%s/%s" % (address_type, address) result = self._api_request("PUT", uri) return result["opt_out"] def delete_optout(self, address_type, address): """ Remove an out opt record. :param str address_type: Type of address, e.g. `msisdn`. :param str address: The address to remove the opt out record for, e.g. `+271235678`. :return: The deleted opt out record (a dict) or None if the API returned an HTTP 404 reponse. Example:: >>> client.delete_optout('msisdn', '+12345') { u'created_at': u'2015-11-10 20:33:03.742409', u'message': None, u'user_account': u'fxxxeee', } """ uri = "optouts/%s/%s" % (address_type, address) result = self._api_request("DELETE", uri, none_for_statuses=(404,)) if result is None: return None return result["opt_out"] def count(self): """ Return a count of the total number of opt out records. :return: The total number of opt outs (an integer). Example:: >>> client.count() 215 """ uri = "optouts/count" result = self._api_request("GET", uri) return result["opt_out_count"] PKcOG8<.  go_http/exceptions.py""" Exceptions raised by API calls. """ class UserOptedOutException(Exception): """ Exception raised if a message is sent to a recipient who has opted out. Attributes: to_addr - The address of the opted out recipient message - The message content reason - The error reason given by the API """ def __init__(self, to_addr, message, reason): self.to_addr = to_addr self.message = message self.reason = reason class PagedException(Exception): """ Exception raised during paged API calls that can be restarted by specifying a start cursor. Attributes: cursor - The value of the cursor for which the paged request failed. error - The exception that occurred. """ def __init__(self, cursor, error): self.cursor = cursor self.error = error def __repr__(self): return "" % ( self.cursor, self.error) def __str__(self): return repr(self) PKJkG:..go_http/tests/test_send.py""" Tests for go_http.send. """ import json import logging from unittest import TestCase from requests_testadapter import TestAdapter, TestSession from go_http.send import HttpApiSender, LoggingSender from go_http.exceptions import UserOptedOutException from requests.exceptions import HTTPError class RecordingAdapter(TestAdapter): """ Record the request that was handled by the adapter. """ request = None def send(self, request, *args, **kw): self.request = request return super(RecordingAdapter, self).send(request, *args, **kw) class TestHttpApiSender(TestCase): def setUp(self): self.session = TestSession() self.sender = HttpApiSender( account_key="acc-key", conversation_key="conv-key", api_url="http://example.com/api/v1/go/http_api_nostream", conversation_token="conv-token", session=self.session) def test_default_session(self): import requests sender = HttpApiSender( account_key="acc-key", conversation_key="conv-key", conversation_token="conv-token") self.assertTrue(isinstance(sender.session, requests.Session)) def test_default_api_url(self): sender = HttpApiSender( account_key="acc-key", conversation_key="conv-key", conversation_token="conv-token") self.assertEqual(sender.api_url, "https://go.vumi.org/api/v1/go/http_api_nostream") def check_request(self, request, method, data=None, headers=None): self.assertEqual(request.method, method) if data is not None: self.assertEqual(json.loads(request.body), data) if headers is not None: for key, value in headers.items(): self.assertEqual(request.headers[key], value) def check_successful_send(self, send, expected_data): adapter = RecordingAdapter(json.dumps({"message_id": "id-1"})) self.session.mount( "http://example.com/api/v1/go/http_api_nostream/conv-key/" "messages.json", adapter) result = send() self.assertEqual(result, { "message_id": "id-1", }) self.check_request( adapter.request, 'PUT', data=expected_data, headers={"Authorization": u'Basic YWNjLWtleTpjb252LXRva2Vu'}) def test_send_text(self): self.check_successful_send( lambda: self.sender.send_text("to-addr-1", "Hello!"), { "content": "Hello!", "to_addr": "to-addr-1", }) def test_send_text_with_session_event(self): self.check_successful_send( lambda: self.sender.send_text( "to-addr-1", "Hello!", session_event="close"), { "content": "Hello!", "to_addr": "to-addr-1", "session_event": "close", }) def test_send_text_to_opted_out(self): """ UserOptedOutException raised for sending messages to opted out recipients """ self.session.mount( "http://example.com/api/v1/go/http_api_nostream/conv-key/" "messages.json", TestAdapter( json.dumps({ "success": False, "reason": "Recipient with msisdn to-addr-1 has opted out"} ), status=400)) try: self.sender.send_text('to-addr-1', "foo") except UserOptedOutException as e: self.assertEqual(e.to_addr, 'to-addr-1') self.assertEqual(e.message, 'foo') self.assertEqual( e.reason, 'Recipient with msisdn to-addr-1 has opted out') def test_send_text_to_other_http_error(self): """ HTTP errors should not be raised as UserOptedOutExceptions if they are not user opted out errors. """ self.session.mount( "http://example.com/api/v1/go/http_api_nostream/conv-key/" "messages.json", TestAdapter( json.dumps({ "success": False, "reason": "No unicorns were found" }), status=400)) try: self.sender.send_text('to-addr-1', 'foo') except HTTPError as e: self.assertEqual(e.response.status_code, 400) response = e.response.json() self.assertFalse(response['success']) self.assertEqual(response['reason'], "No unicorns were found") def test_send_text_to_other_http_error_not_json(self): """ HTTP errors that are not json encode should be raised without decoding errors """ self.session.mount( "http://example.com/api/v1/go/http_api_nostream/conv-key/" "messages.json", TestAdapter( "401 Client Error: Unauthorized", status=401)) try: self.sender.send_text('to-addr-1', 'foo') except HTTPError as e: self.assertEqual(e.response.status_code, 401) self.assertEqual(e.response.text, "401 Client Error: Unauthorized") def test_send_voice(self): self.check_successful_send( lambda: self.sender.send_voice("to-addr-1", "Hello!"), { "content": "Hello!", "to_addr": "to-addr-1", }) def test_send_voice_with_session_event(self): self.check_successful_send( lambda: self.sender.send_voice( "to-addr-1", "Hello!", session_event="close"), { "content": "Hello!", "to_addr": "to-addr-1", "session_event": "close", }) def test_send_voice_with_speech_url(self): self.check_successful_send( lambda: self.sender.send_voice( "to-addr-1", "Hello!", speech_url="http://example.com/voice.ogg"), { "content": "Hello!", "to_addr": "to-addr-1", "helper_metadata": { "voice": {"speech_url": "http://example.com/voice.ogg"}, }, }) def test_send_voice_with_wait_for(self): self.check_successful_send( lambda: self.sender.send_voice( "to-addr-1", "Hello!", wait_for="#"), { "content": "Hello!", "to_addr": "to-addr-1", "helper_metadata": { "voice": {"wait_for": "#"}, }, }) def test_fire_metric(self): adapter = RecordingAdapter( json.dumps({"success": True, "reason": "Yay"})) self.session.mount( "http://example.com/api/v1/go/http_api_nostream/conv-key/" "metrics.json", adapter) result = self.sender.fire_metric("metric-1", 5.1, agg="max") self.assertEqual(result, { "success": True, "reason": "Yay", }) self.check_request( adapter.request, 'PUT', data=[["metric-1", 5.1, "max"]], headers={"Authorization": u'Basic YWNjLWtleTpjb252LXRva2Vu'}) def test_fire_metric_default_agg(self): adapter = RecordingAdapter( json.dumps({"success": True, "reason": "Yay"})) self.session.mount( "http://example.com/api/v1/go/http_api_nostream/conv-key/" "metrics.json", adapter) result = self.sender.fire_metric("metric-1", 5.2) self.assertEqual(result, { "success": True, "reason": "Yay", }) self.check_request( adapter.request, 'PUT', data=[["metric-1", 5.2, "last"]], headers={"Authorization": u'Basic YWNjLWtleTpjb252LXRva2Vu'}) class RecordingHandler(logging.Handler): """ Record logs. """ logs = None def emit(self, record): if self.logs is None: self.logs = [] self.logs.append(record) class TestLoggingSender(TestCase): def setUp(self): self.sender = LoggingSender('go_http.test') self.handler = RecordingHandler() logger = logging.getLogger('go_http.test') logger.setLevel(logging.INFO) logger.addHandler(self.handler) def check_logs(self, msg, levelno=logging.INFO): [log] = self.handler.logs self.assertEqual(log.msg, msg) self.assertEqual(log.levelno, levelno) def test_send_text(self): result = self.sender.send_text("to-addr-1", "Hello!") self.assertEqual(result, { "message_id": result["message_id"], "to_addr": "to-addr-1", "content": "Hello!", }) self.check_logs("Message: 'Hello!' sent to 'to-addr-1'") def test_send_text_with_session_event(self): result = self.sender.send_text( "to-addr-1", "Hello!", session_event='close') self.assertEqual(result, { "message_id": result["message_id"], "to_addr": "to-addr-1", "content": "Hello!", "session_event": "close", }) self.check_logs( "Message: 'Hello!' sent to 'to-addr-1'" " [session_event: close]") def test_send_voice(self): result = self.sender.send_voice("to-addr-1", "Hello!") self.assertEqual(result, { "message_id": result["message_id"], "to_addr": "to-addr-1", "content": "Hello!", }) self.check_logs("Message: 'Hello!' sent to 'to-addr-1'") def test_send_voice_with_session_event(self): result = self.sender.send_voice( "to-addr-1", "Hello!", session_event='close') self.assertEqual(result, { "message_id": result["message_id"], "to_addr": "to-addr-1", "content": "Hello!", "session_event": "close", }) self.check_logs( "Message: 'Hello!' sent to 'to-addr-1'" " [session_event: close]") def test_send_voice_with_speech_url(self): result = self.sender.send_voice( "to-addr-1", "Hello!", speech_url='http://example.com/voice.ogg') self.assertEqual(result, { "message_id": result["message_id"], "to_addr": "to-addr-1", "content": "Hello!", "helper_metadata": { "voice": { "speech_url": "http://example.com/voice.ogg", }, } }) self.check_logs( "Message: 'Hello!' sent to 'to-addr-1'" " [voice: {'speech_url': 'http://example.com/voice.ogg'}]") def test_send_voice_with_wait_for(self): result = self.sender.send_voice( "to-addr-1", "Hello!", wait_for="#") self.assertEqual(result, { "message_id": result["message_id"], "to_addr": "to-addr-1", "content": "Hello!", "helper_metadata": { "voice": { "wait_for": "#", }, } }) self.check_logs( "Message: 'Hello!' sent to 'to-addr-1'" " [voice: {'wait_for': '#'}]") def test_fire_metric(self): result = self.sender.fire_metric("metric-1", 5.1, agg="max") self.assertEqual(result, { "success": True, "reason": "Metrics published", }) self.check_logs("Metric: 'metric-1' [max] -> 5.1") def test_fire_metric_default_agg(self): result = self.sender.fire_metric("metric-1", 5.2) self.assertEqual(result, { "success": True, "reason": "Metrics published", }) self.check_logs("Metric: 'metric-1' [last] -> 5.2") PKJkGoNgo_http/tests/test_optouts.py""" Tests for go_http.optouts. """ import json from unittest import TestCase from requests import HTTPError from requests_testadapter import TestAdapter, TestSession from go_http.optouts import OptOutsApiClient class RecordingAdapter(TestAdapter): """ Record the request that was handled by the adapter. """ request = None def send(self, request, *args, **kw): self.request = request return super(RecordingAdapter, self).send(request, *args, **kw) class TestOptOutsApiClient(TestCase): def setUp(self): self.session = TestSession() self.client = OptOutsApiClient( auth_token="auth-token", api_url="http://example.com/api/v1/go", session=self.session) def response_ok(self, data): data.update({ u'status': { u'reason': u'OK', u'code': 200, }, }) return data def test_default_session(self): import requests client = OptOutsApiClient( auth_token="auth-token") self.assertTrue(isinstance(client.session, requests.Session)) def test_default_api_url(self): client = OptOutsApiClient( auth_token="auth-token") self.assertEqual(client.api_url, "https://go.vumi.org/api/v1/go") def check_request(self, request, method, data=None, headers=None): self.assertEqual(request.method, method) if headers is not None: for key, value in headers.items(): self.assertEqual(request.headers[key], value) if data is None: self.assertEqual(request.body, None) else: self.assertEqual(json.loads(request.body), data) def test_error_response(self): response = { u'status': { u'reason': u'Bad request', u'code': 400, }, } adapter = RecordingAdapter(json.dumps(response), status=400) self.session.mount( "http://example.com/api/v1/go/" "optouts/msisdn/%2b1234", adapter) self.assertRaises( HTTPError, self.client.get_optout, "msisdn", "+1234") self.check_request( adapter.request, 'GET', headers={"Authorization": u'Bearer auth-token'}) def test_non_json_error_response(self): response = "Not JSON" adapter = RecordingAdapter(json.dumps(response), status=503) self.session.mount( "http://example.com/api/v1/go/" "optouts/msisdn/%2b1234", adapter) self.assertRaises( HTTPError, self.client.get_optout, "msisdn", "+1234") def test_get_optout(self): opt_out = { u'created_at': u'2015-11-10 20:33:03.742409', u'message': None, u'user_account': u'fxxxeee', } response = self.response_ok({u'opt_out': opt_out}) adapter = RecordingAdapter(json.dumps(response)) self.session.mount( "http://example.com/api/v1/go/" "optouts/msisdn/%2b1234", adapter) result = self.client.get_optout("msisdn", "+1234") self.assertEqual(result, opt_out) self.check_request( adapter.request, 'GET', headers={"Authorization": u'Bearer auth-token'}) def test_get_optout_not_found(self): response = { u'status': { u'reason': u'Opt out not found', u'code': 404, }, } adapter = RecordingAdapter(json.dumps(response), status=404) self.session.mount( "http://example.com/api/v1/go/" "optouts/msisdn/%2b1234", adapter) result = self.client.get_optout("msisdn", "+1234") self.assertEqual(result, None) def test_set_optout(self): opt_out = { u'created_at': u'2015-11-10 20:33:03.742409', u'message': None, u'user_account': u'fxxxeee', } response = self.response_ok({u'opt_out': opt_out}) adapter = RecordingAdapter(json.dumps(response)) self.session.mount( "http://example.com/api/v1/go/" "optouts/msisdn/%2b1234", adapter) result = self.client.set_optout("msisdn", "+1234") self.assertEqual(result, opt_out) self.check_request( adapter.request, 'PUT', headers={"Authorization": u'Bearer auth-token'}) def test_delete_optout(self): opt_out = { u'created_at': u'2015-11-10 20:33:03.742409', u'message': None, u'user_account': u'fxxxeee', } response = self.response_ok({u'opt_out': opt_out}) adapter = RecordingAdapter(json.dumps(response)) self.session.mount( "http://example.com/api/v1/go/" "optouts/msisdn/%2b1234", adapter) result = self.client.delete_optout("msisdn", "+1234") self.assertEqual(result, opt_out) self.check_request( adapter.request, 'DELETE', headers={"Authorization": u'Bearer auth-token'}) def test_delete_optout_not_found(self): response = { u'status': { u'reason': u'Opt out not found', u'code': 404, }, } adapter = RecordingAdapter(json.dumps(response), status=404) self.session.mount( "http://example.com/api/v1/go/" "optouts/msisdn/%2b1234", adapter) result = self.client.delete_optout("msisdn", "+1234") self.assertEqual(result, None) def test_count(self): response = self.response_ok({ u'opt_out_count': 2, }) adapter = RecordingAdapter(json.dumps(response)) self.session.mount( "http://example.com/api/v1/go/" "optouts/count", adapter) result = self.client.count() self.assertEqual(result, 2) self.check_request( adapter.request, 'GET', headers={"Authorization": u'Bearer auth-token'}) PKcOG|J0K0Kgo_http/tests/test_contacts.py""" Tests for go_http.contacts. """ from unittest import TestCase from requests import HTTPError from requests.adapters import HTTPAdapter from requests_testadapter import TestSession, Resp, TestAdapter from fake_go_contacts import Request, FakeContactsApi from go_http.contacts import ContactsApiClient from go_http.exceptions import PagedException class FakeContactsApiAdapter(HTTPAdapter): """ Adapter for FakeContactsApi. This inherits directly from HTTPAdapter instead of using TestAdapter because it overrides everything TestAdaptor does. """ def __init__(self, contacts_api): self.contacts_api = contacts_api super(FakeContactsApiAdapter, self).__init__() def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): req = Request( request.method, request.path_url, request.body, request.headers) resp = self.contacts_api.handle_request(req) response = Resp(resp.body, resp.code, resp.headers) r = self.build_response(request, response) if not stream: # force prefetching content unless streaming in use r.content return r make_contact_dict = FakeContactsApi.make_contact_dict make_group_dict = FakeContactsApi.make_group_dict class TestContactsApiClient(TestCase): API_URL = "http://example.com/go" AUTH_TOKEN = "auth_token" MAX_CONTACTS_PER_PAGE = 10 def setUp(self): self.contacts_data = {} self.groups_data = {} self.contacts_backend = FakeContactsApi( "go/", self.AUTH_TOKEN, self.contacts_data, self.groups_data, contacts_limit=self.MAX_CONTACTS_PER_PAGE) self.session = TestSession() self.adapter = FakeContactsApiAdapter(self.contacts_backend) self.simulate_api_up() def simulate_api_down(self): self.session.mount(self.API_URL, TestAdapter("API is down", 500)) def simulate_api_up(self): self.session.mount(self.API_URL, self.adapter) def make_client(self, auth_token=AUTH_TOKEN): return ContactsApiClient( auth_token, api_url=self.API_URL, session=self.session) def make_existing_contact(self, contact_data): existing_contact = make_contact_dict(contact_data) self.contacts_data[existing_contact[u"key"]] = existing_contact return existing_contact def make_existing_group(self, group_data): existing_group = make_group_dict(group_data) self.groups_data[existing_group[u'key']] = existing_group return existing_group def make_n_contacts(self, n, groups=None): contacts = [] for i in range(n): data = { u"msisdn": u"+155564%d" % (i,), u"name": u"Arthur", u"surname": u"of Camelot", } if groups is not None: data["groups"] = groups contacts.append(self.make_existing_contact(data)) return contacts def assert_contacts_equal(self, contacts_a, contacts_b): contacts_a.sort(key=lambda d: d['msisdn']) contacts_b.sort(key=lambda d: d['msisdn']) self.assertEqual(contacts_a, contacts_b) def assert_contact_status(self, contact_key, exists=True): exists_status = (contact_key in self.contacts_data) self.assertEqual(exists_status, exists) def assert_group_status(self, group_key, exists=True): exists_status = (group_key in self.groups_data) self.assertEqual(exists_status, exists) def assert_http_error(self, expected_status, func, *args, **kw): try: func(*args, **kw) except HTTPError as err: self.assertEqual(err.response.status_code, expected_status) else: self.fail( "Expected HTTPError with status %s." % (expected_status,)) def assert_paged_exception(self, f, *args, **kw): try: f(*args, **kw) except Exception as err: self.assertTrue(isinstance(err, PagedException)) self.assertTrue(isinstance(err.cursor, unicode)) self.assertTrue(isinstance(err.error, Exception)) return err def test_assert_http_error(self): self.session.mount("http://bad.example.com/", TestAdapter("", 500)) def bad_req(): r = self.session.get("http://bad.example.com/") r.raise_for_status() # Fails when no exception is raised. self.assertRaises( self.failureException, self.assert_http_error, 404, lambda: None) # Fails when an HTTPError with the wrong status code is raised. self.assertRaises( self.failureException, self.assert_http_error, 404, bad_req) # Passes when an HTTPError with the expected status code is raised. self.assert_http_error(500, bad_req) # Non-HTTPError exceptions aren't caught. def raise_error(): raise ValueError() self.assertRaises(ValueError, self.assert_http_error, 404, raise_error) def test_default_session(self): import requests contacts = ContactsApiClient(self.AUTH_TOKEN) self.assertTrue(isinstance(contacts.session, requests.Session)) def test_default_api_url(self): contacts = ContactsApiClient(self.AUTH_TOKEN) self.assertEqual( contacts.api_url, "https://go.vumi.org/api/v1/go") def test_auth_failure(self): contacts = self.make_client(auth_token="bogus_token") self.assert_http_error(403, contacts.get_contact, "foo") def test_contacts_single_page(self): [expected_contact] = self.make_n_contacts(1) contacts_api = self.make_client() [contact] = list(contacts_api.contacts()) self.assertEqual(contact, expected_contact) def test_contacts_no_results(self): contacts_api = self.make_client() contacts = list(contacts_api.contacts()) self.assertEqual(contacts, []) def test_contacts_multiple_pages(self): expected_contacts = self.make_n_contacts( self.MAX_CONTACTS_PER_PAGE + 1) contacts_api = self.make_client() contacts = list(contacts_api.contacts()) self.assert_contacts_equal(contacts, expected_contacts) def test_contacts_multiple_pages_with_cursor(self): expected_contacts = self.make_n_contacts( self.MAX_CONTACTS_PER_PAGE + 1) contacts_api = self.make_client() first_page = contacts_api._api_request("GET", "contacts", "") cursor = first_page['cursor'] contacts = list(contacts_api.contacts(start_cursor=cursor)) contacts.extend(first_page['data']) self.assert_contacts_equal(contacts, expected_contacts) def test_contacts_multiple_pages_with_failure(self): expected_contacts = self.make_n_contacts( self.MAX_CONTACTS_PER_PAGE + 1) contacts_api = self.make_client() it = contacts_api.contacts() contacts = [it.next() for _ in range(self.MAX_CONTACTS_PER_PAGE)] self.simulate_api_down() err = self.assert_paged_exception(it.next) self.simulate_api_up() [last_contact] = list(contacts_api.contacts(start_cursor=err.cursor)) self.assert_contacts_equal( contacts + [last_contact], expected_contacts) def test_create_contact(self): contacts = self.make_client() contact_data = { u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", } contact = contacts.create_contact(contact_data) expected_contact = make_contact_dict(contact_data) # The key is generated for us. expected_contact[u"key"] = contact[u"key"] self.assertEqual(contact, expected_contact) self.assert_contact_status(contact[u"key"], exists=True) def test_create_contact_with_extras(self): contacts = self.make_client() contact_data = { u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", u"extra": { u"quest": u"Grail", u"sidekick": u"Percy", }, } contact = contacts.create_contact(contact_data) expected_contact = make_contact_dict(contact_data) # The key is generated for us. expected_contact[u"key"] = contact[u"key"] self.assertEqual(contact, expected_contact) self.assert_contact_status(contact[u"key"], exists=True) def test_create_contact_with_key(self): contacts = self.make_client() contact_data = { u"key": u"foo", u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", } self.assert_http_error(400, contacts.create_contact, contact_data) self.assert_contact_status(u"foo", exists=False) def test_get_contact(self): contacts = self.make_client() existing_contact = self.make_existing_contact({ u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", }) contact = contacts.get_contact(existing_contact[u"key"]) self.assertEqual(contact, existing_contact) def test_get_contact_with_extras(self): contacts = self.make_client() existing_contact = self.make_existing_contact({ u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", u"extra": { u"quest": u"Grail", u"sidekick": u"Percy", }, }) contact = contacts.get_contact(existing_contact[u"key"]) self.assertEqual(contact, existing_contact) def test_get_missing_contact(self): contacts = self.make_client() self.assert_http_error(404, contacts.get_contact, "foo") def test_get_contact_from_field(self): contacts = self.make_client() existing_contact = self.make_existing_contact({ u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", }) contact = contacts.get_contact(msisdn='+15556483') self.assertEqual(contact, existing_contact) def test_get_contact_from_field_missing(self): contacts = self.make_client() self.make_existing_contact({ u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", }) self.assert_http_error( 400, contacts.get_contact, msisdn='+12345') def test_update_contact(self): contacts = self.make_client() existing_contact = self.make_existing_contact({ u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", }) new_contact = existing_contact.copy() new_contact[u"surname"] = u"Pendragon" contact = contacts.update_contact( existing_contact[u"key"], {u"surname": u"Pendragon"}) self.assertEqual(contact, new_contact) def test_update_contact_with_extras(self): contacts = self.make_client() existing_contact = self.make_existing_contact({ u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", u"extra": { u"quest": u"Grail", u"sidekick": u"Percy", }, }) new_contact = existing_contact.copy() new_contact[u"surname"] = u"Pendragon" new_contact[u"extra"] = { u"quest": u"lunch", u"knight": u"Lancelot", } contact = contacts.update_contact(existing_contact[u"key"], { u"surname": u"Pendragon", u"extra": { u"quest": u"lunch", u"knight": u"Lancelot", }, }) self.assertEqual(contact, new_contact) def test_update_missing_contact(self): contacts = self.make_client() self.assert_http_error(404, contacts.update_contact, "foo", {}) def test_delete_contact(self): contacts = self.make_client() existing_contact = self.make_existing_contact({ u"msisdn": u"+15556483", u"name": u"Arthur", u"surname": u"of Camelot", }) self.assert_contact_status(existing_contact[u"key"], exists=True) contact = contacts.delete_contact(existing_contact[u"key"]) self.assertEqual(contact, existing_contact) self.assert_contact_status(existing_contact[u"key"], exists=False) def test_delete_missing_contact(self): contacts = self.make_client() self.assert_http_error(404, contacts.delete_contact, "foo") def test_create_group(self): client = self.make_client() group_data = { u'name': u'Bob', } group = client.create_group(group_data) expected_group = make_group_dict(group_data) # The key is generated for us. expected_group[u'key'] = group[u'key'] self.assertEqual(group, expected_group) self.assert_group_status(group[u'key'], exists=True) def test_create_smart_group(self): client = self.make_client() group_data = { u'name': u'Bob', u'query': u'test-query', } group = client.create_group(group_data) expected_group = make_group_dict(group_data) # The key is generated for us expected_group[u'key'] = group[u'key'] self.assertEqual(group, expected_group) self.assert_group_status(group[u'key'], exists=True) def test_create_group_with_key(self): client = self.make_client() group_data = { u'key': u'foo', u'name': u'Bob', u'query': u'test-query', } self.assert_http_error(400, client.create_group, group_data) def test_get_group(self): client = self.make_client() existing_group = self.make_existing_group({ u'name': 'Bob', }) group = client.get_group(existing_group[u'key']) self.assertEqual(group, existing_group) def test_get_smart_group(self): client = self.make_client() existing_group = self.make_existing_group({ u'name': 'Bob', u'query': 'test-query', }) group = client.get_group(existing_group[u'key']) self.assertEqual(group, existing_group) def test_get_missing_group(self): client = self.make_client() self.assert_http_error(404, client.get_group, 'foo') def test_update_group(self): client = self.make_client() existing_group = self.make_existing_group({ u'name': u'Bob', }) new_group = existing_group.copy() new_group[u'name'] = u'Susan' group = client.update_group(existing_group[u'key'], {'name': 'Susan'}) self.assertEqual(existing_group, group) self.assertEqual(group, new_group) def test_update_smart_group(self): client = self.make_client() existing_group = self.make_existing_group({ u'name': u'Bob', u'query': u'test-query', }) new_group = existing_group.copy() new_group[u'query'] = u'another-query' group = client.update_group(existing_group[u'key'], {'query': 'another-query'}) self.assertEqual(existing_group, group) self.assertEqual(group, new_group) def test_update_missing_group(self): client = self.make_client() self.assert_http_error(404, client.update_group, 'foo', {}) def test_delete_group(self): client = self.make_client() existing_group = self.make_existing_group({ u'name': u'Bob' }) self.assert_group_status(existing_group[u'key'], exists=True) group = client.delete_group(existing_group[u'key']) self.assertEqual(existing_group, group) self.assert_group_status(group[u'key'], exists=False) def test_delete_missing_group(self): client = self.make_client() self.assert_http_error(404, client.delete_group, 'foo') def test_group_contacts_multiple_pages_with_cursor(self): self.make_existing_group({ u'name': 'key', }) expected_contacts = self.make_n_contacts( self.MAX_CONTACTS_PER_PAGE + 1, groups=["key"]) client = self.make_client() first_page = client._api_request("GET", "groups/key", "contacts") cursor = first_page['cursor'] contacts = list(client.group_contacts(group_key="key", start_cursor=cursor)) contacts.extend(first_page['data']) contacts.sort(key=lambda d: d['msisdn']) expected_contacts.sort(key=lambda d: d['msisdn']) self.assertEqual(contacts, expected_contacts) def test_group_contacts_multiple_pages(self): self.make_existing_group({ u'name': 'key', }) self.make_existing_group({ u'name': 'diffkey', }) expected_contacts = self.make_n_contacts( self.MAX_CONTACTS_PER_PAGE + 1, groups=["key"]) self.make_existing_contact({ u"msisdn": u"+1234567", u"name": u"Nancy", u"surname": u"of Camelot", u"groups": ["diffkey"], }) client = self.make_client() contacts = list(client.group_contacts("key")) self.assert_contacts_equal(contacts, expected_contacts) def test_group_contacts_multiple_pages_with_failure(self): self.make_existing_group({ u'name': 'key', }) self.make_existing_group({ u'name': 'diffkey', }) expected_contacts = self.make_n_contacts( self.MAX_CONTACTS_PER_PAGE + 1, groups=["key"]) self.make_existing_contact({ u"msisdn": u"+1234567", u"name": u"Nancy", u"surname": u"of Camelot", u"groups": ["diffkey"], }) contacts_api = self.make_client() it = contacts_api.group_contacts("key") contacts = [it.next() for _ in range(self.MAX_CONTACTS_PER_PAGE)] self.simulate_api_down() err = self.assert_paged_exception(it.next) self.simulate_api_up() [last_contact] = list(contacts_api.group_contacts( "key", start_cursor=err.cursor)) self.assert_contacts_equal( contacts + [last_contact], expected_contacts) def test_group_contacts_none_found(self): self.make_existing_group({ u'name': 'key', }) self.make_existing_group({ u'name': 'diffkey', }) self.make_n_contacts( self.MAX_CONTACTS_PER_PAGE + 1, groups=["diffkey"]) client = self.make_client() contacts = list(client.group_contacts("key")) self.assert_contacts_equal(contacts, []) PKJkG&Rq go_http/tests/test_metrics.py""" Tests for go_http.metrics. """ import urlparse import json from unittest import TestCase from requests_testadapter import TestAdapter, TestSession from go_http.metrics import MetricsApiClient class RecordingAdapter(TestAdapter): """ Record the request that was handled by the adapter. """ request = None def send(self, request, *args, **kw): self.request = request return super(RecordingAdapter, self).send(request, *args, **kw) class TestMetricApiClient(TestCase): def setUp(self): self.session = TestSession() self.client = MetricsApiClient( auth_token="auth-token", api_url="http://example.com/api/v1/go", session=self.session) def test_default_session(self): import requests client = MetricsApiClient( auth_token="auth-token") self.assertTrue(isinstance(client.session, requests.Session)) def test_default_api_url(self): client = MetricsApiClient( auth_token="auth-token") self.assertEqual(client.api_url, "https://go.vumi.org/api/v1/go") def check_request( self, request, method, params=None, data=None, headers=None): self.assertEqual(request.method, method) if params is not None: url = urlparse.urlparse(request.url) qs = urlparse.parse_qsl(url.query) self.assertEqual(dict(qs), params) if headers is not None: for key, value in headers.items(): self.assertEqual(request.headers[key], value) if data is None: self.assertEqual(request.body, None) else: self.assertEqual(json.loads(request.body), data) def test_get_metric(self): response = {u'stores.store_name.metric_name.agg': [{u'x': 1413936000000, u'y': 88916.0}, {u'x': 1414022400000, u'y': 91339.0}, {u'x': 1414108800000, u'y': 92490.0}, {u'x': 1414195200000, u'y': 92655.0}, {u'x': 1414281600000, u'y': 92786.0}]} adapter = RecordingAdapter(json.dumps(response)) self.session.mount( "http://example.com/api/v1/go/" "metrics/", adapter) result = self.client.get_metric( "stores.store_name.metric_name.agg", "-30d", "1d", "omit") self.assertEqual(result, response) self.check_request( adapter.request, 'GET', params={ "m": "stores.store_name.metric_name.agg", "interval": "1d", "from": "-30d", "nulls": "omit"}, headers={"Authorization": u'Bearer auth-token'}) def test_fire(self): response = [{ 'name': 'foo.last', 'value': 3.1415, 'aggregator': 'last', }] adapter = RecordingAdapter(json.dumps(response)) self.session.mount( "http://example.com/api/v1/go/" "metrics/", adapter) result = self.client.fire({ "foo.last": 3.1415, }) self.assertEqual(result, response) self.check_request( adapter.request, 'POST', data={"foo.last": 3.1415}, headers={"Authorization": u'Bearer auth-token'}) PKcOGgo_http/tests/__init__.pyPKcOGnK77 go_http/tests/test_exceptions.py""" Tests for go_http.exceptions. """ from unittest import TestCase from go_http.exceptions import PagedException class TestPagedException(TestCase): def test_creation(self): err = ValueError("Testing Error") p = PagedException(u"12345", err) self.assertTrue(isinstance(p, Exception)) self.assertEqual(p.cursor, u"12345") self.assertEqual(p.error, err) def test_repr(self): p = PagedException(u"abcde", ValueError("Test ABC")) self.assertEqual( repr(p), "") def test_str(self): p = PagedException(u"lmnop", ValueError("Test LMN")) self.assertEqual( str(p), "") PKJkGھ'go_http-0.3.0.dist-info/DESCRIPTION.rstVumi Go HTTP API client ======================= A client library for the `Vumi Go`_ HTTP API. .. _Vumi Go: http://github.com/praekelt/vumi-go |gha-ci|_ |gha-cover|_ .. |gha-ci| image:: https://travis-ci.org/praekelt/go-http-api.png?branch=develop .. _gha-ci: https://travis-ci.org/praekelt/go-http-api .. |gha-cover| image:: https://coveralls.io/repos/praekelt/go-http-api/badge.png?branch=develop .. _gha-cover: https://coveralls.io/r/praekelt/go-http-api You can contact the Vumi development team in the following ways: * via *email* by joining the the `vumi-dev@googlegroups.com`_ mailing list * on *irc* in *#vumi* on the `Freenode IRC network`_ .. _vumi-dev@googlegroups.com: https://groups.google.com/forum/?fromgroups#!forum/vumi-dev .. _Freenode IRC network: https://webchat.freenode.net/?channels=#vumi Issues can be filed in the GitHub issue tracker. Please don't use the issue tracker for general support queries. PKJkG)%go_http-0.3.0.dist-info/metadata.json{"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: POSIX", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Networking"], "extensions": {"python.details": {"contacts": [{"email": "dev@praekeltfoundation.org", "name": "Praekelt Foundation", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/praekelt/go-http-api"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "license": "BSD", "metadata_version": "2.0", "name": "go-http", "run_requires": [{"requires": ["requests (>=2)"]}], "summary": "A client library for Vumi Go's HTTP API", "version": "0.3.0"}PKJkGƫ'%go_http-0.3.0.dist-info/top_level.txtgo_http PKJkG''\\go_http-0.3.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PKJkG=2) Vumi Go HTTP API client ======================= A client library for the `Vumi Go`_ HTTP API. .. _Vumi Go: http://github.com/praekelt/vumi-go |gha-ci|_ |gha-cover|_ .. |gha-ci| image:: https://travis-ci.org/praekelt/go-http-api.png?branch=develop .. _gha-ci: https://travis-ci.org/praekelt/go-http-api .. |gha-cover| image:: https://coveralls.io/repos/praekelt/go-http-api/badge.png?branch=develop .. _gha-cover: https://coveralls.io/r/praekelt/go-http-api You can contact the Vumi development team in the following ways: * via *email* by joining the the `vumi-dev@googlegroups.com`_ mailing list * on *irc* in *#vumi* on the `Freenode IRC network`_ .. _vumi-dev@googlegroups.com: https://groups.google.com/forum/?fromgroups#!forum/vumi-dev .. _Freenode IRC network: https://webchat.freenode.net/?channels=#vumi Issues can be filed in the GitHub issue tracker. Please don't use the issue tracker for general support queries. PKJkG݁]1go_http-0.3.0.dist-info/RECORDgo_http/__init__.py,sha256=bVBGEydnxJTkZy5VMiClSl6QN8Do2kEgTMo5-gvNZuE,163 go_http/contacts.py,sha256=PAGE4xOLlPlm4HmoqWdpx_tAIOT1h40ikenYBZbjQ44,6930 go_http/exceptions.py,sha256=NYJrZwmdn8V8uIkNJV0UGhFiKRs2CXgJzzSahn00Lt4,1033 go_http/metrics.py,sha256=BUGRfRSiLi-Ui2x7g_SIY_JU-AiNtPO7R1bi99qO7K4,3553 go_http/optouts.py,sha256=b-a0gDRspfm58HiCZFZX3TKyVvs1Bxd3AQ5RqiJkSpo,4396 go_http/send.py,sha256=5ZEmNoRvcxEvClnKyOvM02OU-NwTJxRhwifQjmX5EaE,7770 go_http/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 go_http/tests/test_contacts.py,sha256=xbzUysO8VT35UZ1up3Kd2WjxC-BCLyt7N3p_xjXKTUo,19248 go_http/tests/test_exceptions.py,sha256=5npqG7kcGJ3LWQl6Crlfm9pZUwoZULytecvCpN-m4sY,823 go_http/tests/test_metrics.py,sha256=44PeVexdSGdWD-ZiHdenvnLUC1U0xr_z0goCQn8tiHI,3471 go_http/tests/test_optouts.py,sha256=2r5icEy0vkX4bkpkt8fbm-uIxJv5OLYm5_Yd7okFl0M,6137 go_http/tests/test_send.py,sha256=yTvjph7PEVC97njs7I0kWAcPN03DqbnTDD_8KpAzwz4,11798 go_http-0.3.0.dist-info/DESCRIPTION.rst,sha256=T1ZEAoHqPaNCAseI0fmDxmsg-nwE4WUmqtYMkTW3e64,936 go_http-0.3.0.dist-info/METADATA,sha256=uA24WKlz9ltJmSfJch1IgrxtTC8POA8R4RY99nPHxmo,1551 go_http-0.3.0.dist-info/RECORD,, go_http-0.3.0.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 go_http-0.3.0.dist-info/metadata.json,sha256=Y__iEy6MtdbZJGneuhX4EMyhJaauc0UCs5fkLGPMxHk,791 go_http-0.3.0.dist-info/top_level.txt,sha256=ypocd8_wK07cg1MEbIzkneJMaQJ1ciLCD0SzGGOrmIk,8 PKcOG kxr go_http/metrics.pyPKcOGTTgo_http/contacts.pyPKJkGl)T)go_http/__init__.pyPKJkGuZZ(*go_http/send.pyPKJkGrX,,Hgo_http/optouts.pyPKcOG8<.   Zgo_http/exceptions.pyPKJkG:..G^go_http/tests/test_send.pyPKJkGoNgo_http/tests/test_optouts.pyPKcOG|J0K0Kɤgo_http/tests/test_contacts.pyPKJkG&Rq 5go_http/tests/test_metrics.pyPKcOGgo_http/tests/__init__.pyPKcOGnK77 6go_http/tests/test_exceptions.pyPKJkGھ'go_http-0.3.0.dist-info/DESCRIPTION.rstPKJkG)%go_http-0.3.0.dist-info/metadata.jsonPKJkGƫ'%go_http-0.3.0.dist-info/top_level.txtPKJkG''\\= go_http-0.3.0.dist-info/WHEELPKJkG