diff --git a/CHANGES.txt b/CHANGES.txt index d026e5d..e39c937 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,3 +24,4 @@ v0.5.2, Tue March 7 -- Required API keys, configuration settings v0.5.3, Wed Apr 15 -- Added scipy to requirements, edited Readme to not break pypi page v0.6.0, Thu May 29 -- Remove numpy / scipy dependency in favor of Pillow v0.7.0, Tue Jun 9 -- Added support for calling multiple APIs in a single function and accepting filenames as image API inputs +v0.7.1 Thu Jun 11 -- High quality sentiment API for private beta, fix for multi API support diff --git a/indicoio/__init__.py b/indicoio/__init__.py index 4b0af6b..b766713 100644 --- a/indicoio/__init__.py +++ b/indicoio/__init__.py @@ -1,6 +1,6 @@ from functools import partial -Version, version, __version__, VERSION = ('0.7.0',) * 4 +Version, version, __version__, VERSION = ('0.7.1',) * 4 JSON_HEADERS = { 'Content-type': 'application/json', @@ -9,7 +9,7 @@ JSON_HEADERS = { 'version-number': VERSION } -from indicoio.text.sentiment import political, posneg +from indicoio.text.sentiment import political, posneg, sentiment_hq from indicoio.text.sentiment import posneg as sentiment from indicoio.text.lang import language from indicoio.text.tagging import text_tags diff --git a/indicoio/config.py b/indicoio/config.py index 895c76a..0b24332 100644 --- a/indicoio/config.py +++ b/indicoio/config.py @@ -49,7 +49,8 @@ TEXT_APIS = [ 'text_tags', 'political', 'sentiment', - 'language' + 'language', + 'sentiment_hq' ] IMAGE_APIS = [ diff --git a/indicoio/text/sentiment.py b/indicoio/text/sentiment.py index 182908a..5172e40 100644 --- a/indicoio/text/sentiment.py +++ b/indicoio/text/sentiment.py @@ -50,3 +50,26 @@ def posneg(text, cloud=None, batch=False, api_key=None, **kwargs): """ return api_handler(text, cloud=cloud, api="sentiment", url_params={"batch":batch, "api_key":api_key}, **kwargs) + +def sentiment_hq(text, cloud=None, batch=False, api_key=None, **kwargs): + """ + Given input text, returns a scalar estimate of the sentiment of that text. + Values are roughly in the range 0 to 1 with 0.5 indicating neutral sentiment. + For reference, 0 suggests very negative sentiment and 1 suggests very positive sentiment. + + Example usage: + + .. code-block:: python + + >>> from indicoio import sentimenthq + >>> text = 'Thanks everyone for the birthday wishes!! It was a crazy few days ><' + >>> sentiment = sentimenthq(text) + >>> sentiment + 0.6210052967071533 + + :param text: The text to be analyzed. + :type text: str or unicode + :rtype: Float + """ + + return api_handler(text, cloud=cloud, api="sentimenthq", url_params={"batch":batch, "api_key":api_key}, **kwargs) diff --git a/indicoio/utils/api.py b/indicoio/utils/api.py index 71a43b5..664e441 100644 --- a/indicoio/utils/api.py +++ b/indicoio/utils/api.py @@ -17,6 +17,7 @@ def api_handler(arg, cloud, api, url_params = {"batch":False, "api_key":None}, * if cloud: host = "%s.indico.domains" % cloud + else: # default to indico public cloud host = config.PUBLIC_API_HOST @@ -24,8 +25,14 @@ def api_handler(arg, cloud, api, url_params = {"batch":False, "api_key":None}, * url = config.url_protocol + "//%s/%s" % (host, api) url = url + "/batch" if url_params.get("batch", False) else url url += "?key=%s" % (url_params.get("api_key", None) or config.api_key) - if "apis" in url_params: - url += "&apis=%s" % ",".join(url_params["apis"]) + apis = url_params.get("apis", []) + if apis: + url += "&apis=%s" % ",".join(apis) + + # private beta + if host == config.PUBLIC_API_HOST: + if (api == 'sentimenthq') or ('sentimenthq' in apis): + raise IndicoError("The high quality sentiment API is currently in private beta.") response = requests.post(url, data=json_data, headers=JSON_HEADERS) if response.status_code == 503 and cloud != None: diff --git a/indicoio/utils/multi.py b/indicoio/utils/multi.py index 31725c3..94a4c7d 100644 --- a/indicoio/utils/multi.py +++ b/indicoio/utils/multi.py @@ -6,8 +6,12 @@ from indicoio.utils.errors import IndicoError CLIENT_SERVER_MAP = dict((api, api.strip().replace("_", "").lower()) for api in API_NAMES) SERVER_CLIENT_MAP = dict((v, k) for k, v in CLIENT_SERVER_MAP.iteritems()) +AVAILABLE_APIS = { + 'text': TEXT_APIS, + 'image': IMAGE_APIS +} -def multi(data, type, apis, available, batch=False, **kwargs): +def multi(data, datatype, apis, batch=False, **kwargs): """ Helper to make multi requests of different types. @@ -22,23 +26,39 @@ def multi(data, type, apis, available, batch=False, **kwargs): :rtype: Dictionary of api responses """ # Client side api name checking - strictly only accept func name api + available = AVAILABLE_APIS.get(datatype) invalid_apis = [api for api in apis if api not in available] if invalid_apis: - raise IndicoError("%s are not valid %s APIs. Please reference the available APIs below:\n%s" - % (", ".join(invalid_apis), type, ", ".join(available)) - ) + raise IndicoError( + "%s are not valid %s APIs. Please reference the available APIs below:\n%s" + % (", ".join(invalid_apis), datatype, ", ".join(available)) + ) + # Convert client api names to server names before sending request apis = map(CLIENT_SERVER_MAP.get, apis) - result = api_handler(data, url_params = {"apis":apis, "batch":batch}, **kwargs) + cloud = kwargs.pop("cloud", None) + api_key = kwargs.pop('api_key', None) + result = api_handler( + data, + cloud=cloud, + api='apis', + url_params={ + "apis":apis, + "batch":batch, + "api_key":api_key + }, + **kwargs + ) return handle_response(result) + def handle_response(result): # Parse out the results to a dicionary of api: result return dict((SERVER_CLIENT_MAP[api], parsed_response(api, res)) for api, res in result.iteritems()) -def predict_text(input_text, apis=TEXT_APIS, cloud=None, batch=False, api_key=None, **kwargs): +def predict_text(input_text, apis=TEXT_APIS, **kwargs): """ Given input text, returns the results of specified text apis. Possible apis include: [ 'text_tags', 'political', 'sentiment', 'language' ] @@ -60,19 +80,22 @@ def predict_text(input_text, apis=TEXT_APIS, cloud=None, batch=False, api_key=No :rtype: Dictionary of api responses """ + cloud = kwargs.pop('cloud', None) + batch = kwargs.pop('batch', False) + api_key = kwargs.pop('api_key', None) + return multi( - api="apis", data=input_text, - type="text", - available = TEXT_APIS, + datatype="text", cloud=cloud, batch=batch, api_key=api_key, apis=apis, - **kwargs) + **kwargs + ) -def predict_image(image, apis=IMAGE_APIS, cloud=None, batch=False, api_key=None, **kwargs): +def predict_image(image, apis=IMAGE_APIS, **kwargs): """ Given input image, returns the results of specified image apis. Possible apis include: ['fer', 'facial_features', 'image_features'] @@ -95,16 +118,19 @@ def predict_image(image, apis=IMAGE_APIS, cloud=None, batch=False, api_key=None, :rtype: Dictionary of api responses """ + cloud = kwargs.pop('cloud', None) + batch = kwargs.pop('batch', False) + api_key = kwargs.pop('api_key', None) + return multi( - api="apis", data=image_preprocess(image, batch=batch), - type="image", - available=IMAGE_APIS, + datatype="image", cloud=cloud, batch=batch, api_key=api_key, apis=apis, - **kwargs) + **kwargs + ) def parsed_response(api, response): result = response.get('results', False) diff --git a/tests/test_remote.py b/tests/test_remote.py index dcb0f81..0e80b0b 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -9,9 +9,13 @@ from indicoio import config from indicoio import political, sentiment, fer, facial_features, language, image_features, text_tags from indicoio import batch_political, batch_sentiment, batch_fer, batch_facial_features from indicoio import batch_language, batch_image_features, batch_text_tags +from indicoio import sentiment_hq, batch_sentiment_hq from indicoio import predict_image, predict_text, batch_predict_image, batch_predict_text from indicoio.utils.errors import IndicoError +# TODO: remove once sentiment_hq is added to the public API +config.TEXT_APIS.remove("sentiment_hq") + DIR = os.path.dirname(os.path.realpath(__file__)) class BatchAPIRun(unittest.TestCase): @@ -37,6 +41,13 @@ class BatchAPIRun(unittest.TestCase): self.assertTrue(isinstance(response, list)) self.assertTrue(response[0] < 0.5) + # TODO: uncomment once the high quality sentiment API is publicly released + # def test_batch_sentiment_hq(self): + # test_data = ['Worst song ever', 'Best song ever'] + # response = batch_sentiment_hq(test_data, api_key=self.api_key) + # self.assertTrue(isinstance(response, list)) + # self.assertTrue(response[0] < 0.5) + def test_batch_political(self): test_data = ["Guns don't kill people, people kill people."] response = batch_political(test_data, api_key=self.api_key) @@ -220,6 +231,19 @@ class FullAPIRun(unittest.TestCase): self.assertTrue(isinstance(response, float)) self.assertTrue(response > 0.5) + # TODO: uncomment when the high quality sentiment API is publicly released + # def test_sentiment_hq(self): + # test_string = "Worst song ever." + # response = sentiment_hq(test_string) + + # self.assertTrue(isinstance(response, float)) + # self.assertTrue(response < 0.5) + + # test_string = "Best song ever." + # response = sentiment_hq(test_string) + # self.assertTrue(isinstance(response, float)) + # self.assertTrue(response > 0.5) + def test_good_fer(self): fer_set = set(['Angry', 'Sad', 'Neutral', 'Surprise', 'Fear', 'Happy']) test_face = generate_array((48,48))