diff --git a/.gitignore b/.gitignore index 3918b12..da2aef1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ dist/ my_info *.pyc -unbabel_py.egg-info/ \ No newline at end of file +unbabel_py.egg-info/ +.idea +.DS_Store +*.swp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5376ceb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Unbabel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6582c42..2d2f465 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Python SDK for the Unbabel REST API Documentation: ============= -Please visit our documentation page at https://github.com/Unbabel +Please visit our documentation page at https://github.com/Unbabel/unbabel_api @@ -25,7 +25,7 @@ Getting started: ``` api.post_translations(text="This is a test translation",target_language="pt", - source_language="en", + source_language="en", callback_url="http://my-awesome-service.com/unbabel_endpoint") ``` @@ -35,7 +35,7 @@ Check out the [API documentation](https://github.com/Unbabel/unbabel_api#transla Returns a translation by its uid. -`t = api.get_translation(uid)` +`t = api.get_translation(uid)` @@ -47,18 +47,18 @@ Returns all the translations for a given user. -## Getting Available Language Pairs +## Getting Available Language Pairs `api.get_language_pairs()` > [pt_en, pt_fr, - ... + ... it_en, it_fr, it_es] - - Each element of the list is a **LanguagePair** object that contains a source language and a target language. Each language is an instance of the **Language** class that contains a shortname ( iso639-1 language code ) and a name. + + Each element of the list is a **LanguagePair** object that contains a source language and a target language. Each language is an instance of the **Language** class that contains a shortname ( iso639-1 language code ) and a name. ## Getting Available Tones diff --git a/localhost.env b/localhost.env new file mode 100644 index 0000000..2287637 --- /dev/null +++ b/localhost.env @@ -0,0 +1,3 @@ +UNBABEL_TEST_USERNAME=gracaninja +UNBABEL_TEST_API_KEY=5a6406e31f77ef779c4024b1579f0f6103944c5e +UNBABEL_TEST_API_URL=http://0.0.0.0:8000/tapi/v2/ \ No newline at end of file diff --git a/requirements.py b/requirements.py index f229360..1190bd8 100644 --- a/requirements.py +++ b/requirements.py @@ -1 +1,2 @@ requests +beautifulsoup4 diff --git a/runtests b/runtests new file mode 100755 index 0000000..435b218 --- /dev/null +++ b/runtests @@ -0,0 +1,2 @@ +export $(cat .env) +python -m unittest discover -s 'tests/' -p '*test*.py' \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 224a779..b88034e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -description-file = README.md \ No newline at end of file +description-file = README.md diff --git a/setup.py b/setup.py index b5c9bb2..c22f97f 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,21 @@ from setuptools import setup setup(name='unbabel-py', - version='0.27', + version='0.50', description='Python Wrapper around Unbabel HTTP API', author='Joao Graca', - author_email='gracaninja@unbabel.co', - packages=[ - 'unbabel', - ], - package_dir={ - 'unbabel': 'unbabel/', - }, + author_email='joao@unbabel.com', + packages=['unbabel'], + package_dir={'unbabel': 'unbabel',}, install_requires=[ - 'requests', - ], - url = 'https://github.com/Unbabel/unbabel-py', - download_url = 'https://github.com/Unbabel/unbabel-py/tarball/0.1', - classifiers = ['Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Programming Language :: Python ', - 'Topic :: Text Processing' - ] + 'requests', + 'beautifulsoup4', + ], + url='https://github.com/Unbabel/unbabel-py', + download_url='https://github.com/Unbabel/unbabel-py/archive/0.50.tar.gz', + classifiers=['Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Programming Language :: Python ', + 'Topic :: Text Processing' + ] ) diff --git a/tests/tests.py b/tests/tests.py index 0b6ec1a..23951a6 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,11 +1,215 @@ +# -*- coding: utf-8 -*- ''' Created on Jan 3, 2014 @author: joaograca ''' +import os import unittest +import uuid +from unbabel.api import (UnbabelApi, Order, LangPair, Tone, Topic, + Translation, Account, Job, BadRequestException) -class TestGetLanguages(unittest.TestCase): - pass - \ No newline at end of file +class TestUnbabelAPI(unittest.TestCase): + UNBABEL_TEST_USERNAME = os.environ.get('UNBABEL_TEST_USERNAME') + UNBABEL_TEST_API_KEY = os.environ.get('UNBABEL_TEST_API_KEY') + UNBABEL_TEST_API_URL = os.environ.get('UNBABEL_TEST_API_URL') + + @property + def api(self): + if not hasattr(self, '_api'): + self._api = UnbabelApi(username = self.UNBABEL_TEST_USERNAME, + api_key = self.UNBABEL_TEST_API_KEY, + sandbox = True) + self._api.api_url = self.UNBABEL_TEST_API_URL + return self._api + + def test_api_get_language_pairs(self): + pairs = self.api.get_language_pairs() + + self.assertIsInstance(pairs, list, 'Got something that is not a list') + self.assertGreater(len(pairs), 0, 'Got 0 pairs') + self.assertTrue( + reduce(lambda x, y: x and y, + [isinstance(p, LangPair) for p in pairs]), + 'The pairs are not all instance of LangPair') + + def test_api_get_available_tones(self): + tones = self.api.get_tones() + + self.assertIsInstance(tones, list, 'Got something that is not a list') + self.assertGreater(len(tones), 0, 'Got 0 tones') + self.assertTrue( + reduce(lambda x, y: x and y, + [isinstance(t, Tone) for t in tones]), + 'The tones are not all instance of Tone') + + def test_api_get_topics(self): + topics = self.api.get_topics() + + self.assertIsInstance(topics, list, 'Got something that is not a list') + self.assertGreater(len(topics), 0, 'Got 0 topics') + self.assertTrue( + reduce(lambda x, y: x and y, + [isinstance(t, Topic) for t in topics]), + 'The topics are not all instance of Topic') + + def test_api_post_translation(self): + data = { + 'text': "This is a test translation", + 'source_language': 'en', + 'target_language': 'pt', + } + account = self.api.get_account() + translation = self.api.post_translations(**data) + self.assertIsInstance(translation, Translation, + 'Should get a Translation instance') + self.assertIsNotNone(translation.uid, 'Did not get a uid') + self.assertGreater(translation.price, 0, 'Price is not greater than 0') + self.assertEqual(translation.source_language, 'en', + 'Source language is not en but %s'%translation.source_language) + self.assertEqual(translation.target_language, 'pt', + 'Target language is not pt but %s'%translation.target_language) + self.assertEqual(translation.text, data['text']) + self.assertEqual(translation.status, 'New', 'Wrong status: [{}]'.format(translation.status)) + self.assertIsNone(translation.topics, 'Topics is not None') + self.assertIsInstance(translation.translators, list, + 'Translators is not a list') + self.assertIsNone(translation.translation, 'Got a translation') + + account2 = self.api.get_account() + self.assertEqual(account.balance, account2.balance + translation.price, + "Balance inconsistency after post translation") + + trans = self.api.get_translation(translation.uid) + self.assertEqual(translation.uid, trans.uid, 'uids not equal') + self.assertEqual(translation.source_language, trans.source_language, + 'source language not equal') + self.assertEqual(translation.source_language, trans.source_language, + 'target language not equal') + self.assertEqual(translation.price, trans.price, 'price not equal') + self.assertEqual(translation.text, trans.text, 'text not equal') + + + def test_api_get_account(self): + account = self.api.get_account() + self.assertIsInstance(account, Account, + 'Should be an Account instance') + self.assertIsInstance(account.username, unicode, + 'Username is not unicode') + self.assertEqual(account.username, self.UNBABEL_TEST_USERNAME, + 'Wrong username') + self.assertIsInstance(account.balance, float, 'Balance is not float') + self.assertIsInstance(account.email, unicode, 'Email is not unicode') + + def test_api_get_translations(self): + data = { + 'text': "This is a test translation", + 'source_language': 'en', + 'target_language': 'pt', + } + self.api.post_translations(**data) + translations = self.api.get_translations() + self.assertIsInstance(translations, list, + 'Translations is not a list!') + self.assertTrue(len(translations) > 0, + 'Translations list is empty!') + self.assertTrue(all(isinstance(t, Translation) for t in translations), + 'Items are not all instance of Translation') + + def test_order_post(self): + order = self.api.post_order() + self.assertIsInstance(order, Order, 'Result is not an Order') + self.assertIsNotNone(order.id, 'ID is None') + self.assertEqual(order.price, 0, 'Price is not 0') + + def test_job_add_job_to_order(self): + order = self.api.post_order() + + data = { + 'order_id': order.id, + 'text': "This is a test translation", + 'source_language': 'en', + 'target_language': 'pt', + } + + job = self.api.post_job(**data) + self.assertIsInstance(job, Job) + self.assertEqual(job.order_id, order.id, 'Order ID is not equal') + self.assertEqual(job.text, data['text'], 'Job text is not correct') + self.assertEqual(job.source_language, data['source_language'], + 'Job source_language is not correct') + self.assertEqual(job.target_language, data['target_language'], + 'Job target_language is not correct') + + def test_job_fail_mandatory_fields(self): + self.assertRaises(BadRequestException, self.api.post_job, 0, '', '', '') + + def test_api_unauthorized_call(self): + api = self.api + self._api = UnbabelApi(username='fake_username', + api_key='fake_api_key') + + pairs = self.api.get_language_pairs() + self.assertIsInstance(pairs, list, 'Got something that is not a list') + + self._api = api + + def test_job_add_job_to_order_all_params(self): + order = self.api.post_order() + + data = { + 'order_id': order.id, + 'uid': uuid.uuid4().hex, + 'text': "This is a test translation", + 'source_language': 'en', + 'target_language': 'pt', + 'target_text': u"Isto é uma tradução de teste", + 'text_format': 'text', + 'tone': 'Informal', + 'topic': ['startups', 'tech'], + 'visibility': 'private', + 'instructions': "Ok people, there's nothing to see here. go home!", + 'public_url': 'http://google.com', + 'callback_url': 'http://dev.unbabel.com/', + 'job_type': 'paid', + } + + job = self.api.post_job(**data) + self.assertIsInstance(job, Job) + self.assertEqual(job.order_id, order.id, 'Order ID is not equal') + self.assertEqual(job.text, data['text'], 'Job text is not correct') + self.assertEqual(job.source_language, data['source_language'], + 'Job source_language is not correct') + self.assertEqual(job.target_language, data['target_language'], + 'Job target_language is not correct') + + def test_job_no_balance(self): + self.UNBABEL_TEST_USERNAME="gracaninja-1" + self.UNBABEL_TEST_API_KEY="d004dd5659e177d11ef9c22798b767a264f74b17" + if hasattr(self, '_api'): delattr(self, '_api') + + data = { + 'text': "This is a test translation", + 'source_language': 'en', + 'target_language': 'pt', + 'ttype': 'paid' + } + + self.assertEqual(self.api.username,"gracaninja-1","API Username not correct %s"%self.api.username) + self.assertEqual(self.api.api_key,"d004dd5659e177d11ef9c22798b767a264f74b17","API key not correct %s"%self.api.api_key) + + translation = self.api.post_translations(**data) + self.assertEqual(translation.status, "insufficient_balance", 'Job status not insufficient_balance but %s'%translation.status) + + self.UNBABEL_TEST_USERNAME = os.environ.get('UNBABEL_TEST_USERNAME') + self.UNBABEL_TEST_API_KEY = os.environ.get('UNBABEL_TEST_API_KEY') + if hasattr(self, '_api'): delattr(self, '_api') + + def test_pay_order(self): + order = self.api.post_order() + self.assertTrue(self.api.pay_order(order.id)) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/unbabel/__init__.py b/unbabel/__init__.py index ed5eca9..ef65117 100644 --- a/unbabel/__init__.py +++ b/unbabel/__init__.py @@ -1,3 +1,4 @@ +import unbabel.api LOG_CONFIG = { 'version': 1, diff --git a/unbabel/abc.py b/unbabel/abc.py new file mode 100644 index 0000000..67ade5a --- /dev/null +++ b/unbabel/abc.py @@ -0,0 +1,102 @@ +"""Abstract base classes. Mainly Pythonic container objects from JSON.""" + +class NamedObject(object): + """Abstract object that has a name and its name is the __repr__ and __str__.""" + def __init__(self, name): + self.name + def __repr__(self): + return self.name + def __str__(self): + return self.name + + +class Language(NamedObject): + def __init__(self, shortname, name): + super(Language, self).__init__(name=name) + self.shortname = shortname + + +class Tone(NamedObject): + def __init__(self, description, name): + super(Language, self).__init__(name=name) + self.description = description + + +class Topic(NamedObject): + def __init__(self, name): + super(Language, self).__init__(name=name) + + +class LangPair(NamedObject): + def __init__(self, source_language, target_language): + name = "{}_{}".format(source_language, target_language) + super(Language, self).__init__(name=name) + self.source_language = source_language + self.target_language = target_language + + +class Translator(object): + def __init__(self, first_name="", last_name="", + picture_url="", profile_url=""): + self.first_name = first_name + self.last_name = last_name + self.picture_url = picture_url + self.profile_url = profile_url + + @classmethod + def from_json(cls, json_object): + return Translator(json_object["first_name"], json_object["last_name"], + json_object["picture_url"], json_object["profile_url"]) + + +class UnicodeReprObject: + """Abstract object that suports unicode representation objects.""" + def __init__(self): + pass + def __repr__(self): + return self._repr + def __str__(self): + return self._repr + def __unicode__(self): + return self._repr + + +class Account(UnicodeReprObject): + def __init__(self, username, email, balance): + self.username = username + self.email = email + self.balance = balance + # For __unicode__ + self._repr = u'email: {email}, balance: {balance}'.format( + email=self.email, balance=self.balance) + + +class Job(UnicodeReprObject): + def __init__(self, id, uid, order_id, status, source_language, + target_language, + text, price, tone, text_format): + self.id = id + self.uid = uid + self.order_id = order_id + self.status = status + self.text = text + self.price = price + self.source_language = source_language + self.target_language = target_language + self.tone = tone + self.text_format = text_format + # For __unicode__ + self._repr = u'order_id: {}, id: {}, status: {}'.format(self.order_id, self.id, self.status) + + +class Order(UnicodeReprObject): + def __init__(self, id, status, price): + self.id = id + self.status = status + self.price = price + # For __unicode__ + self._repr = u'{id} - {status} - {price}'.format(id=self.id, status=self.status, price=self.price) + + +__all__ = ['Language', 'Tone', 'Topic', 'LangPair', + 'Translator', 'Account', 'Job', 'Order'] diff --git a/unbabel/api.py b/unbabel/api.py index 888fa67..fda48dc 100644 --- a/unbabel/api.py +++ b/unbabel/api.py @@ -1,161 +1,70 @@ -''' -Created on Dec 13, 2013 - -@author: joaograca -''' - +import json +import os import copy +import logging import requests -import json -import logging -logger = logging.getLogger() - -class UnauthorizedException(Exception): - - def __init__(self, value): - self.value = value - def __str__(self): - return repr(self.value) - -class BadRequestException(Exception): - - def __init__(self, value): - self.value = value - def __str__(self): - return repr(self.value) - -class Language(object): - - def __init__(self,shortname,name): - self.shortname = shortname - self.name = name - - def __repr__(self): - return self.name - - def __str__(self): - return self.name - - - -class Tone(object): - - def __init__(self,description,name): - self.description = description - self.name = name - - def __repr__(self): - return self.name - - def __str__(self): - return self.name - - -class Topic(object): - - def __init__(self,name): - self.name = name - - def __repr__(self): - return self.name - - def __str__(self): - return self.name - - -class LangPair(object): - - def __init__(self,source_language,target_language): - self.source_language = source_language - self.target_language = target_language - - def __repr__(self): - return "%s_%s"%(self.source_language.shortname, self.target_language.shortname) - - def __str__(self): - return "%s_%s"%(self.source_language.shortname, self.target_language.shortname) - -class Translator(object): - - def __init__(self,first_name="",last_name="",picture_url="",profile_url=""): - self.first_name = first_name - self.last_name = last_name - self.picture_url = picture_url - self.profile_url = profile_url - - @classmethod - def from_json(cls,json): - t = Translator(json["first_name"],json["last_name"],json["picture_url"],json["profile_url"]) - return t - -class Translation(object): - - def __init__(self, - uid=-1, - text="", - translatedText=None, - target_language="", - source_language=None, - status=None, - translators=[], - topics=None, - price=None, - balance=None, - **kwargs): - self.uid = uid - self.text = text - self.translation = translatedText - self.source_language = source_language - self.target_language = target_language - self.status = status - self.translators = translators - self.topics = topics - self.price = price - self.balance = balance - - def __repr__(self): - return "%s %s %s_%s_%s"%(self.uid,self.status,self.source_language, self.target_language, self.balance) - - def __str__(self): - return "%s %s %s_%s_%s"%(self.uid,self.status,self.source_language, self.target_language, self.balance) +from unbabel.abc import * +from unbabel.exceptions import UnauthorizedException, BadRequestException +from unbabel.translations import Translation, MTTranslation + +log = logging.getLogger() + +UNBABEL_SANDBOX_API_URL = os.environ.get( + 'UNBABEL_SANDOX_API_URL', 'https://sandbox.unbabel.com/tapi/v2/') +UNBABEL_API_URL = os.environ.get( + 'UNBABEL_API_URL', 'https://unbabel.com/tapi/v2/') class UnbabelApi(object): - - def __init__(self, username,api_key,sandbox=False): - if sandbox: - api_url= "http://sandbox.unbabel.com/tapi/v2/" - else: - api_url = "https://www.unbabel.co/tapi/v2/" + def __init__(self, username, api_key, sandbox=False): + ''' + The Unbabel Client object. + + :param username: Customer's username. + :type username: str + :param api_key: Customer's API key. + :type api_key: str + :param sandbox: Whether to use the sandbox API. + :type sandbox: bool + ''' + api_url = UNBABEL_SANDBOX_API_URL if sandbox else UNBABEL_API_URL + self.username = username self.api_key = api_key self.api_url = api_url self.is_bulk = False - - - def post_translations(self, - text, - target_language, - source_language=None, - ttype=None, - tone=None, - visibility=None, - public_url=None, - callback_url = None, - topics = None, - instructions=None, - uid=None - ): - - - #data = self.create_default_translation(text, target_language) - data = {} - for k, v in locals().iteritems(): - if v is self or v is data or v is None: - continue - data[k] = v + self.headers = { + 'Authorization': 'ApiKey {}:{}'.format(self.username, self.api_key), + 'content-type': 'application/json'} + + def api_call(self, uri, data=None, internal_api_call=False): + ''' + Wrapper function to fire GET/POST requests. + + :param uri: The request endpoint. + :type uri: str + :param data: The payload. + :param internal_api_call: Whether to re-route the endpoint to internal. + :type internal_api_call: bool + + ''' + # Routes the URL appropriately. + api_url = self.api_url + if internal_api_call: + api_url = api_url.replace('/tapi/v2/', '/api/v1/') + url = "{}{}".format(api_url, uri) + # Actual request firing. + if data is None: # GET requests. + return requests.get(url, headers=self.headers) + # POST requests. + return requests.post(url, headers=self.headers, data=json.dumps(data)) + + def post_translations(self, text, target_language, source_language=None, type=None, tone=None, visibility=None, + public_url=None, callback_url=None, topics=None, instructions=None, uid=None, + text_format="text", target_text=None, origin=None, client_owner_email=None, context=None): + data = {k: v for k, v in locals().items() if not v in (self, None)} if self.is_bulk: self.bulk_data.append(data) @@ -163,36 +72,81 @@ def post_translations(self, return self._make_request(data) + def post_mt_translations(self, text, target_language, source_language=None, tone=None, callback_url=None, + topics=None, instructions=None, uid=None, text_format="text", origin=None, + client_owner_email=None): + data = {k: v for k, v in locals().iteritems() if not v in (self, None)} + endpoint = "{}/mt_translation/".format(self.api_url) + result = requests.post(endpoint, headers=self.headers, data=json.dumps(data)) + + if result.status_code in (201, 202): + json_object = json.loads(result.content) + toret = self._build_mt_translation_object(json_object) + return toret + elif result.status_code == 401: + raise UnauthorizedException(result.content) + elif result.status_code == 400: + raise BadRequestException(result.content) + else: + raise Exception("Unknown Error return status %d: %s", + result.status_code, result.content[0:100]) + def _build_translation_object(self, json_object): - source_lang = json_object.get("source_language",None) - translation = json_object.get("translation",None) - status = json_object.get("status",None) - - translators = [Translator.from_json(t) for t in json_object.get("translators",[])] - + source_lang = json_object.get("source_language", None) + translation = json_object.get("translation", None) + status = json_object.get("status", None) + + translators = [Translator.from_json(t) for t in + json_object.get("translators", [])] + translation = Translation( - uid = json_object["uid"], - text = json_object["text"], - target_language = json_object.get('target_language', None), - source_language = json_object.get('source_lang', None), - translation = json_object.get('translation', None), - status = json_object.get('status', None), - translators = translators, - topics = json_object.get('topics', None), - price = json_object.get('price', None), - balance = json_object.get('balance', None) + uid=json_object["uid"], + text=json_object["text"], + target_language=json_object.get('target_language', None), + source_language=json_object.get('source_language', None), + translatedText=json_object.get('translatedText', None), + status=json_object.get('status', None), + translators=translators, + topics=json_object.get('topics', None), + price=json_object.get('price', None), + balance=json_object.get('balance', None), + text_format=json_object.get('text_format', "text"), + origin=json_object.get('origin', None), + price_plan=json_object.get('price_plan', None), + client=json_object.get('client', None), + ) + return translation + + def _build_mt_translation_object(self, json_object): + source_lang = json_object.get("source_language", None) + translation = json_object.get("translation", None) + status = json_object.get("status", None) + + translation = MTTranslation( + uid=json_object["uid"], + text=json_object["text"], + target_language=json_object.get('target_language', None), + source_language=json_object.get('source_language', None), + translatedText=json_object.get('translatedText', None), + status=json_object.get('status', None), + topics=json_object.get('topics', None), + text_format=json_object.get('text_format', "text"), + origin=json_object.get('origin', None), + client=json_object.get('client', None), ) return translation - def _make_request(self, data): - - headers={'Authorization': 'ApiKey %s:%s'%(self.username,self.api_key),'content-type': 'application/json'} + + # headers={'Authorization': 'ApiKey %s:%s'%(self.username, + # self.api_key),'content-type': 'application/json'} if self.is_bulk: f = requests.patch else: f = requests.post - result = f("%stranslation/"% self.api_url, headers=headers, data=json.dumps(data)) + print(self.headers) + result = f("%stranslation/" % self.api_url, headers=self.headers, + data=json.dumps(data)) if result.status_code in (201, 202): json_object = json.loads(result.content) toret = None @@ -208,14 +162,15 @@ def _make_request(self, data): elif result.status_code == 400: raise BadRequestException(result.content) else: - raise Exception("Unknown Error return status %d: %s", result.status_code, result.content[0:100]) + raise Exception("Unknown Error return status %d: %s", + result.status_code, result.content[0:100]) def start_bulk_transaction(self): self.bulk_data = [] self.is_bulk = True def _post_bulk(self): - data = {'objects' : self.bulk_data} + data = {'objects': self.bulk_data} return self._make_request(data=data) def post_bulk_translations(self, translations): @@ -229,69 +184,160 @@ def post_bulk_translations(self, translations): return self._post_bulk() - def get_translations(self): + def get_translations(self, status=None): ''' Returns the translations requested by the user ''' - headers={'Authorization': 'ApiKey %s:%s'%(self.username,self.api_key),'content-type': 'application/json'} - result = requests.get("%stranslation/"%self.api_url,headers=headers) - translations_json = json.loads(result.content)["objects"] - translations = [Translation(**tj) for tj in translations_json] + if status is not None: + result = self.api_call('translation/?status=%s' % status) + else: + result = self.api_call('translation/') + if result.status_code == 200: + translations_json = json.loads(result.content)["objects"] + translations = [Translation(**tj) for tj in translations_json] + else: + log.critical( + 'Error status when fetching translation from server: {' + '}!'.format( + result.status_code)) + translations = [] return translations - + def get_translation(self, uid): + ''' + Returns a translation with the given id + ''' + result = self.api_call('translation/{}/'.format(uid)) + if result.status_code == 200: + translation = Translation(**json.loads(result.content)) + else: + log.critical( + 'Error status when fetching translation from server: {' + '}!'.format( + result.status_code)) + raise ValueError(result.content) + return translation + + def upgrade_mt_translation(self, uid, properties=None): + """ + :param uid: + :param properties: This is suppose to be a dictionary with new + properties values to be replaced on the upgraded job + :return: + """ + api_url = self.api_url + uri = 'mt_translation/{}/'.format(uid) + url = "{}{}".format(api_url, uri) + data = {"status": "upgrade", "properties": properties} + return requests.patch(url, headers=self.headers, data=json.dumps(data)) + + def get_mt_translations(self, status=None): + ''' + Returns the translations requested by the user + ''' + if status is not None: + result = self.api_call('mt_translation/?status=%s' % status) + else: + result = self.api_call('mt_translation/') + if result.status_code == 200: + translations_json = json.loads(result.content)["objects"] + translations = [Translation(**tj) for tj in translations_json] + else: + log.critical( + 'Error status when fetching machine translation from server: ' + '{}!'.format( + result.status_code)) + translations = [] + return translations - def get_translation(self,uid): + def get_mt_translation(self, uid): ''' Returns a translation with the given id ''' - headers={'Authorization': 'ApiKey %s:%s'%(self.username,self.api_key),'content-type': 'application/json'} - result = requests.get("%stranslation/%s/"%(self.api_url,uid),headers=headers) - translation = Translation(**json.loads(result.content)) + result = self.api_call('mt_translation/{}/'.format(uid)) + if result.status_code == 200: + translation = Translation(**json.loads(result.content)) + else: + log.critical( + 'Error status when fetching machine translation from server: ' + '{}!'.format( + result.status_code)) + raise ValueError(result.content) return translation - - def get_language_pairs(self,train_langs=None): + def get_language_pairs(self, train_langs=None): ''' Returns the language pairs available on unbabel ''' - headers={'Authorization': 'ApiKey %s:%s'%(self.username,self.api_key),'content-type': 'application/json'} if train_langs is None: - result = requests.get("%slanguage_pair/"%self.api_url,headers=headers) + result = self.api_call('language_pair/') else: - result = requests.get("%slanguage_pair/?train_langs=%s"%(self.api_url,train_langs),headers=headers) + result = self.api_call( + 'language_pair/?train_langs={}'.format(train_langs)) try: - logger.debug(result.content) - langs_json = json.loads(result.content) - languages = [LangPair(Language(shortname=lang_json["lang_pair"]["source_language"]["shortname"], - name=lang_json["lang_pair"]["source_language"]["name"]), - Language(shortname=lang_json["lang_pair"]["target_language"]["shortname"], - name=lang_json["lang_pair"]["target_language"]["name"]) - ) for lang_json in langs_json["objects"]] - except: - logger.exception("Error decoding get language pairs") - languages = [] + langs_json = json.loads(result.content) + if 'error' in langs_json: + return [] + languages = [LangPair(Language( + shortname=lang_json["lang_pair"]["source_language"][ + "shortname"], + name=lang_json["lang_pair"]["source_language"]["name"]), + Language(shortname=lang_json["lang_pair"][ + "target_language"]["shortname"], + name=lang_json["lang_pair"][ + "target_language"]["name"]) + ) for lang_json in langs_json["objects"]] + except Exception as e: + log.exception("Error decoding get language pairs") + raise e return languages - + def get_tones(self): ''' Returns the tones available on unbabel ''' - headers={'Authorization': 'ApiKey %s:%s'%(self.username,self.api_key),'content-type': 'application/json'} - result = requests.get("%stone/"%self.api_url,headers=headers) - tones_json = json.loads(result.content) + result = self.api_call('tone/') + tones_json = json.loads(result.content) tones = [Tone(name=tone_json["tone"]["name"], - description=tone_json["tone"]["description"]) + description=tone_json["tone"]["description"]) for tone_json in tones_json["objects"]] return tones - + def get_topics(self): ''' Returns the topics available on unbabel ''' - headers={'Authorization': 'ApiKey %s:%s'%(self.username,self.api_key),'content-type': 'application/json'} - result = requests.get("%stopic/"%self.api_url,headers=headers) - topics_json = json.loads(result.content) - topics = [Topic(name=tone_json["topic"]["name"]) - for tone_json in topics_json["objects"]] + result = self.api_call('topic/') + topics_json = json.loads(result.content) + topics = [Topic(name=topic_json["topic"]["name"]) + for topic_json in topics_json["objects"]] return topics + + def get_account(self): + result = self.api_call('account/') + account_json = json.loads(result.content) + account_data = account_json['objects'][0]['account'] + account = Account(**account_data) + return account + + def get_word_count(self, text): + result = self.api_call('wordcount/', {"text": text}) + + if result.status_code == 201: + json_object = json.loads(result.content) + return json_object["word_count"] + else: + log.debug('Got a HTTP Error [{}]'.format(result.status_code)) + raise Exception("Unknown Error") + + def get_user(self): + result = self.api_call('app/user/', internal_api_call=True) + + if result.status_code == 200: + return json.loads(result.content) + else: + log.debug('Got a HTTP Error [{}]'.format(result.status_code)) + raise Exception("Unknown Error: %s" % result.status_code) + + +__all__ = ['UnbabelApi'] diff --git a/unbabel/exceptions.py b/unbabel/exceptions.py new file mode 100644 index 0000000..f7d216b --- /dev/null +++ b/unbabel/exceptions.py @@ -0,0 +1,17 @@ + + + +class UnauthorizedException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class BadRequestException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) diff --git a/unbabel/translations.py b/unbabel/translations.py new file mode 100644 index 0000000..c622462 --- /dev/null +++ b/unbabel/translations.py @@ -0,0 +1,76 @@ +"""Pythonic objects to hold the translation responses from JSON.""" + +class Translation(object): + def __init__( + self, + uid=-1, + text="", + translatedText=None, + target_language="", + source_language=None, + status=None, + translators=[], + topics=None, + price=None, + text_format='text', + origin=None, + price_plan=None, + balance=None, + client=None, + order_number=None): + self.uid = uid + self.text = text + self.translation = translatedText + self.source_language = source_language + self.target_language = target_language + self.status = status + self.translators = translators + self.topics = topics + self.price = price + self.text_format = text_format + self.origin = origin + self.price_plan = price_plan + self.client = client + self.balance = balance + self.order_number = order_number + + def __repr__(self): + return "{} {} {}_{}".format( + self.uid, self.status, self.source_language, self.target_language) + + def __str__(self): + return "{} {} {}_{}".format( + self.uid, self.status, self.source_language, self.target_language) + + +class MTTranslation(object): + def __init__( + self, + uid=-1, + text="", + translatedText=None, + target_language="", + source_language=None, + status=None, + topics=None, + text_format='text', + origin=None, + client=None): + self.uid = uid + self.text = text + self.translation = translatedText + self.source_language = source_language + self.target_language = target_language + self.status = status + self.topics = topics + self.text_format = text_format + self.origin = origin + self.client = client + + def __repr__(self): + return "{} {} {}_{}".format( + self.uid, self.status, self.source_language, self.target_language) + + def __str__(self): + return "{} {} {}_{}".format( + self.uid, self.status, self.source_language, self.target_language) diff --git a/unbabel/xliff_converter.py b/unbabel/xliff_converter.py new file mode 100644 index 0000000..34f6fb3 --- /dev/null +++ b/unbabel/xliff_converter.py @@ -0,0 +1,72 @@ +__author__ = 'joaograca' + +from bs4 import BeautifulSoup + +def generate_xliff(entry_dict): + """ + Given a dictionary with keys = ids + and values equals to strings generates + and xliff file to send to unbabel. + + Example: + {"123": "This is blue car", + "234": "This house is yellow" + } + + returns + + + + + + T2 apartment, as new building with swimming pool, sauna and gym. Inserted in Quinta da Beloura 1, which offers a variety of services such as private security 24 hours, tennis, golf, hotel, restaurants, and more. The apartment has air conditioning in all rooms, central heating, balcony and security screen for children in all windows. + + + + + + + """ + entries = "" + for key,value in entry_dict.iteritems(): + entries+=create_trans_unit(key,value).strip()+"\n" + xliff_str = get_head_xliff().strip()+"\n"+entries+get_tail_xliff().strip() + return xliff_str + +def get_head_xliff(): + return ''' + + + + + ''' +def get_tail_xliff(): + return ''' + + + + ''' + +def create_trans_unit(key, value): + return ''' + + + %s + + + '''%(key,value) + +def get_dictionary_from_xliff(xliff_text,side="target"): + soup = BeautifulSoup(xliff_text, "html.parser") + trans_units = soup.find_all("trans-unit") + result_dic = {} + for trans_unit in trans_units: + _id = trans_unit["id"] + if side == "target": + if trans_unit.target is None: + result_dic[_id] = trans_unit.source.text.strip() + else: + result_dic[_id] = trans_unit.target.text.strip() + else: + result_dic[_id] = trans_unit.source.text.strip() + return result_dic