diff --git a/convertapi/__init__.py b/convertapi/__init__.py index a98c554..830a05c 100644 --- a/convertapi/__init__.py +++ b/convertapi/__init__.py @@ -3,7 +3,7 @@ from .exceptions import * from .client import Client from .upload_io import UploadIO -from .api import convert, user +from .api import convert, user, async_convert, async_poll # configuration diff --git a/convertapi/api.py b/convertapi/api.py index b4f3c81..c04eb8a 100644 --- a/convertapi/api.py +++ b/convertapi/api.py @@ -1,10 +1,25 @@ import convertapi from .task import Task +from .result import Result +from .exceptions import AsyncConversionInProgress def convert(to_format, params, from_format = None, timeout = None): task = Task(from_format, to_format, params, timeout = timeout) return task.run() + def user(timeout = None): return convertapi.client.get('user', timeout = timeout) + + +def async_convert(to_format, params, from_format = None, timeout = None): + task = Task(from_format, to_format, params, timeout = timeout, is_async = True) + return task.run() + + +def async_poll(job_id): + response = convertapi.client.get('async/job/%s' % job_id) + if not response: + raise AsyncConversionInProgress + return Result(response) diff --git a/convertapi/client.py b/convertapi/client.py index aac975e..382d40e 100644 --- a/convertapi/client.py +++ b/convertapi/client.py @@ -50,6 +50,9 @@ def handle_response(self, r): except ValueError: raise e + if r.content == b'': + return None + return r.json() def url(self, path): diff --git a/convertapi/exceptions.py b/convertapi/exceptions.py index 82ec32a..e560862 100644 --- a/convertapi/exceptions.py +++ b/convertapi/exceptions.py @@ -13,3 +13,7 @@ def __init__(self, result): def __str__(self): message = "%s Code: %s. %s" % (self.message, self.code, self.invalid_parameters) return message.strip() + + +class AsyncConversionInProgress(BaseError): + pass diff --git a/convertapi/task.py b/convertapi/task.py index e580456..8e98382 100644 --- a/convertapi/task.py +++ b/convertapi/task.py @@ -6,11 +6,12 @@ DEFAULT_URL_FORMAT = 'web' class Task: - def __init__(self, from_format, to_format, params, timeout = None): + def __init__(self, from_format, to_format, params, timeout = None, is_async = False): self.from_format = from_format self.to_format = to_format self.params = params self.timeout = timeout or convertapi.conversion_timeout + self.is_async = is_async self.default_params = { 'Timeout': self.timeout, @@ -21,7 +22,8 @@ def run(self): params = self.__normalize_params() from_format = self.from_format or self.__detect_format() timeout = self.timeout + convertapi.conversion_timeout_delta if self.timeout else None - path = "convert/%s/to/%s" % (from_format, self.to_format) + base_path = 'convert' if not self.is_async else 'async/convert' + path = "%s/%s/to/%s" % (base_path, from_format, self.to_format) if 'converter' in params: path += "/converter/%s" % (params['converter']) diff --git a/tests/test_convertapi.py b/tests/test_convertapi.py index 4453a16..2b9c4e9 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -2,6 +2,7 @@ import os import io import tempfile +import time import requests from . import utils @@ -74,3 +75,49 @@ def test_api_error(self): def test_user_info(self): user_info = convertapi.user() assert user_info['Active'] + + +class TestAsyncConvertapi(utils.TestCase): + retry_sleep_base_timeout = 0.1 + + def setUp(self): + convertapi.api_secret = os.environ['CONVERT_API_SECRET'] + convertapi.max_parallel_uploads = 10 + + def test_async_conversion_and_polling(self): + convert_result = convertapi.async_convert('pdf', { 'File': 'examples/files/test.docx' }) + assert convert_result.response['JobId'] + + poll_result = self.get_poll_result(convert_result.response['JobId']) + assert poll_result.save_files(tempfile.gettempdir()) + assert poll_result.conversion_cost > 0 + + def test_polling_of_invalid_job_id(self): + fake_job_id = 'rwvd6vtq58eurfwv5zw5h2ci2pgno1pn' + try: + convertapi.async_poll(fake_job_id) + except requests.exceptions.HTTPError: + pass + else: + raise AssertionError + + def test_polling_too_fast_and_getting_202_accepted(self): + convert_result = convertapi.async_convert('pdf', { 'File': 'examples/files/test.docx' }) + try: + convertapi.async_poll(convert_result.response['JobId']) + except convertapi.AsyncConversionInProgress: + pass + else: + raise AssertionError + + def get_poll_result(self, job_id, retry_count=5): + try: + result = convertapi.async_poll(job_id) + except convertapi.AsyncConversionInProgress as error: + if retry_count > 0: + time.sleep((1 + 0.1) ** (5 - retry_count) - 1) + return self.get_poll_result(job_id, retry_count=retry_count - 1) + else: + raise error + else: + return result