diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22043ce..e136225 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,12 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] workflow_dispatch: -concurrency: +concurrency: group: mailosaur-python cancel-in-progress: true @@ -21,7 +21,7 @@ jobs: strategy: max-parallel: 1 matrix: - python-version: ['3.9'] + python-version: ["3.9"] env: MAILOSAUR_BASE_URL: https://mailosaur.com/ @@ -30,29 +30,28 @@ jobs: MAILOSAUR_API_KEY: ${{ secrets.MAILOSAUR_API_KEY }} MAILOSAUR_SERVER: ${{ secrets.MAILOSAUR_SERVER }} MAILOSAUR_VERIFIED_DOMAIN: ${{ secrets.MAILOSAUR_VERIFIED_DOMAIN }} - MAILOSAUR_PREVIEWS_SERVER: ${{ secrets.MAILOSAUR_PREVIEWS_SERVER }} steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Test with pytest - run: | - pytest - - name: Notify on Failure - uses: skitionek/notify-microsoft-teams@master - if: ${{ failure() }} - with: - webhook_url: ${{ secrets.TEAMS_BUILDS_WEBHOOK }} - needs: ${{ toJson(needs) }} - job: ${{ toJson(job) }} - overwrite: "{ title: `${workflow} failed for ${repository.name}` }" + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Test with pytest + run: | + pytest + - name: Notify on Failure + uses: skitionek/notify-microsoft-teams@master + if: ${{ failure() }} + with: + webhook_url: ${{ secrets.TEAMS_BUILDS_WEBHOOK }} + needs: ${{ toJson(needs) }} + job: ${{ toJson(job) }} + overwrite: "{ title: `${workflow} failed for ${repository.name}` }" build-next: if: ${{ always() }} @@ -62,7 +61,7 @@ jobs: strategy: max-parallel: 1 matrix: - python-version: ['3.9'] + python-version: ["3.9"] env: MAILOSAUR_BASE_URL: https://next.mailosaur.com/ @@ -71,26 +70,25 @@ jobs: MAILOSAUR_API_KEY: ${{ secrets.MAILOSAUR_API_KEY }} MAILOSAUR_SERVER: ${{ secrets.MAILOSAUR_SERVER }} MAILOSAUR_VERIFIED_DOMAIN: ${{ secrets.MAILOSAUR_VERIFIED_DOMAIN }} - MAILOSAUR_PREVIEWS_SERVER: ${{ secrets.MAILOSAUR_PREVIEWS_SERVER }} steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Test with pytest - run: | - pytest - - name: Notify on Failure - uses: skitionek/notify-microsoft-teams@master - if: ${{ failure() }} - with: - webhook_url: ${{ secrets.TEAMS_BUILDS_WEBHOOK }} - needs: ${{ toJson(needs) }} - job: ${{ toJson(job) }} - overwrite: "{ title: `${workflow} failed for ${repository.name}` }" + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Test with pytest + run: | + pytest + - name: Notify on Failure + uses: skitionek/notify-microsoft-teams@master + if: ${{ failure() }} + with: + webhook_url: ${{ secrets.TEAMS_BUILDS_WEBHOOK }} + needs: ${{ toJson(needs) }} + job: ${{ toJson(job) }} + overwrite: "{ title: `${workflow} failed for ${repository.name}` }" diff --git a/mailosaur/mailosaur_client.py b/mailosaur/mailosaur_client.py index dc48b78..6a77871 100644 --- a/mailosaur/mailosaur_client.py +++ b/mailosaur/mailosaur_client.py @@ -28,7 +28,7 @@ def __init__(self, api_key, base_url="https://mailosaur.com/"): """ Pass in your mailbox id and api key to authenticate """ session = requests.Session() session.auth = (api_key, '') - session.headers.update({'User-Agent': 'mailosaur-python/7.0.0'}) + session.headers.update({'User-Agent': 'mailosaur-python/8.0.0'}) if base_url is None: base_url = "https://mailosaur.com/" @@ -67,6 +67,9 @@ def handle_http_error(self, response): elif response.status_code == 404: raise MailosaurException("Not found, check input parameters.", "invalid_request", response.status_code, response.text) + elif response.status_code == 410: + raise MailosaurException("Permanently expired or deleted.", + "gone", response.status_code, response.text) else: raise MailosaurException("An API error occurred, see httpResponse for further information.", "api_error", response.status_code, response.text) diff --git a/mailosaur/models/__init__.py b/mailosaur/models/__init__.py index f6466f9..0e443e1 100644 --- a/mailosaur/models/__init__.py +++ b/mailosaur/models/__init__.py @@ -30,8 +30,8 @@ from .device_create_options import DeviceCreateOptions from .otp_result import OtpResult from .preview_list_result import PreviewListResult -from .preview_email_client_list_result import PreviewEmailClientListResult -from .preview_request import PreviewRequest +from .email_client_list_result import EmailClientListResult +from .email_client import EmailClient from .preview_request_options import PreviewRequestOptions __all__ = [ @@ -68,7 +68,7 @@ 'DeviceCreateOptions', 'OtpResult', 'PreviewListResult', - 'PreviewEmailClientListResult', - 'PreviewRequest', + 'EmailClientListResult', + 'EmailClient', 'PreviewRequestOptions' ] diff --git a/mailosaur/models/email_client.py b/mailosaur/models/email_client.py new file mode 100644 index 0000000..e2de97f --- /dev/null +++ b/mailosaur/models/email_client.py @@ -0,0 +1,22 @@ +class EmailClient(object): + """Describes an email client with which email previews can be generated. + + :param label: The unique email client label. Used when generating email preview requests. + :type label: str + + :param name: The display name of the email client. + :type name: str + """ + + def __init__(self, data=None): + if data is None: + data = {} + + self.label = data.get('label', None) + self.name = data.get('name', None) + + def to_json(self): + return { + 'label': self.label, + 'name': self.name + } diff --git a/mailosaur/models/email_client_list_result.py b/mailosaur/models/email_client_list_result.py new file mode 100644 index 0000000..d0388dc --- /dev/null +++ b/mailosaur/models/email_client_list_result.py @@ -0,0 +1,15 @@ +from .email_client import EmailClient + + +class EmailClientListResult(object): + """A list of available email clients with which to generate email previews. + + :param items: A list of available email clients. + :type items: list[~mailosaur.models.EmailClient] + """ + + def __init__(self, data=None): + if data is None: + data = {} + + self.items = [EmailClient(i) for i in data.get('items', None)] diff --git a/mailosaur/models/preview_email_client.py b/mailosaur/models/preview_email_client.py deleted file mode 100644 index 9e543fd..0000000 --- a/mailosaur/models/preview_email_client.py +++ /dev/null @@ -1,47 +0,0 @@ -class PreviewEmailClient(object): - """Describes an email client with which email previews can be generated. - - :param id: The unique identifier of the email client. - :type id: str - - :param name: The display name of the email client. - :type name: str - - :param platform_group: Whether the platform is desktop, mobile, or web-based. - :type platform_group: str - - :param platform_type: The type of platform on which the email client is running. - :type platform_type: str - - :param platform_version: The platform version number. - :type platform_version: str - - :param can_disable_images: If true, images can be disabled when generating previews. - :type can_disable_images: bool - - :param status: The current status of the email client. - :type status: str - """ - - def __init__(self, data=None): - if data is None: - data = {} - - self.id = data.get('id', None) - self.name = data.get('name', None) - self.platform_group = data.get('platformGroup', None) - self.platform_type = data.get('platformType', None) - self.platform_version = data.get('platformVersion', None) - self.can_disable_images = data.get('canDisableImages', None) - self.status = data.get('status', None) - - def to_json(self): - return { - 'id': self.id, - 'name': self.name, - 'platformGroup': self.platform_group, - 'platformType': self.platform_type, - 'platformVersion': self.platform_version, - 'canDisableImages': self.can_disable_images, - 'status': self.status - } diff --git a/mailosaur/models/preview_email_client_list_result.py b/mailosaur/models/preview_email_client_list_result.py deleted file mode 100644 index 61fbe3b..0000000 --- a/mailosaur/models/preview_email_client_list_result.py +++ /dev/null @@ -1,15 +0,0 @@ -from .preview_email_client import PreviewEmailClient - - -class PreviewEmailClientListResult(object): - """A list of available email clients with which to generate email previews. - - :param items: A list of available email clients. - :type items: list[~mailosaur.models.PreviewEmailClient] - """ - - def __init__(self, data=None): - if data is None: - data = {} - - self.items = [PreviewEmailClient(i) for i in data.get('items', None)] diff --git a/mailosaur/models/preview_request.py b/mailosaur/models/preview_request.py deleted file mode 100644 index b2e2974..0000000 --- a/mailosaur/models/preview_request.py +++ /dev/null @@ -1,19 +0,0 @@ -class PreviewRequest(object): - """Describes an email preview request. - - :param email_client: The email client you wish to generate a preview for. - :type email_client: str - - :param disable_images: If true, images will be disabled (only if supported by the client). - :type disable_images: bool - """ - - def __init__(self, email_client, disable_images=False): - self.email_client = email_client - self.disable_images = disable_images - - def to_json(self): - return { - 'emailClient': self.email_client, - 'disableImages': self.disable_images - } diff --git a/mailosaur/models/preview_request_options.py b/mailosaur/models/preview_request_options.py index c460e02..149c7ea 100644 --- a/mailosaur/models/preview_request_options.py +++ b/mailosaur/models/preview_request_options.py @@ -4,20 +4,14 @@ class PreviewRequestOptions(object): """PreviewRequestOptions. - :param previews: The list of email preview requests. - :type previews: list[~mailosaur.models.PreviewRequest] + :param email_clients: The list email clients to generate previews with. + :type email_clients: list[str] """ - def __init__(self, previews=None): - self.previews = previews + def __init__(self, email_clients=None): + self.email_clients = email_clients def to_json(self): - previews = [] - - if self.previews is not None: - for a in self.previews: - previews.append(a.to_json()) - return { - 'previews': previews + 'emailClients': self.email_clients } diff --git a/mailosaur/operations/files_operations.py b/mailosaur/operations/files_operations.py index 44a8f22..29024dc 100644 --- a/mailosaur/operations/files_operations.py +++ b/mailosaur/operations/files_operations.py @@ -1,4 +1,6 @@ from ..models import MailosaurException +import time +from datetime import datetime class FilesOperations(object): @@ -67,11 +69,34 @@ def get_preview(self, id): :raises: :class:`HttpOperationError` """ - url = "%sapi/files/previews/%s" % (self.base_url, id) - response = self.session.get(url, stream=True) + timeout = 120000 + poll_count = 0 + start_time = datetime.today() - if response.status_code not in [200]: - self.handle_http_error(response) - return + while True: + url = "%sapi/files/screenshots/%s" % (self.base_url, id) + response = self.session.get(url, stream=True) - return response + if response.status_code == 200: + return response + + if response.status_code not in [202]: + self.handle_http_error(response) + return + + # List conversion necessary for Python 3 compatibility + # https://stackoverflow.com/questions/36982858/object-of-type-map-has-no-len-in-python-3 + delay_pattern = list( + map(int, (response.headers.get('x-ms-delay') or '1000').split(','))) + + delay = delay_pattern[len( + delay_pattern) - 1] if poll_count >= len(delay_pattern) else delay_pattern[poll_count] + + poll_count += 1 + + # Stop if timeout will be exceeded + if ((1000 * (datetime.today() - start_time).total_seconds()) + delay) > timeout: + raise MailosaurException( + "An email preview was not generated in time. The email client may not be available, or the preview ID [%s] may be incorrect." % id, "preview_timeout") + + time.sleep(delay / 1000) diff --git a/mailosaur/operations/messages_operations.py b/mailosaur/operations/messages_operations.py index 2749c61..7513cde 100644 --- a/mailosaur/operations/messages_operations.py +++ b/mailosaur/operations/messages_operations.py @@ -326,7 +326,7 @@ def generate_previews(self, id, options): :raises: :class:`MailosaurException` """ - url = "%sapi/messages/%s/previews" % (self.base_url, id) + url = "%sapi/messages/%s/screenshots" % (self.base_url, id) response = self.session.post(url, json=options.to_json()) if response.status_code not in [200]: diff --git a/mailosaur/operations/previews_operations.py b/mailosaur/operations/previews_operations.py index 0903419..20e7006 100644 --- a/mailosaur/operations/previews_operations.py +++ b/mailosaur/operations/previews_operations.py @@ -1,4 +1,4 @@ -from ..models import PreviewEmailClientListResult +from ..models import EmailClientListResult class PreviewsOperations(object): @@ -15,12 +15,12 @@ def list_email_clients(self): Returns a list of available email clients. - :return: PreviewEmailClientListResult - :rtype: ~mailosaur.models.PreviewEmailClientListResult + :return: EmailClientListResult + :rtype: ~mailosaur.models.EmailClientListResult :raises: :class:`MailosaurException` """ - url = "%sapi/previews/clients" % (self.base_url) + url = "%sapi/screenshots/clients" % (self.base_url) response = self.session.get(url) if response.status_code not in [200]: @@ -29,4 +29,4 @@ def list_email_clients(self): data = response.json() - return PreviewEmailClientListResult(data) + return EmailClientListResult(data) diff --git a/mailosaur/version.py b/mailosaur/version.py index 3e74aba..35ba86e 100644 --- a/mailosaur/version.py +++ b/mailosaur/version.py @@ -1,2 +1 @@ -VERSION = "7.0.0" - +VERSION = "8.0.0" diff --git a/setup.py b/setup.py index 8d15fc7..848279b 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='mailosaur', - version='7.0.0', + version='8.0.0', description='The Mailosaur Python library lets you integrate email and SMS testing into your continuous integration process.', long_description=long_description, long_description_content_type='text/markdown', diff --git a/tests/previews_test.py b/tests/previews_test.py index 7a1937f..4d9fb58 100644 --- a/tests/previews_test.py +++ b/tests/previews_test.py @@ -5,7 +5,7 @@ import random from .mailer import Mailer from mailosaur import MailosaurClient -from mailosaur.models import SearchCriteria, PreviewRequest, PreviewRequestOptions, MailosaurException +from mailosaur.models import SearchCriteria, PreviewRequestOptions, MailosaurException class PreviewsTest(TestCase): @@ -13,9 +13,9 @@ class PreviewsTest(TestCase): def setUpClass(cls): api_key = os.getenv('MAILOSAUR_API_KEY') base_url = os.getenv('MAILOSAUR_BASE_URL') - cls.server = os.getenv('MAILOSAUR_PREVIEWS_SERVER') + cls.server = os.getenv('MAILOSAUR_SERVER') - if api_key is None: + if api_key is None or cls.server is None: raise Exception( "Missing necessary environment variables - refer to README.md") @@ -27,9 +27,6 @@ def test_list_clients(self): self.assertTrue(len(result.items) > 1) def test_generate_previews(self): - if self.server is None: - pytest.skip("Requires server with previews enabled") - random_string = ''.join(random.choice( string.ascii_uppercase + string.ascii_lowercase) for _ in range(10)) host = os.getenv('MAILOSAUR_SMTP_HOST', 'mailosaur.net') @@ -42,8 +39,7 @@ def test_generate_previews(self): criteria.sent_to = test_email_address email = self.client.messages.get(self.server, criteria) - request = PreviewRequest("OL2021") - options = PreviewRequestOptions([request]) + options = PreviewRequestOptions(["iphone-16plus-applemail-lightmode-portrait"]) result = self.client.messages.generate_previews( email.id, options)