From 78416a0abb87c8002dbf71f4e2fe80fb291845e2 Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 19:52:53 -0300 Subject: [PATCH 01/10] Add support for async mode operation --- convertapi/__init__.py | 2 +- convertapi/api.py | 12 ++++++++++++ convertapi/task.py | 6 ++++-- tests/test_convertapi.py | 27 +++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) 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..58d607e 100644 --- a/convertapi/api.py +++ b/convertapi/api.py @@ -1,10 +1,22 @@ import convertapi from .task import Task +from .result import Result 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) + return Result(response) 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..d23307c 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,29 @@ def test_api_error(self): def test_user_info(self): user_info = convertapi.user() assert user_info['Active'] + +class TestAsyncConvertapi(utils.TestCase): + def setUp(self): + convertapi.api_secret = os.environ['CONVERT_API_SECRET'] + convertapi.max_parallel_uploads = 10 + + def test_async_convert_file(self): + convert_result = convertapi.async_convert('pdf', { 'File': 'examples/files/test.docx' }) + assert convert_result.response['JobId'] + + poll_result = get_poll_result(convert_result.response['JobId']) + assert poll_result.save_files(tempfile.gettempdir()) + assert poll_result.conversion_cost > 0 + + +def get_poll_result(job_id, retry_count=5): + try: + result = convertapi.async_poll(job_id) + except Exception as error: + if retry_count > 0: + time.sleep(0.1) + return get_poll_result(job_id, retry_count=retry_count - 1) + else: + raise error + else: + return result From 6f1b901e887808a9ae57aa067f00d97f9a11152d Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 20:10:42 -0300 Subject: [PATCH 02/10] Test also polling of invalid url --- tests/test_convertapi.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_convertapi.py b/tests/test_convertapi.py index d23307c..07aca48 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -1,9 +1,11 @@ +from unittest import mock import convertapi import os import io import tempfile import time import requests +from requests.models import Response from . import utils from nose.tools import * @@ -76,12 +78,13 @@ def test_user_info(self): user_info = convertapi.user() assert user_info['Active'] + class TestAsyncConvertapi(utils.TestCase): def setUp(self): convertapi.api_secret = os.environ['CONVERT_API_SECRET'] convertapi.max_parallel_uploads = 10 - def test_async_convert_file(self): + def test_async_conversion_and_polling(self): convert_result = convertapi.async_convert('pdf', { 'File': 'examples/files/test.docx' }) assert convert_result.response['JobId'] @@ -89,6 +92,15 @@ def test_async_convert_file(self): 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 get_poll_result(job_id, retry_count=5): try: From 2f9fd3cd73f5cab9ea3f94b51a9a42f08e1e4b51 Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 20:24:29 -0300 Subject: [PATCH 03/10] Avoid a JSONDecodeError when polling too earlier --- convertapi/client.py | 6 +++++- tests/test_convertapi.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/convertapi/client.py b/convertapi/client.py index aac975e..5a6d418 100644 --- a/convertapi/client.py +++ b/convertapi/client.py @@ -1,5 +1,6 @@ import requests import convertapi +import simplejson from io import BytesIO from .exceptions import * @@ -50,7 +51,10 @@ def handle_response(self, r): except ValueError: raise e - return r.json() + try: + return r.json() + except simplejson.errors.JSONDecodeError as e: + raise ApiError({'message': 'Conversion in progress'}) def url(self, path): return "%s%s?Secret=%s" % (convertapi.base_uri, path, convertapi.api_secret) diff --git a/tests/test_convertapi.py b/tests/test_convertapi.py index 07aca48..8590e7d 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -101,6 +101,15 @@ def test_polling_of_invalid_job_id(self): 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.ApiError: + pass + else: + raise AssertionError + def get_poll_result(job_id, retry_count=5): try: From 33bfe354a1f61f052911f9de163ed9b10334bf80 Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 20:29:51 -0300 Subject: [PATCH 04/10] Use exponential growing retry timeout --- tests/test_convertapi.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_convertapi.py b/tests/test_convertapi.py index 8590e7d..1fd1075 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -111,12 +111,14 @@ def test_polling_too_fast_and_getting_202_accepted(self): raise AssertionError +retry_sleep_base_timeout = 0.1 + def get_poll_result(job_id, retry_count=5): try: result = convertapi.async_poll(job_id) - except Exception as error: + except convertapi.ApiError: if retry_count > 0: - time.sleep(0.1) + time.sleep((1 + 0.1) ** (5 - retry_count) - 1) return get_poll_result(job_id, retry_count=retry_count - 1) else: raise error From 9ae5d5aabff8816b3439f38b23587126a292bfa1 Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 20:37:28 -0300 Subject: [PATCH 05/10] Use an explicit exception to tell the conversion is in progress --- convertapi/client.py | 2 +- convertapi/exceptions.py | 4 ++++ tests/test_convertapi.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/convertapi/client.py b/convertapi/client.py index 5a6d418..2556fe3 100644 --- a/convertapi/client.py +++ b/convertapi/client.py @@ -54,7 +54,7 @@ def handle_response(self, r): try: return r.json() except simplejson.errors.JSONDecodeError as e: - raise ApiError({'message': 'Conversion in progress'}) + raise AsyncConversionInProgress def url(self, path): return "%s%s?Secret=%s" % (convertapi.base_uri, path, convertapi.api_secret) 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/tests/test_convertapi.py b/tests/test_convertapi.py index 1fd1075..0ea0e67 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -105,7 +105,7 @@ 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.ApiError: + except convertapi.AsyncConversionInProgress: pass else: raise AssertionError @@ -116,7 +116,7 @@ def test_polling_too_fast_and_getting_202_accepted(self): def get_poll_result(job_id, retry_count=5): try: result = convertapi.async_poll(job_id) - except convertapi.ApiError: + except convertapi.AsyncConversionInProgress: if retry_count > 0: time.sleep((1 + 0.1) ** (5 - retry_count) - 1) return get_poll_result(job_id, retry_count=retry_count - 1) From 2d4108c49c0e45334df0fcb52cc8cd32e29945a0 Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 20:39:59 -0300 Subject: [PATCH 06/10] Explicit test a blank response instead of any non-json response --- convertapi/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/convertapi/client.py b/convertapi/client.py index 2556fe3..cbd5586 100644 --- a/convertapi/client.py +++ b/convertapi/client.py @@ -51,11 +51,11 @@ def handle_response(self, r): except ValueError: raise e - try: - return r.json() - except simplejson.errors.JSONDecodeError as e: + if r.content == b'': raise AsyncConversionInProgress + return r.json() + def url(self, path): return "%s%s?Secret=%s" % (convertapi.base_uri, path, convertapi.api_secret) From 118e8b6f19e15e2e47cab671d38134629889d628 Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 20:43:01 -0300 Subject: [PATCH 07/10] Remove unnecessary import --- tests/test_convertapi.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_convertapi.py b/tests/test_convertapi.py index 0ea0e67..df08e8f 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -1,11 +1,9 @@ -from unittest import mock import convertapi import os import io import tempfile import time import requests -from requests.models import Response from . import utils from nose.tools import * From 9930a167ef445f77c96ed03bc4044cdd328346cc Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 20:45:06 -0300 Subject: [PATCH 08/10] Pack the helper function inside the test class --- tests/test_convertapi.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/test_convertapi.py b/tests/test_convertapi.py index df08e8f..b7f552c 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -78,6 +78,8 @@ def test_user_info(self): 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 @@ -86,7 +88,7 @@ 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 = get_poll_result(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 @@ -108,17 +110,14 @@ def test_polling_too_fast_and_getting_202_accepted(self): else: raise AssertionError - -retry_sleep_base_timeout = 0.1 - -def get_poll_result(job_id, retry_count=5): - try: - result = convertapi.async_poll(job_id) - except convertapi.AsyncConversionInProgress: - if retry_count > 0: - time.sleep((1 + 0.1) ** (5 - retry_count) - 1) - return get_poll_result(job_id, retry_count=retry_count - 1) + def get_poll_result(self, job_id, retry_count=5): + try: + result = convertapi.async_poll(job_id) + except convertapi.AsyncConversionInProgress: + 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: - raise error - else: - return result + return result From 27b517fe06c3d3f206932744ae1bdce2c58038ac Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 21:07:36 -0300 Subject: [PATCH 09/10] Do not raise AsyncConversionInProgress for regular requests --- convertapi/api.py | 3 +++ convertapi/client.py | 2 +- tests/test_convertapi.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/convertapi/api.py b/convertapi/api.py index 58d607e..c04eb8a 100644 --- a/convertapi/api.py +++ b/convertapi/api.py @@ -2,6 +2,7 @@ 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) @@ -19,4 +20,6 @@ def async_convert(to_format, params, from_format = None, timeout = None): 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 cbd5586..ddadf8c 100644 --- a/convertapi/client.py +++ b/convertapi/client.py @@ -52,7 +52,7 @@ def handle_response(self, r): raise e if r.content == b'': - raise AsyncConversionInProgress + return None return r.json() diff --git a/tests/test_convertapi.py b/tests/test_convertapi.py index b7f552c..2b9c4e9 100644 --- a/tests/test_convertapi.py +++ b/tests/test_convertapi.py @@ -113,7 +113,7 @@ def test_polling_too_fast_and_getting_202_accepted(self): def get_poll_result(self, job_id, retry_count=5): try: result = convertapi.async_poll(job_id) - except convertapi.AsyncConversionInProgress: + 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) From cccb50b73e32b229c8157358a1b635d8746eab46 Mon Sep 17 00:00:00 2001 From: Michel Sabchuk Date: Wed, 30 Nov 2022 21:28:05 -0300 Subject: [PATCH 10/10] Remove unnecessary import --- convertapi/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/convertapi/client.py b/convertapi/client.py index ddadf8c..382d40e 100644 --- a/convertapi/client.py +++ b/convertapi/client.py @@ -1,6 +1,5 @@ import requests import convertapi -import simplejson from io import BytesIO from .exceptions import *