pax_global_header00006660000000000000000000000064141425112660014514gustar00rootroot0000000000000052 comment=a0a5b337b3510aa7e8ee78dc06c1d44f9c7be7af python-sparkpost-1.3.10/000077500000000000000000000000001414251126600151435ustar00rootroot00000000000000python-sparkpost-1.3.10/.editorconfig000066400000000000000000000002331414251126600176160ustar00rootroot00000000000000root = true [*] end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.py] indent_style = space indent_size = 4 python-sparkpost-1.3.10/.gitignore000066400000000000000000000013531414251126600171350ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # virtualenv venv/ # autoenv .env # Mac OSX .DS_Store .idea python-sparkpost-1.3.10/.travis.yml000066400000000000000000000013651414251126600172610ustar00rootroot00000000000000language: python python: - "2.7" - "3.4" - "3.5" env: - DJANGO_VERSION='>=1.7,<1.8' - DJANGO_VERSION='>=1.8,<1.9' - DJANGO_VERSION='>=1.9,<1.10' matrix: exclude: - python: "3.5" env: DJANGO_VERSION='>=1.7,<1.8' install: - pip install -r dev-requirements.txt - pip uninstall django --yes - pip install -q django$DJANGO_VERSION - pip install coveralls==1.7.0 - pip install -e . before_script: - flake8 sparkpost test script: - py.test --cov sparkpost test/ --cov-report term-missing after_success: - coveralls notifications: slack: secure: VNIwgvrbcwj0b2gfYSOeTyK7rkV62/belwhYthasHmN+DoTsEJF4+HFVtzOykS72LM/f5Id5zieWtxYG+soy+yEOc1iZizXRpRJORtTYfZJB9RCffavosl322BcpoTX99cGyiZjWjOFH70UWlFMB3zT0jS+9icuTfk7ZqBX/zDA= python-sparkpost-1.3.10/AUTHORS.rst000066400000000000000000000022301414251126600170170ustar00rootroot00000000000000Maintainers ----------------- - Jose Zamora `@jgzamora `_ Contributors ----------------------- - Bob Evans `@bizob2828 `_ - Aydrian Howard `@aydrian `_ - Rich Leland `@richleland `_ - Artur Felipe Sousa `@arturfelipe `_ - Barthelemy Dagenais `@bartdag `_ - Dmitry Tyukin `@deems `_ - Jared Morse `@jarcoal `_ - Marko Mrdjenovic `@friedcell `_ - Mohammad Hossain `@rajumsys `_ - Simeon Visser `@svisser `_ - Zdeněk Softič `@btx `_ - `@amatissart `_ - `@gnarvaja `_ - `@pegler `_ - `@puttu `_ - Janusz Skonieczny `@wooyek `_ - Richard Dawe `@richdawe77 `_ - John Keyes `@jkeyes `_ - ADD YOURSELF HERE (and link to your github page) python-sparkpost-1.3.10/CHANGELOG.md000066400000000000000000000175531414251126600167670ustar00rootroot00000000000000# Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased - [Compare to latest release][unreleased] ## [1.3.10] - 2021-11-09 - [#219](https://github.com/SparkPost/python-sparkpost/pull/219) Support for perform_substitution ## [1.3.9] - 2021-04-29 - [#215](https://github.com/SparkPost/python-sparkpost/pull/215) Fix bug in _translate_keys that affects non-nested parameters ## [1.3.8] - 2021-04-09 - [#214](https://github.com/SparkPost/python-sparkpost/pull/214) Support for AMP HTML for transmissions ## [1.3.7] - 2021-03-24 - [#211](https://github.com/SparkPost/python-sparkpost/pull/211) Support for top open pixel for transmissions ## [1.3.6] - 2018-03-23 ### Added - [#160](https://github.com/SparkPost/python-sparkpost/pull/160) Extra header support for Django messages ## [1.3.5] - 2017-03-07 ### Added - [#141](https://github.com/SparkPost/python-sparkpost/pull/141) Validation for recipients parameter for transmissions - [#142](https://github.com/SparkPost/python-sparkpost/pull/142) URI parameter support for transmissions list endpoint. Also added deprecation warning ## [1.3.4] - 2017-02-16 ### Added - Examples for exception handling and using the base resource class ### Fixed - [#137](https://github.com/SparkPost/python-sparkpost/pull/137) Added missing `campaign` support for Django backend ## [1.3.3] - 2017-01-13 ### Fixed - [#135](https://github.com/SparkPost/python-sparkpost/pull/135) Issue where exceptions were not returning properly for some underlying API errors ## [1.3.2] - 2016-11-14 ### Fixed - [#129](https://github.com/SparkPost/python-sparkpost/pull/129) Reverted change for emojis in the body of a message, needs further investigation - [#129](https://github.com/SparkPost/python-sparkpost/pull/129) `substitution_data`, `metadata`, and `tags` are now supplied properly for cc/bcc recipients ## [1.3.1] - 2016-11-13 ### Added - Instructions for use with Google Cloud ### Fixed - [#114](https://github.com/SparkPost/python-sparkpost/pull/114) Issue where emojis in the body of a message were being forced to ASCII - [#118](https://github.com/SparkPost/python-sparkpost/pull/118) Fixed improper setting of header_to value when using cc and primary recipient has substitution data - [#119](https://github.com/SparkPost/python-sparkpost/pull/119) Added missing `sources` to suppression list key map ## [1.3.0] - 2016-10-01 ### Added - [#121](https://github.com/SparkPost/python-sparkpost/pull/121) Added extended error code to `SparkPostAPIException` class - [#124](https://github.com/SparkPost/python-sparkpost/pull/124) Added `delete` method to `Transmission` class - CI tests now also run against Python 3.5 ### Changed - [#123](https://github.com/SparkPost/python-sparkpost/pull/123) Updated RequestsTransport to use a requests session so HTTP Keep Alive is honored ### Fixed - [#115](https://github.com/SparkPost/python-sparkpost/pull/115) Guess attachment mimetype in Django email backend if not provided ## [1.2.0] - 2016-04-19 ### Added - [#109](https://github.com/SparkPost/python-sparkpost/pull/109) Support for specifying `template`, `substitution_data` when using the Django email backend - [#100](https://github.com/SparkPost/python-sparkpost/pull/100) Support for the `content_subtype` attribute when using the `EmailMessage` class in Django ## [1.1.1] - 2016-04-08 ### Fixed - [#99](https://github.com/SparkPost/python-sparkpost/pull/99) Issue where inline images were always passed to the API, which in turn required HTML content ## [1.1.0] - 2016-03-30 ### Added - [#94](https://github.com/SparkPost/python-sparkpost/pull/94) Better extensibility with support for Tornado - [#95](https://github.com/SparkPost/python-sparkpost/pull/95) Support for CSS inlining - [#98](https://github.com/SparkPost/python-sparkpost/pull/98) Support for inline images - [#98](https://github.com/SparkPost/python-sparkpost/pull/98) Support for specifying IP pool ### Fixed - [#97](https://github.com/SparkPost/python-sparkpost/pull/97) Issue where substitution data was being improperly passed to the templates preview endpoint - [#91](https://github.com/SparkPost/python-sparkpost/pull/91) Issue where Django backend was not properly base64 encoding attachments ## [1.0.5] - 2016-03-18 ### Fixed - [#89](https://github.com/SparkPost/python-sparkpost/pull/89) Issue where global Django settings object was being modified, causing mixed emails ## [1.0.4] - 2016-03-10 ### Added - `SPARKPOST_OPTIONS` setting for Django for passing through additional transmission options like `track_opens`, `track_clicks`, and `transactional` - Support for cc, bcc, reply to, and attachments for Django ### Changed - Refactored some of the bits in the Django email backend out into a `SparkPostMessage` class which prepares parameters for calls to the `Transmissions.send` method ## [1.0.3] - 2016-03-03 ### Added - Tox for local testing - Allow unicode recipients - Automatically parse emails with friendly from e.g. `Friendly Name ` - Support for cc/bcc ## [1.0.2] - 2016-02-25 ### Added - Support for attachments via the `attachments` parameter in `Transmissions` ## [1.0.1] - 2016-02-25 ### Fixed - Subpackages now get included properly - Updated examples to use plural `transmissions` ## [1.0.0] - 2015-11-06 ### Added - Django email backend - Support for scheduled sending via the `start_time` parameter in `Transmissions` - Support for marking messages as transactional or non-transactional via the `transactional` parameter in `Transmissions` - Support for skipping suppression (SparkPost Elite only) via the `skip_suppression` parameter in `Transmissions` ## [1.0.0.dev2] - 2015-09-01 ### Added - Code coverage via [coveralls] - CONTRIBUTING file for notes on how to contribute - `Templates` class to manage templates - `RecipientLists` class to manage recipients we want to send to - `SuppressionLists` class to manage recipients that are suppressed ### Changed - Renamed `Transmission` class to `Transmissions` (backwards compatible) ### Removed - Tox file for running tests in favor of `make test` and Travis CI ### Fixed - Engagement tracking no longer automatically enabled for all transmissions - Documentation generation issues ## 1.0.0.dev1 - 2014-02-09 ### Added - Base SparkPost class - `Transmission` class for sending messages - Examples for Transmission usage - Metrics class for getting a list of campaigns and domains - Docs on readthedocs.org [unreleased]: https://github.com/sparkpost/python-sparkpost/compare/v1.3.6...HEAD [1.3.6]: https://github.com/sparkpost/python-sparkpost/compare/v1.3.5...v1.3.6 [1.3.5]: https://github.com/sparkpost/python-sparkpost/compare/v1.3.4...v1.3.5 [1.3.4]: https://github.com/sparkpost/python-sparkpost/compare/v1.3.3...v1.3.4 [1.3.3]: https://github.com/sparkpost/python-sparkpost/compare/v1.3.2...v1.3.3 [1.3.2]: https://github.com/sparkpost/python-sparkpost/compare/v1.3.1...v1.3.2 [1.3.1]: https://github.com/sparkpost/python-sparkpost/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/sparkpost/python-sparkpost/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/sparkpost/python-sparkpost/compare/v1.1.1...v1.2.0 [1.1.1]: https://github.com/sparkpost/python-sparkpost/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/sparkpost/python-sparkpost/compare/v1.0.5...v1.1.0 [1.0.5]: https://github.com/sparkpost/python-sparkpost/compare/v1.0.4...v1.0.5 [1.0.4]: https://github.com/sparkpost/python-sparkpost/compare/v1.0.3...v1.0.4 [1.0.3]: https://github.com/sparkpost/python-sparkpost/compare/v1.0.2...v1.0.3 [1.0.2]: https://github.com/sparkpost/python-sparkpost/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/sparkpost/python-sparkpost/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/sparkpost/python-sparkpost/compare/1.0.0.dev2...v1.0.0 [1.0.0.dev2]: https://github.com/sparkpost/python-sparkpost/compare/1.0.0.dev1...1.0.0.dev2 [coveralls]: https://coveralls.io/github/SparkPost/python-sparkpost python-sparkpost-1.3.10/CODE_OF_CONDUCT.md000066400000000000000000000062251414251126600177470ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developers@sparkpost.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ python-sparkpost-1.3.10/CONTRIBUTING.md000066400000000000000000000057771414251126600174140ustar00rootroot00000000000000# Contributing to python-sparkpost Transparency is one of our core values, and we encourage developers to contribute and become part of the SparkPost developer community. The following is a set of guidelines for contributing to python-sparkpost, which is hosted in the [SparkPost Organization](https://github.com/sparkpost) on GitHub. These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. ## Submitting Issues * You can create an issue [here](https://github.com/sparkpost/python-sparkpost/issues/new), but before doing that please read the notes below on debugging and submitting issues, and include as many details as possible with your report. * Include the version of python-sparkpost you are using. * Perform a [cursory search](https://github.com/SparkPost/python-sparkpost/issues?q=is%3Aissue+is%3Aopen) to see if a similar issue has already been submitted. ## Local development * Fork this repository * Clone your fork * Install virtualenv: ``pip install virtualenv`` * Run ``make install`` * Run ``source venv/bin/activate`` * Write code! ## Contribution Steps ### Guidelines - Provide documentation for any newly added code. - Provide tests for any newly added code. - Follow PEP8. 1. Create a new branch named after the issue you’ll be fixing (include the issue number as the branch name, example: Issue in GH is #8 then the branch name should be ISSUE-8)) 2. Write corresponding tests and code (only what is needed to satisfy the issue and tests please) * Include your tests in the 'test' directory in an appropriate test file * Write code to satisfy the tests 3. Ensure automated tests pass 4. Submit a new Pull Request applying your feature/fix branch to the develop branch ## Testing Once you are set up for local development: * Run ``make test`` to test against your current Python environment * Open htmlcov/index.html to view coverage information ### Testing all version combinations You can also test all the supported Python and dependencies versions with tox: 1. Install tox: ``pip install tox`` 2. Run tox: ``tox`` If you do not have Python 2.7, 3.4, and 3.5, you can install them with pyenv: 1. Install [pyenv](https://github.com/yyuu/pyenv) 2. Install the required versions of Python: 1. ``pyenv install 2.7.11`` 2. ``pyenv install 3.4.4`` 3. ``pyenv install 3.5.1`` 3. Set the global versions: ``pyenv global 2.7.11 3.4.4 3.5.1`` 4. Run tox: ``tox`` ## Releasing ### Increment the library version number * Update version number in setup.py * Update version number in sparkpost/__init__.py * Update CHANGELOG.md to reflect the changes ### To put python-sparkpost on PyPI * Ensure you have maintainer privileges in PyPI * Update your ``~/.pypirc`` if necessary to contain your username and password (hint: you can run ``python setup.py register``) * Run ``make release``, which will create the dists and upload them to PyPI * Confirm you are able to successfully install the new version by running ``pip install sparkpost`` python-sparkpost-1.3.10/LICENSE000066400000000000000000000010711414251126600161470ustar00rootroot00000000000000Copyright 2015 Message Systems, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this software except in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0.html or in the "license" file accompanying this software. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. python-sparkpost-1.3.10/MANIFEST.in000066400000000000000000000000471414251126600167020ustar00rootroot00000000000000include AUTHORS.rst README.rst LICENSE python-sparkpost-1.3.10/Makefile000066400000000000000000000017341414251126600166100ustar00rootroot00000000000000.PHONY: analysis all build clean docs docs-install docs-open install release release-test test venv all: clean venv install venv: virtualenv venv install: venv . venv/bin/activate; pip install -r dev-requirements.txt . venv/bin/activate; pip install -e . analysis: . venv/bin/activate; flake8 sparkpost test test: analysis . venv/bin/activate; py.test --cov-report term-missing --cov-report html --cov sparkpost test/ docs-install: . venv/bin/activate; pip install -r docs/requirements.txt docs: . venv/bin/activate; cd docs && make html docs-open: docs . venv/bin/activate; open docs/_build/html/index.html release: install . venv/bin/activate; python setup.py sdist bdist_wheel; twine upload -r pypi dist/* release-test: install . venv/bin/activate; python setup.py sdist bdist_wheel; twine upload -r test dist/* build: install . venv/bin/activate; python setup.py sdist . venv/bin/activate; python setup.py bdist_wheel clean: rm -rf venv build dist *.egg-info python-sparkpost-1.3.10/README.rst000066400000000000000000000115331414251126600166350ustar00rootroot00000000000000.. image:: https://www.sparkpost.com/sites/default/files/attachments/SparkPost_Logo_2-Color_Gray-Orange_RGB.svg :target: https://www.sparkpost.com :width: 200px `Sign up`_ for a SparkPost account and visit our `Developer Hub`_ for even more content. .. _Sign up: https://app.sparkpost.com/join?plan=free-0817?src=Social%20Media&sfdcid=70160000000pqBb&pc=GitHubSignUp&utm_source=github&utm_medium=social-media&utm_campaign=github&utm_content=sign-up .. _Developer Hub: https://developers.sparkpost.com SparkPost Python API client =========================== .. image:: https://travis-ci.org/SparkPost/python-sparkpost.svg?branch=master :target: https://travis-ci.org/SparkPost/python-sparkpost :alt: Build Status .. image:: https://readthedocs.org/projects/python-sparkpost/badge/?version=latest :target: https://python-sparkpost.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://coveralls.io/repos/SparkPost/python-sparkpost/badge.svg?branch=master&service=github :target: https://coveralls.io/github/SparkPost/python-sparkpost?branch=master :alt: Coverage Status The official Python package for using the SparkPost API. Documentation ------------- * Documentation for `python-sparkpost`_ * `SparkPost API Reference`_ .. _python-sparkpost: https://python-sparkpost.readthedocs.io/ .. _SparkPost API Reference: https://www.sparkpost.com/api Installation ------------ Install from PyPI using `pip`_: .. code-block:: bash $ pip install sparkpost .. _pip: http://www.pip-installer.org/en/latest/ .. _pip: http://www.pip-installer.org/en/latest/ Python 2.7 or later is required. Get a key --------- Go to `API & SMTP`_ in the SparkPost app and create an API key. We recommend using the ``SPARKPOST_API_KEY`` environment variable: .. code-block:: python from sparkpost import SparkPost sp = SparkPost() # uses environment variable Alternatively, you can pass the API key to the SparkPost class: .. code-block:: python from sparkpost import SparkPost sp = SparkPost('YOUR API KEY') For SparkPost EU and Enterprise accounts, pass in a second parameter to set the API host. .. code-block:: python from sparkpost import SparkPost sp = SparkPost('YOUR API KEY', 'https://api.eu.sparkpost.com') .. _API & SMTP: https://app.sparkpost.com/#/configuration/credentials Send a message -------------- Here at SparkPost, our messages are known as transmissions. Let's use the underlying `transmissions API`_ to send a friendly test message: .. code-block:: python from sparkpost import SparkPost sp = SparkPost() response = sp.transmissions.send( use_sandbox=True, recipients=['someone@somedomain.com'], html='

Hello world

', from_email='test@sparkpostbox.com', subject='Hello from python-sparkpost' ) print(response) # outputs {u'total_accepted_recipients': 1, u'id': u'47960765679942446', u'total_rejected_recipients': 0} .. _transmissions API: https://www.sparkpost.com/api#/reference/transmissions Django Integration ------------------ The SparkPost python library comes with an email backend for Django. Put the following configuration in `settings.py` file. .. code-block:: python SPARKPOST_API_KEY = 'API_KEY' SPARKPOST_BASE_URI = 'api.sparkpost.com' EMAIL_BACKEND = 'sparkpost.django.email_backend.SparkPostEmailBackend' Replace *API_KEY* with an actual API key that you've generated in `Get a Key`_ section. Check out the `full documentation`_ on the Django email backend. If you are using an EU account, set *SPARKPOST_BASE_URI* to `api.eu.sparkpost.com`. The default value is `api.sparkpost.com`. .. _full documentation: https://python-sparkpost.readthedocs.io/en/latest/django/backend.html Using with Google Cloud ----------------------- There are a few simple modifications necessary to enable the use of the underlying ``requests`` library that python-sparkpost uses. First, add the ``requests`` and ``requests-toolbelt`` to your project's ``requirements.txt``: .. code-block:: requests requests-toolbelt Then create or update your ``appengine_config.py`` file to include the following: .. code-block:: python import requests import requests_toolbelt.adapters.appengine requests_toolbelt.adapters.appengine.monkeypatch() Then deploy your app and you should be able to send using python-sparkpost on Google Cloud. Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub and make your changes in a branch on your fork #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Send a pull request. Make sure to add yourself to AUTHORS_. .. _`the repository`: http://github.com/SparkPost/python-sparkpost .. _AUTHORS: https://github.com/SparkPost/python-sparkpost/blob/master/AUTHORS.rst python-sparkpost-1.3.10/dev-requirements.txt000066400000000000000000000000761414251126600212060ustar00rootroot00000000000000-r test-requirements.txt wheel Django>=1.7,<1.10 tornado>=3.2 python-sparkpost-1.3.10/examples/000077500000000000000000000000001414251126600167615ustar00rootroot00000000000000python-sparkpost-1.3.10/examples/base_resource.py000066400000000000000000000006151414251126600221560ustar00rootroot00000000000000import os from sparkpost.base import Resource class Webhooks(Resource): key = "webhooks" def list(self, **kwargs): results = self.request('GET', self.uri, **kwargs) return results api_key = os.environ.get('SPARKPOST_API_KEY', None) webhooks = Webhooks('https://api.sparkpost.com/api/v1', api_key) # returns a list of webhooks for your account print(webhooks.list()) python-sparkpost-1.3.10/examples/recipient_lists/000077500000000000000000000000001414251126600221615ustar00rootroot00000000000000python-sparkpost-1.3.10/examples/recipient_lists/create_recipient_list.py000066400000000000000000000007531414251126600271000ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() response = sp.recipient_lists.create( id='UNIQUE_TEST_ID', name='Test Recipient list', recipients=[ { 'address': { 'email': 'test1@test.com' } }, { 'address': { 'email': 'test2@test.com' } }, { 'address': { 'email': 'test3@test.com' } } ] ) print(response) python-sparkpost-1.3.10/examples/recipient_lists/delete_recipient_list.py000066400000000000000000000001561414251126600270740ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() result = sp.recipient_lists.delete('list_id') print(result) python-sparkpost-1.3.10/examples/recipient_lists/get_recipient_list.py000066400000000000000000000001731414251126600264100ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() recipient_list = sp.recipient_lists.get('list_id') print(recipient_list) python-sparkpost-1.3.10/examples/recipient_lists/get_recipient_list_with_recipients.py000066400000000000000000000002011414251126600316600ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() recipient_list = sp.recipient_lists.get('list_id', True) print(recipient_list) python-sparkpost-1.3.10/examples/recipient_lists/list_recipient_lists.py000066400000000000000000000001651414251126600267700ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() recipient_lists = sp.recipient_lists.list() print(recipient_lists) python-sparkpost-1.3.10/examples/recipient_lists/update_recipient_list.py000066400000000000000000000007521414251126600271160ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() response = sp.recipient_lists.update( 'EXISTING_TEST_ID', name='Test Recipient list', recipients=[ { 'address': { 'email': 'test1@test.com' } }, { 'address': { 'email': 'test2@test.com' } }, { 'address': { 'email': 'test3@test.com' } } ] ) print(response) update_recipient_list_add_single_recipient_maintain_current_recipients.py000066400000000000000000000014661414251126600411240ustar00rootroot00000000000000python-sparkpost-1.3.10/examples/recipient_listsfrom sparkpost import SparkPost # Example list id. Information on getting one is available here: # https://developers.sparkpost.com/api/recipient-lists/#recipient-lists-post-create-a-recipient-list list_id = "1234" # Change this from None to the recipient email that should be added to the recipient list email = None sp = SparkPost() recipient_list = sp.recipient_lists.get(list_id, True) current_recipients = recipient_list["recipients"] recipient_values = [recipient["address"] for recipient in current_recipients] updated_recipients = [ {"address": recipient_value} for recipient_value in recipient_values ] updated_recipients.append({"address": {"email": email.strip().lower(), "name": ""}}) response = sp.recipient_lists.update( list_id, name="list name", recipients=updated_recipients ) print(response) python-sparkpost-1.3.10/examples/suppression_list/000077500000000000000000000000001414251126600224065ustar00rootroot00000000000000python-sparkpost-1.3.10/examples/suppression_list/create_suppression_entries_bulk.py000066400000000000000000000011341414251126600314420ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() recipients = [ { 'email': 'test1@test.com', 'transactional': False, 'non_transactional': True, 'description': 'Test description 1' }, { 'email': 'test2@test.com', 'transactional': True, 'non_transactional': True, 'description': 'Test description 2' }, { 'email': 'test3@test.com', 'transactional': True, 'non_transactional': False, 'description': 'Test description 3' } ] result = sp.suppression_list.create(recipients) print(result) python-sparkpost-1.3.10/examples/suppression_list/create_suppression_entry.py000066400000000000000000000003501414251126600301140ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() result = sp.suppression_list.create({ 'email': 'test@test.com', 'transactional': False, 'non_transactional': True, 'description': 'Test description' }) print(result) python-sparkpost-1.3.10/examples/suppression_list/delete_suppression_entry.py000066400000000000000000000001651414251126600301170ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() result = sp.suppression_list.delete('test@test.com') print(result) python-sparkpost-1.3.10/examples/suppression_list/get_suppression_entry.py000066400000000000000000000001601414251126600274270ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() entry = sp.suppression_list.get('test@test.com') print(entry) python-sparkpost-1.3.10/examples/suppression_list/list_suppression_entries.py000066400000000000000000000003051414251126600301340ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() results = sp.suppression_list.list( from_date='2015-05-07T00:00:00+0000', to_date='2015-05-07T23:59:59+0000', limit=5 ) print(results) python-sparkpost-1.3.10/examples/suppression_list/update_suppression_entries_bulk.py000066400000000000000000000011341414251126600314610ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() recipients = [ { 'email': 'test1@test.com', 'transactional': False, 'non_transactional': True, 'description': 'Test description 1' }, { 'email': 'test2@test.com', 'transactional': True, 'non_transactional': True, 'description': 'Test description 2' }, { 'email': 'test3@test.com', 'transactional': True, 'non_transactional': False, 'description': 'Test description 3' } ] result = sp.suppression_list.update(recipients) print(result) python-sparkpost-1.3.10/examples/suppression_list/update_suppression_enty.py000066400000000000000000000003501414251126600277510ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() result = sp.suppression_list.update({ 'email': 'test@test.com', 'transactional': False, 'non_transactional': True, 'description': 'Test description' }) print(result) python-sparkpost-1.3.10/examples/templates/000077500000000000000000000000001414251126600207575ustar00rootroot00000000000000python-sparkpost-1.3.10/examples/templates/create_template.py000066400000000000000000000004071414251126600244700ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() response = sp.templates.create( id='TEST_ID', name='Test Template', from_email='test@test.com', subject='Test email template!', html='This is a test email template!' ) print(response) python-sparkpost-1.3.10/examples/templates/delete_template.py000066400000000000000000000001541414251126600244660ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() result = sp.templates.delete('template_id') print(result) python-sparkpost-1.3.10/examples/templates/get_template.py000066400000000000000000000001551414251126600240040ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() template = sp.templates.get('template_id') print(template) python-sparkpost-1.3.10/examples/templates/list_templates.py000066400000000000000000000001531414251126600243610ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() template_list = sp.templates.list() print(template_list) python-sparkpost-1.3.10/examples/templates/preview_draft_template.py000066400000000000000000000003011414251126600260570ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() sub_data = { 'first_name': 'John', 'last_name': 'Doe' } template = sp.templates.preview('template_id', sub_data, True) print(template) python-sparkpost-1.3.10/examples/templates/preview_template.py000066400000000000000000000002731414251126600247070ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() sub_data = { 'first_name': 'John', 'last_name': 'Doe' } template = sp.templates.preview('template_id', sub_data) print(template) python-sparkpost-1.3.10/examples/templates/update_template.py000066400000000000000000000004231414251126600245050ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() response = sp.templates.update( 'TEST_ID', name='Test Template', from_email='test@test.com', subject='Updated Test email template!', html='This is a test email template! Updated!' ) print(response) python-sparkpost-1.3.10/examples/transmissions/000077500000000000000000000000001414251126600216755ustar00rootroot00000000000000python-sparkpost-1.3.10/examples/transmissions/a-file.txt000066400000000000000000000000711414251126600235710ustar00rootroot00000000000000Woo hoo! This attachment was added via python-sparkpost. python-sparkpost-1.3.10/examples/transmissions/find_transmission.py000066400000000000000000000001751414251126600260030ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() transmission = sp.transmissions.get('transmission_id') print(transmission) python-sparkpost-1.3.10/examples/transmissions/handle_exception.py000066400000000000000000000021251414251126600255600ustar00rootroot00000000000000""" This script demonstrates how to use the SparkPostAPIException class. This particular example will yield output similar to the following: $ python send_transmission_exception.py 400 {u'errors': [{u'message': u'Invalid domain', u'code': u'7001', u'description': u'Unconfigured Sending Domain '}]} ['Invalid domain Code: 7001 Description: Unconfigured Sending Domain \n'] """ from sparkpost import SparkPost from sparkpost.exceptions import SparkPostAPIException sp = SparkPost() try: response = sp.transmissions.send( recipients=['john.doe@example.com'], text='Hello there', from_email='Testing ', subject='Testing python-sparkpost exceptions' ) except SparkPostAPIException as err: # http response status code print(err.status) # python requests library response object # http://docs.python-requests.org/en/master/api/#requests.Response print(err.response.json()) # list of formatted errors print(err.errors) python-sparkpost-1.3.10/examples/transmissions/schedule_transmission.py000066400000000000000000000015071414251126600266570ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() response = sp.transmissions.send( recipients=[ 'postmaster@example.com', 'you@me.com', { 'address': { 'email': 'john.doe@example.com', 'name': 'John Doe' } } ], html='

Hello world {{name}}

', text='Hello world {{name}}', from_email='test@sparkpostbox.com', subject='Example Script', description='contrived example', custom_headers={ 'X-CUSTOM-HEADER': 'foo bar' }, track_opens=True, track_clicks=True, start_time='2015-11-06T09:10:00-05:00', campaign='python-sparkpost example', metadata={ 'key': 'value', 'arbitrary': 'values' }, substitution_data={ 'name': 'Example User' } ) print(response) python-sparkpost-1.3.10/examples/transmissions/send_transmission.py000066400000000000000000000022321414251126600260100ustar00rootroot00000000000000import os from sparkpost import SparkPost parent_dir = os.path.dirname(os.path.realpath(__file__)) attachment_path = os.path.abspath(os.path.join(parent_dir, "a-file.txt")) sp = SparkPost() response = sp.transmissions.send( recipients=[ 'postmaster@example.com', 'you@me.com', { 'address': { 'email': 'john.doe@example.com', 'name': 'John Doe' } } ], cc=['carboncopy@example.com'], bcc=['blindcarboncopy@example.com'], html='

Hello {{name}}

', text='Hello {{name}}', from_email='Test User ', subject='Example Script', description='contrived example', custom_headers={ 'X-CUSTOM-HEADER': 'foo bar' }, track_opens=True, track_clicks=True, attachments=[ { "name": "test.txt", "type": "text/plain", "filename": attachment_path } ], campaign='sdk example', metadata={ 'key': 'value', 'arbitrary': 'values' }, substitution_data={ 'name': 'Example User' }, reply_to='no-reply@sparkpostmail.com' ) python-sparkpost-1.3.10/examples/transmissions/transmission_stored_list.py000066400000000000000000000010721414251126600274130ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() response = sp.transmissions.send( recipient_list='my_list', html='

Hello world {{name}}

', text='Hello world {{name}}', from_email='test@sparkpostbox.com', subject='Example Script', description='contrived example', custom_headers={ 'X-CUSTOM-HEADER': 'foo bar' }, campaign='sdk example', metadata={ 'key': 'value', 'arbitrary': 'values' }, substitution_data={ 'name': 'Example User' }, reply_to='no-reply@sparkpostmail.com' ) python-sparkpost-1.3.10/examples/transmissions/transmission_stored_template.py000066400000000000000000000002611414251126600302520ustar00rootroot00000000000000from sparkpost import SparkPost sp = SparkPost() response = sp.transmissions.send( recipients=['you@me.com'], template='my-template-id', use_draft_template=True ) python-sparkpost-1.3.10/setup.cfg000066400000000000000000000000321414251126600167570ustar00rootroot00000000000000[bdist_wheel] universal=1 python-sparkpost-1.3.10/setup.py000066400000000000000000000017001414251126600166530ustar00rootroot00000000000000from codecs import open from setuptools import setup, find_packages with open('README.rst', 'r', 'utf-8') as f: readme = f.read() setup( name='sparkpost', version='1.3.10', author='SparkPost', author_email='developers@sparkpost.com', packages=find_packages(), url='https://github.com/SparkPost/python-sparkpost', license='Apache 2.0', description='SparkPost Python API client', long_description=readme, install_requires=['requests>=2.20.1'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Communications :: Email', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ], ) python-sparkpost-1.3.10/sparkpost/000077500000000000000000000000001414251126600171715ustar00rootroot00000000000000python-sparkpost-1.3.10/sparkpost/__init__.py000066400000000000000000000033701414251126600213050ustar00rootroot00000000000000import os from .base import RequestsTransport from .exceptions import SparkPostException from .metrics import Metrics from .recipient_lists import RecipientLists from .suppression_list import SuppressionList from .templates import Templates from .transmissions import Transmissions __version__ = '1.3.10' EU_API = 'api.eu.sparkpost.com' US_API = 'api.sparkpost.com' class SparkPost(object): TRANSPORT_CLASS = RequestsTransport def __init__(self, api_key=None, base_uri=US_API, version='1'): "Set up the SparkPost API client" if not api_key: api_key = self.get_api_key() if not api_key: raise SparkPostException("No API key. Improve message.") self.base_uri = 'https://' + base_uri + '/api/v' + version self.api_key = api_key self.metrics = Metrics(self.base_uri, self.api_key, self.TRANSPORT_CLASS) self.recipient_lists = RecipientLists(self.base_uri, self.api_key, self.TRANSPORT_CLASS) self.suppression_list = SuppressionList(self.base_uri, self.api_key, self.TRANSPORT_CLASS) self.templates = Templates(self.base_uri, self.api_key, self.TRANSPORT_CLASS) self.transmissions = Transmissions(self.base_uri, self.api_key, self.TRANSPORT_CLASS) # Keeping self.transmission for backwards compatibility. # Will be removed in a future release. self.transmission = self.transmissions def get_api_key(self): "Get API key from environment variable" return os.environ.get('SPARKPOST_API_KEY', None) python-sparkpost-1.3.10/sparkpost/base.py000066400000000000000000000030231414251126600204530ustar00rootroot00000000000000import sparkpost from .exceptions import SparkPostAPIException class RequestsTransport(object): def __init__(self): import requests self.sess = requests.Session() def request(self, method, uri, headers, **kwargs): response = self.sess.request(method, uri, headers=headers, **kwargs) if response.status_code == 204: return True if not response.ok: raise SparkPostAPIException(response) if 'results' in response.json(): return response.json()['results'] return response.json() class Resource(object): key = "" def __init__(self, base_uri, api_key, transport_class=RequestsTransport): self.base_uri = base_uri self.api_key = api_key self.transport = transport_class() @property def uri(self): return "%s/%s" % (self.base_uri, self.key) def request(self, method, uri, **kwargs): headers = { 'User-Agent': 'python-sparkpost/' + sparkpost.__version__, 'Content-Type': 'application/json', 'Authorization': self.api_key } response = self.transport.request(method, uri, headers=headers, **kwargs) return response def get(self): raise NotImplementedError def list(self): raise NotImplementedError def create(self): raise NotImplementedError def update(self): raise NotImplementedError def delete(self): raise NotImplementedError python-sparkpost-1.3.10/sparkpost/django/000077500000000000000000000000001414251126600204335ustar00rootroot00000000000000python-sparkpost-1.3.10/sparkpost/django/__init__.py000066400000000000000000000000001414251126600225320ustar00rootroot00000000000000python-sparkpost-1.3.10/sparkpost/django/email_backend.py000066400000000000000000000024261414251126600235470ustar00rootroot00000000000000from django.conf import settings from django.core.mail.backends.base import BaseEmailBackend from sparkpost import SparkPost, US_API from .message import SparkPostMessage class SparkPostEmailBackend(BaseEmailBackend): """ SparkPost wrapper for Django email backend """ def __init__(self, fail_silently=False, **kwargs): super(SparkPostEmailBackend, self)\ .__init__(fail_silently=fail_silently, **kwargs) sp_api_key = getattr(settings, 'SPARKPOST_API_KEY', None) sp_base_uri = getattr(settings, 'SPARKPOST_BASE_URI', US_API) self.client = SparkPost(sp_api_key, sp_base_uri) def send_messages(self, email_messages): """ Send emails, returns integer representing number of successful emails """ success = 0 for message in email_messages: try: response = self._send(SparkPostMessage(message)) success += response['total_accepted_recipients'] except Exception: if not self.fail_silently: raise return success def _send(self, message): params = getattr(settings, 'SPARKPOST_OPTIONS', {}).copy() params.update(message) return self.client.transmissions.send(**params) python-sparkpost-1.3.10/sparkpost/django/exceptions.py000066400000000000000000000001341414251126600231640ustar00rootroot00000000000000class UnsupportedContent(Exception): pass class UnsupportedParam(Exception): pass python-sparkpost-1.3.10/sparkpost/django/message.py000066400000000000000000000067321414251126600224410ustar00rootroot00000000000000import mimetypes from base64 import b64encode from django.core.mail import EmailMultiAlternatives from django.core.mail.message import DEFAULT_ATTACHMENT_MIME_TYPE from django.conf import settings from .exceptions import UnsupportedContent class SparkPostMessage(dict): """ Takes a Django EmailMessage and formats it for use with the SparkPost API. The dictionary returned would be formatted like this: { 'recipients': ['recipient@example.com'], 'from_email': 'from@example.com', 'text': 'Hello world', 'html': '

Hello world

', 'subject': 'Hello from the SparkPost Django email backend' } """ def __init__(self, message): formatted = dict() if message.to: formatted['recipients'] = message.to if message.from_email: formatted['from_email'] = message.from_email if message.subject: formatted['subject'] = message.subject if hasattr(message, 'template'): formatted['template'] = message.template elif message.content_subtype == 'html': formatted['html'] = message.body else: formatted['text'] = message.body if message.cc: formatted['cc'] = message.cc if message.bcc: formatted['bcc'] = message.bcc if hasattr(message, 'reply_to') and message.reply_to: formatted['reply_to'] = ','.join(message.reply_to) if isinstance(message, EmailMultiAlternatives): for alternative in message.alternatives: if alternative[1] == 'text/html': formatted['html'] = alternative[0] else: raise UnsupportedContent( 'Content type %s is not supported' % alternative[1] ) if message.attachments: formatted['attachments'] = [] str_encoding = settings.DEFAULT_CHARSET for attachment in message.attachments: filename, content, mimetype = attachment if mimetype is None: mimetype, _ = mimetypes.guess_type(filename) if mimetype is None: mimetype = DEFAULT_ATTACHMENT_MIME_TYPE try: if isinstance(content, unicode): content = content.encode(str_encoding) except NameError: if isinstance(content, str): content = content.encode(str_encoding) base64_encoded_content = b64encode(content) formatted['attachments'].append({ 'name': filename, 'data': base64_encoded_content.decode('ascii'), 'type': mimetype }) if hasattr(message, 'substitution_data'): formatted['substitution_data'] = message.substitution_data if hasattr(message, 'campaign'): formatted['campaign'] = message.campaign if message.extra_headers: formatted['custom_headers'] = message.extra_headers if 'X-MSYS-API' in message.extra_headers: import json msys_api = json.loads(message.extra_headers['X-MSYS-API']) if msys_api and msys_api.get('options', {}).get('transactional', False): # noqa: E501 formatted['transactional'] = True super(SparkPostMessage, self).__init__(formatted) python-sparkpost-1.3.10/sparkpost/exceptions.py000066400000000000000000000022301414251126600217210ustar00rootroot00000000000000class SparkPostException(Exception): pass class SparkPostAPIException(SparkPostException): "Handle 4xx and 5xx errors from the SparkPost API" def __init__(self, response, *args, **kwargs): # noinspection PyBroadException try: errors = response.json()['errors'] error_template = "{message} Code: {code} Description: {desc} \n" errors = [error_template.format(message=e.get('message', ''), code=e.get('code', 'none'), desc=e.get('description', 'none')) for e in errors] # TODO: select exception to catch here except: # noqa: E722 errors = [response.text or ""] self.status = response.status_code self.response = response self.errors = errors message = """Call to {uri} returned {status_code}, errors: {errors} """.format( uri=response.url, status_code=response.status_code, errors='\n'.join(errors) ) super(SparkPostAPIException, self).__init__(message, *args, **kwargs) python-sparkpost-1.3.10/sparkpost/metrics.py000066400000000000000000000013341414251126600212120ustar00rootroot00000000000000from .base import Resource, RequestsTransport class Metrics(object): "Wrapper for sub-resources" def __init__(self, base_uri, api_key, transport_class=RequestsTransport): self.base_uri = "%s/%s" % (base_uri, 'metrics') self.campaigns = Campaigns(self.base_uri, api_key, transport_class) self.domains = Domains(self.base_uri, api_key, transport_class) class Campaigns(Resource): key = 'campaigns' def list(self, **kwargs): results = self.request('GET', self.uri, **kwargs) return results['campaigns'] class Domains(Resource): key = 'domains' def list(self, **kwargs): results = self.request('GET', self.uri, **kwargs) return results['domains'] python-sparkpost-1.3.10/sparkpost/recipient_lists.py000066400000000000000000000071271414251126600227520ustar00rootroot00000000000000import json from .base import Resource class RecipientLists(Resource): """ RecipientLists class used to create, update, delete, list and get recipient lists. For detailed request and response formats, see the `Recipient Lists API documentation `_. """ key = 'recipient-lists' def _translate_keys(self, **kwargs): model = {} if 'id' in kwargs: model['id'] = kwargs.get('id') model['name'] = kwargs.get('name') model['description'] = kwargs.get('description') model['attributes'] = kwargs.get('attributes') model['recipients'] = kwargs.get('recipients') return model def create(self, **kwargs): """ Create a recipient list based on the supplied parameters :param str id: ID used to reference the recipient list :param str name: Editable display name :param str description: Detailed description of the recipient list :param dict attributes: Arbitrary metadata related to the list :param list recipients: Array of recipient dicts :returns: a ``dict`` with the ID, name, and number of accepted and rejected recipients :raises: :exc:`SparkPostAPIException` if API call fails """ payload = self._translate_keys(**kwargs) results = self.request('POST', self.uri, data=json.dumps(payload)) return results def update(self, list_id, **kwargs): """ Update a recipient list by ID based on the supplied parameters :param str list_id: ID of the recipient list you want to update :param str name: Editable display name :param str description: Detailed description of the recipient list :param dict attributes: Arbitrary metadata related to the list :param list recipients: Array of recipient dicts :returns: a ``dict`` with the ID, name, and number of accepted and rejected recipients :raises: :exc:`SparkPostAPIException` if API call fails """ uri = "%s/%s" % (self.uri, list_id) payload = self._translate_keys(**kwargs) results = self.request('PUT', uri, data=json.dumps(payload)) return results def delete(self, list_id): """ Delete a recipient list by ID :param str list_id: ID of the recipient list you want to delete :returns: empty ``dict`` :raises: :exc:`SparkPostAPIException` if recipient list is not found or if recipient list is in use """ uri = "%s/%s" % (self.uri, list_id) results = self.request('DELETE', uri) return results def get(self, list_id, show_recipients=None): """ Get a recipient list by ID :param str list_id: ID of the recipient list you want to retrieve :param bool show_recipients: If True, returns attributes for all recipients :returns: the requested recipient list if found :raises: :exc:`SparkPostAPIException` if recipient list is not found """ uri = "%s/%s" % (self.uri, list_id) params = {} if show_recipients is not None: params['show_recipients'] = str(show_recipients).lower() results = self.request('GET', uri, params=params) return results def list(self): """ Get a list of your recipient lists :returns: list of recipient lists :raises: :exc:`SparkPostAPIException` if API call fails """ results = self.request('GET', self.uri) return results python-sparkpost-1.3.10/sparkpost/suppression_list.py000066400000000000000000000067171414251126600232030ustar00rootroot00000000000000import json from .base import Resource class SuppressionList(Resource): """ SuppressionList class used to search, get and modify suppression status. For detailed request and response formats, see the `Suppresion List API documentation `_. """ key = 'suppression-list' def list(self, **kwargs): """ List supression list entries based on the supplied parameters :param datetime from_date: DateTime to start listing :param datetime to_date: DateTime to end listing :param list types: Types of entries to return :param int limit: Maximum number of entries to return :returns: a ``list`` of entries :raises: :exc:`SparkPostAPIException` if API call fails """ key_map = { 'from_date': 'from', 'to_date': 'to', 'types': 'types', 'sources': 'sources', 'limit': 'limit' } params = dict([(key_map[i], kwargs[i]) for i in list(key_map.keys()) if i in kwargs]) results = self.request('GET', self.uri, params=params) return results def get(self, email): """ Retrieve a suppression list entry for a specific recipient by email :param str email: Email of the recipient whose status you want to check_status :returns: a suppression list entry :raises: :exc:`SparkPostAPIException` if API call fails """ uri = "%s/%s" % (self.uri, email) results = self.request('GET', uri) return results def _upsert(self, status): uri = self.uri if isinstance(status, dict): # single upsert, update uri and remove email property uri = "%s/%s" % (self.uri, status.pop("email", None)) else: status = {"recipients": status} results = self.request('PUT', uri, data=json.dumps(status)) return results def create(self, entry): """ Create a suppression list entry. :param dict|list status: If dict it is a single entry to create ``{ 'email': 'test@test.com', 'transactional': True, 'non_transactional': True, 'description': 'Test description' }``, if list it is multiple entries to create :returns: a ``dict`` with a message :raises: :exc:`SparkPostAPIException` if API call fails """ return self._upsert(entry) def update(self, entry): """ Update a suppression list entry. :param dict|list status: If dict it is a single entry to update ``{ 'email': 'test@test.com', 'transactional': True, 'non_transactional': True, 'description': 'Test description' }``, if list it is multiple entries to update :returns: a ``dict`` with a message :raises: :exc:`SparkPostAPIException` if API call fails """ return self._upsert(entry) def delete(self, email): """ Delete the suppression status for a specific recipient by email :param str email: Email of the recipient whose status you want to remove :returns: TODO :raises: :exc:`SparkPostAPIException` if API call fails """ uri = "%s/%s" % (self.uri, email) results = self.request('DELETE', uri) return results python-sparkpost-1.3.10/sparkpost/templates.py000066400000000000000000000160771414251126600215540ustar00rootroot00000000000000import json from .base import Resource class Templates(Resource): """ Templates class used to create, update, delete, list and get templates. For detailed request and response formats, see the `Templates API documentation `_. """ key = 'templates' def _translate_keys(self, **kwargs): model = { 'content': {}, 'options': {} } if 'id' in kwargs: model['id'] = kwargs.get('id') model['name'] = kwargs.get('name') model['description'] = kwargs.get('description') model['published'] = kwargs.get('published') model['options']['open_tracking'] = kwargs.get('track_opens') model['options']['click_tracking'] = kwargs.get('track_clicks') model['options']['transactional'] = kwargs.get('is_transactional') model['content']['html'] = kwargs.get('html') model['content']['text'] = kwargs.get('text') model['content']['subject'] = kwargs.get('subject') model['content']['from'] = kwargs.get('from_email') model['content']['reply_to'] = kwargs.get('reply_to') model['content']['headers'] = kwargs.get('custom_headers') return model def create(self, **kwargs): """ Create a template based on the supplied parameters :param str id: ID used to reference the template :param str name: Editable display name :param str description: Detailed description of the template :param bool published: Defaults to False. Whether the template is a published or draft version :param bool track_opens: Defaults to transmission level setting. Used to track opens of transmission :param bool track_clicks: Defaults to transmission level setting. Used to track clicks of transmission :param bool is_transactional: Defaults to transmission level setting. Distinguishes between transactional and non-transactional messages for unsubscribe and suppression purposes :param str html: HTML part of template :param str text: Text part of template :param str subject: Subject of template :param str from_email: Friendly from of template, domain must be a verified sending domain to your account or template create will fail :param str reply_to: Reply to of template :param dict custom_headers: Used to set any headers associated with template :returns: a ``dict`` with the ID :raises: :exc:`SparkPostAPIException` if template uses an unverified sending domain or there's a syntax error in the content """ payload = self._translate_keys(**kwargs) results = self.request('POST', self.uri, data=json.dumps(payload)) return results def update(self, template_id, **kwargs): """ Update a template by ID based on the supplied parameters :param str template_id: ID of the template you want to retrieve :param str name: Editable display name :param str description: Detailed description of the template :param bool published: Defaults to False. Whether the template is a published or draft version :param bool track_opens: Defaults to transmission level setting. Used to track opens of transmission :param bool track_clicks: Defaults to transmission level setting. Used to track clicks of transmission :param bool is_transactional: Defaults to transmission level setting. Distinguishes between transactional and non-transactional messages for unsubscribe and suppression purposes :param str html: HTML part of template :param str text: Text part of template :param str subject: Subject of template :param str from_email: Friendly from of template, domain must be a verified sending domain to your account or template create will fail :param str reply_to: Reply to of template :param dict custom_headers: Used to set any headers associated with template :returns: TODO :raises: :exc:`SparkPostAPIException` if template is not found """ uri = "%s/%s" % (self.uri, template_id) payload = self._translate_keys(**kwargs) results = self.request('PUT', uri, data=json.dumps(payload)) return results def delete(self, template_id): """ Delete a template by ID :param str template_id: ID of the template you want to delete :returns: TODO :raises: :exc:`SparkPostAPIException` if template is not found or if template is in use """ uri = "%s/%s" % (self.uri, template_id) results = self.request('DELETE', uri) return results def get(self, template_id, draft=None): """ Get a template by ID :param str template_id: ID of the template you want to retrieve :param bool draft: Defaults to None. If True, returns the most recent draft template. If False, returns the most recent published template. If None, returns the most recent template version regardless of draft or published. :returns: the requested template if found :raises: :exc:`SparkPostAPIException` if template is not found """ uri = "%s/%s" % (self.uri, template_id) params = {} if draft is not None: params['draft'] = str(draft).lower() results = self.request('GET', uri, params=params) return results def list(self): """ Get a list of your templates :returns: list of templates :raises: :exc:`SparkPostAPIException` if API call fails """ results = self.request('GET', self.uri) return results def preview(self, template_id, substitution_data, draft=None): """ Get a preivew of your template by ID with the provided substitution_data :param str template_id: ID of the template you want to retrieve :param dict substitution_data: data to be substituted in the template content :param bool draft: Defaults to None. If True, previews the most recent draft template. If False, previews the most recent published template. If None, previews the most recent template version regardless of draft or published. :returns: the requested template if found with content expanded using substitution data provided :raises: :exc:`SparkPostAPIException` if API call fails """ uri = "%s/%s/preview" % (self.uri, template_id) params = {} if draft is not None: params['draft'] = str(draft).lower() data = json.dumps({'substitution_data': substitution_data}) results = self.request('POST', uri, params=params, data=data) return results python-sparkpost-1.3.10/sparkpost/tornado/000077500000000000000000000000001414251126600206375ustar00rootroot00000000000000python-sparkpost-1.3.10/sparkpost/tornado/__init__.py000066400000000000000000000011301414251126600227430ustar00rootroot00000000000000import sparkpost from .exceptions import SparkPostAPIException from .base import TornadoTransport from .transmissions import Transmissions __all__ = ["SparkPost", "TornadoTransport", "SparkPostAPIException", "Transmissions"] class SparkPost(sparkpost.SparkPost): TRANSPORT_CLASS = TornadoTransport def __init__(self, *args, **kwargs): super(SparkPost, self).__init__(*args, **kwargs) self.transmissions = Transmissions(self.base_uri, self.api_key, self.TRANSPORT_CLASS) self.transmission = self.transmissions python-sparkpost-1.3.10/sparkpost/tornado/base.py000066400000000000000000000022051414251126600221220ustar00rootroot00000000000000import json from tornado import gen from tornado.httpclient import AsyncHTTPClient, HTTPError from .exceptions import SparkPostAPIException class TornadoTransport(object): @gen.coroutine def request(self, method, uri, headers, **kwargs): if "data" in kwargs: kwargs["body"] = kwargs.pop("data") client = AsyncHTTPClient() try: response = yield client.fetch(uri, method=method, headers=headers, **kwargs) except HTTPError as ex: raise SparkPostAPIException(ex.response) if response.code == 204: raise gen.Return(True) if response.code == 200: result = None # noinspection PyBroadException try: result = json.loads(response.body.decode("utf-8")) # TODO: select exception to catch here except: # noqa: E722 pass if result: if 'results' in result: raise gen.Return(result['results']) raise gen.Return(result) raise SparkPostAPIException(response) python-sparkpost-1.3.10/sparkpost/tornado/exceptions.py000066400000000000000000000022221414251126600233700ustar00rootroot00000000000000import json from ..exceptions import SparkPostAPIException as RequestsSparkPostAPIException class SparkPostAPIException(RequestsSparkPostAPIException): def __init__(self, response, *args, **kwargs): errors = None # noinspection PyBroadException try: data = json.loads(response.body.decode("utf-8")) if data: errors = data['errors'] errors = [e['message'] + ': ' + e.get('description', '') for e in errors] # TODO: select exception to catch here except: # noqa: E722 pass if not errors: errors = [response.body.decode("utf-8") or ""] self.status = response.code self.response = response self.errors = errors message = """Call to {uri} returned {status_code}, errors: {errors} """.format( uri=response.effective_url, status_code=response.code, errors='\n'.join(errors) ) super(RequestsSparkPostAPIException, self).__init__(message, *args, **kwargs) python-sparkpost-1.3.10/sparkpost/tornado/transmissions.py000066400000000000000000000004401414251126600241230ustar00rootroot00000000000000from .utils import wrap_future from ..transmissions import Transmissions as SyncTransmissions class Transmissions(SyncTransmissions): def get(self, transmission_id): results = self._fetch_get(transmission_id) return wrap_future(results, lambda f: f["transmission"]) python-sparkpost-1.3.10/sparkpost/tornado/utils.py000066400000000000000000000005151414251126600223520ustar00rootroot00000000000000from tornado.concurrent import Future def wrap_future(future, convert): wrapper = Future() def handle_future(future): try: wrapper.set_result(convert(future.result())) except Exception as ex: wrapper.set_exception(ex) future.add_done_callback(handle_future) return wrapper python-sparkpost-1.3.10/sparkpost/transmissions.py000066400000000000000000000320741414251126600224650ustar00rootroot00000000000000import base64 import copy import json import warnings from email.utils import parseaddr from .base import Resource from .exceptions import SparkPostException try: string_types = basestring except NameError: string_types = str # Python 3 doesn't have basestring model_remap = { 'campaign': 'campaign_id', 'start_time': 'options/start_time', 'track_opens': 'options/open_tracking', 'track_initial_opens': 'options/initial_open', 'track_clicks': 'options/click_tracking', 'transactional': 'options/transactional', 'use_sandbox': 'options/sandbox', 'skip_suppression': 'options/skip_suppression', 'ip_pool': 'options/ip_pool', 'inline_css': 'options/inline_css', 'perform_substitutions': 'options/perform_substitutions', 'email_rfc822': 'content/email_rfc822', 'custom_headers': 'content/headers', 'use_draft_template': 'content/use_draft_template', 'reply_to': 'content/reply_to', 'subject': 'content/subject', 'from_email': 'content/from', 'html': 'content/html', 'amp_html': 'content/amp_html', 'text': 'content/text', 'template': 'content/template_id', 'attachments': 'content/attachments', 'inline_images': 'content/inline_images', 'recipient_list': 'recipients/list_id', } model_remap_keys = frozenset(model_remap.keys()) class Transmissions(Resource): """ Transmission class used to send, list and get transmissions. For detailed request and response formats, see the `Transmissions API documentation `_. """ key = 'transmissions' def _translate_keys(self, **kwargs): model = copy.deepcopy(kwargs) # Intersection of keys that need to be remapped data_remap_keys = model_remap_keys.intersection(model.keys()) for from_key in data_remap_keys: # Remap model keys to match API if from_key in model: to_model = model to_key = model_remap[from_key] if '/' in to_key: # Nested within a dict into_list = to_key.split('/') to_key = into_list[-1] to_model = model.setdefault(into_list[0], {}) # Move from current key and place into new key to_model[to_key] = model.pop(from_key) content = model.setdefault('content', {}) recipients = model.setdefault('recipients', []) if content.get('email_rfc822'): # Remove unnecessary keys from model['content'], if rfc822 rfc822_keys = frozenset([ 'headers', 'use_draft_template', 'reply_to', 'subject', 'from', 'html', 'text', 'template_id', 'attachments', 'inline_images', ]) del_content_keys = rfc822_keys.intersection(content.keys()) for key in del_content_keys: del content[key] if 'from' in content: from_email = content.get('from') if isinstance(from_email, string_types): content['from'] = self._parse_address(from_email) if 'attachments' in content: attachments = content.get('attachments') content['attachments'] = self._extract_attachments(attachments) if 'inline_images' in content: inline_images = content.get('inline_images') content['inline_images'] = self._extract_attachments(inline_images) if recipients and not isinstance(recipients, dict): model['recipients'] = self._extract_recipients(recipients) recipients = model['recipients'] cc = model.pop('cc', None) if cc: headers = content.setdefault('headers', {}) headers['CC'] = ','.join(cc) cc_copies = self._format_copies(recipients, cc) recipients.extend(cc_copies) bcc = model.pop('bcc', None) if bcc: bcc_copies = self._format_copies(recipients, bcc) recipients.extend(bcc_copies) return model def _format_copies(self, recipients, copies): formatted_copies = [] if len(recipients) > 0: formatted_copies = self._extract_recipients(copies) main_recipient = copy.deepcopy(recipients[0]) main_recipient.pop('address') for recipient in formatted_copies: recipient['address'].update({ 'header_to': self._format_header_to(recipients[0]) }) recipient.update(**main_recipient) return formatted_copies def _format_header_to(self, recipient): if 'name' in recipient['address']: return '"{name}" <{email}>'.format( name=recipient['address']['name'], email=recipient['address']['email'] ) return recipient['address']['email'] def _extract_attachments(self, attachments): formatted_attachments = [] for attachment in attachments: formatted_attachment = {} formatted_attachment['type'] = attachment.get('type') formatted_attachment['name'] = attachment.get('name') if 'filename' in attachment: formatted_attachment['data'] = self._get_base64_from_file( attachment['filename']) else: formatted_attachment['data'] = attachment.get('data') formatted_attachments.append(formatted_attachment) return formatted_attachments def _get_base64_from_file(self, filename): with open(filename, "rb") as a_file: encoded_string = base64.b64encode(a_file.read()).decode("ascii") return encoded_string def _parse_address(self, address): name, email = parseaddr(address) parsed_address = { 'email': email } if name: parsed_address['name'] = name return parsed_address def _extract_recipients(self, recipients): if not (isinstance(recipients, (list, dict))): raise SparkPostException('recipients must be a list or dict') formatted_recipients = [] for recip in recipients: if isinstance(recip, string_types): formatted_recipients.append({ 'address': self._parse_address(recip) }) else: formatted_recipients.append(recip) return formatted_recipients def send(self, **kwargs): """ Send a transmission based on the supplied parameters :param list|dict recipients: List of email addresses, or a dict: ``{'address': {'name': 'Kyla', 'email': 'hello@example.com' }}`` :param str recipient_list: ID of recipient list. If this is set, the `recipients` param will be ignored :param cc: List of email addresses to send carbon copy to :param bcc: List of email addresses to send blind carbon copy to :param str template: ID of template to be used. Setting a template overrides the HTML and text params :param bool use_draft_template: Defaults to False. Set to true if you want to send a template that is a draft :param str html: HTML part of transmission :param str amp_html: AMP HTML part of the transmission :param str text: Text part of transmission :param str subject: Subject of transmission :param str from_email: Email that the transmission comes from. The domain must be a verified sending domain belonging to your account or the transmission will fail. You can pass a from email or both from name and from email - `testing@example.com` or `Test Email ` will both work. :param str reply_to: Reply to of transmission :param str return_path: Email address to use for envelope FROM. The domain part of the return_path address must be a CNAME-verified sending domain. :param str description: Description of transmission :param str campaign: Campaign of transmission :param dict metadata: Any data you want to send along with transmission, used in WebHooks :param dict substitution_data: Corresponds to substitutions in html/text content. See `substitutions reference `_. :param attachments: List of dicts. For example: .. code-block:: python dict( type='application/pdf', name='document.pdf', data='base64 encoded string' ) Replace `data` with `filename` if you want the library to perform the base64 conversion. For example: .. code-block:: python dict( type='application/pdf', name='document.pdf', filename='/full/path/to/document.pdf' ) :param inline_images: List of dicts. For example: .. code-block:: python dict( type='image/png', name='imageCID', data='base64 encoded string' ) Replace `data` with `filename` if you want the library to perform the base64 conversion. For example: .. code-block:: python dict( type='image/png', name='imageCID', filename='/full/path/to/image.png' ) :param str start_time: Delay generation of messages until this datetime. Format YYYY-MM-DDTHH:MM:SS+-HH:MM. Example: '2015-02-11T08:00:00-04:00'. :param bool track_opens: Defaults to True. Used to track opens of transmission :param bool track_initial_opens: Used to track opens of transmission with top pixel :param bool track_clicks: Defaults to True. Used to track clicks of transmission :param bool use_sandbox: Flag must be set to use sandbox domain instead of verified sending domain. Limited to a lifetime of 50 transmissions with this domain :param bool transactional: Whether message is transactional or non-transactional for unsubscribe and suppression purposes :param bool skip_suppression: Whether or not to ignore customer suppression rules, for this transmission only. Only applicable if your configuration supports this parameter. (Enterprise only) :param str ip_pool: The ID of an IP pool associated with your account :param bool inline_css: Whether or not to perform CSS inlining :param bool perform_substitutions: Whether or not to enable substitutions (default is True) :param dict custom_headers: Used to set any headers associated with transmission. See `header notes `_ :returns: a ``dict`` with the transmission ID and number of accepted and rejected recipients :raises: :exc:`SparkPostAPIException` if transmission cannot be sent """ payload = self._translate_keys(**kwargs) data = json.dumps(payload) results = self.request('POST', self.uri, data=data) return results def _fetch_get(self, transmission_id): uri = "%s/%s" % (self.uri, transmission_id) results = self.request('GET', uri) return results def get(self, transmission_id): """ Get a transmission by ID :param str transmission_id: ID of the transmission you want to retrieve :returns: the requested transmission, if found :raises: :exc:`SparkPostAPIException` if transmission is not found """ results = self._fetch_get(transmission_id) return results['transmission'] def list(self, **kwargs): """ Get a list of your transmissions. This method is deprecated. :param campaign_id: ID of the campaign used by the transmissions :param template_id: ID of the template used by the transmissions :returns: list of transmissions :raises: :exc:`SparkPostAPIException` if API call fails """ warn_msg = 'This method is deprecated.' warnings.warn(warn_msg, DeprecationWarning) return self.request('GET', self.uri, params=kwargs) def delete(self, transmission_id): """ Delete a transmission by ID :param str transmission_id: ID of the transmission you want to delete :returns: {} if transmission is deleted :raises: :exc:`SparkPostAPIException` if transmission is not found or Canceled """ uri = "%s/%s" % (self.uri, transmission_id) results = self.request('DELETE', uri) return results python-sparkpost-1.3.10/test-requirements.txt000066400000000000000000000001251414251126600214020ustar00rootroot00000000000000flake8 pytest==2.8.7 pytest-cov==1.8.1 requests==2.20.1 responses==0.3.0 mock==2.0.0 python-sparkpost-1.3.10/test/000077500000000000000000000000001414251126600161225ustar00rootroot00000000000000python-sparkpost-1.3.10/test/__init__.py000066400000000000000000000000001414251126600202210ustar00rootroot00000000000000python-sparkpost-1.3.10/test/assets/000077500000000000000000000000001414251126600174245ustar00rootroot00000000000000python-sparkpost-1.3.10/test/assets/sparkpostdev.png000066400000000000000000000146261414251126600226700ustar00rootroot00000000000000PNG  IHDR\]IDATx^=lי`z] 6U-a$:j-AE"P1 #€$ N BM ٭6qKfj#=25w3<@>?~ӟ]G{#<MxЛ7@oބ z&<MxЛ7@oބ z&<MxЛ7@oބ z&<MxЛ7@oބ z&<[kWT/?X{ fTӣ?˗־k?iEx3{{Z{k.\X?ݼyGS|>_;:::筭7>Ok믴|'W766ȟ׮\tOAx)m=Xw|p<|Ox@KHVc)ZbtшX1n4<ŋ߂#4yMS3ggi&kB!fYvw TIxbǣGO?Psa=zG0uz88XnޣGln= !t 1Z{: bg!F4>ӜE2u*!'-p`ƒ["Gcs'GxX]5DO늾 Vo-1V HOxev7C]&ku} b|x]UE1FEC$O#>Cjz!8𠷸$ΛLƒ^nΫ*ښ+A/Ullꃮ4+`✮iWkC;wCƒN,z{U>o qͫ<b%#<8W,߀$Jlm<!<*ow|xh&l6~해Ƀ)eynF)Gh&# EQTI@vwߛ7<*2IۘSI8q8NUJ!<*2ՖU(F#:]N.G./2 Kx*XsiEC0*J*Ue\£`=iizku-(Xy赧ܜV:Rwʗ =~o{+(\귿Ql%3? $yPuCxT"zЩ톓 I?<޽(H.xЙF/'&!<*˗ߘY\r {`£2T!ޘ>C{8\ڢ̃_&_{Z9 ~b(# 򆹎yCNJtcpDՑSEj V}rUʪN£B9VA-Uuʱ Va(v֪jܽYUXcF“r X]eUGŢm>O>sGDpF|=DEGrn;a|S6UghbC`.NICxT.OxwnJ$5B#̨K?ϿяFe|⋿fWo7xpv?v!sh47:ם;o Ӊrĩv%K#Oݸ69Yb1$&Hello There

' return { 'total_accepted_recipients': 0, 'total_rejected_recipients': 0 } with mock.patch.object(Transmissions, 'send') as mock_send: mock_send.side_effect = new_send send_mail( 'test subject', 'hello there', 'from@example.com', ['to@example.com'], html_message='

Hello There

' ) def test_send_plain_mail_after_html_mail(): SPARKPOST_OPTIONS = { 'track_opens': False, 'track_clicks': False, 'transactional': True, } reconfigure_settings(SPARKPOST_OPTIONS=SPARKPOST_OPTIONS) def new_send(**kwargs): assert kwargs['text'] == 'hello there' assert kwargs['html'] == '

Hello There

' return { 'total_accepted_recipients': 0, 'total_rejected_recipients': 0 } def new_send_text_only(**kwargs): assert kwargs['text'] == 'hello there again in text only' assert "html" not in kwargs return { 'total_accepted_recipients': 0, 'total_rejected_recipients': 0 } with mock.patch.object(Transmissions, 'send') as mock_send: mock_send.side_effect = new_send send_mail( 'test subject', 'hello there', 'from@example.com', ['to@example.com'], html_message='

Hello There

' ) mock_send.side_effect = new_send_text_only send_mail( 'test subject 2', 'hello there again in text only', 'from@example.com', ['to@example.com'], ) def test_unsupported_content_types(): params = get_params() with pytest.raises(UnsupportedContent): mail = EmailMultiAlternatives( params['subject'], 'plain text', params['from_email'], params['recipient_list']) mail.attach_alternative('non-plain content', 'text/alien') mail.send() def test_settings_options(): SPARKPOST_OPTIONS = { 'track_opens': False, 'track_clicks': False, 'transactional': True, } reconfigure_settings(SPARKPOST_OPTIONS=SPARKPOST_OPTIONS) with mock.patch.object(Transmissions, 'send'): mailer(get_params()) expected_kargs = get_params().copy() expected_kargs["text"] = expected_kargs["message"] expected_kargs["recipients"] = expected_kargs["recipient_list"] del expected_kargs["message"] del expected_kargs["recipient_list"] expected_kargs.update(SPARKPOST_OPTIONS) Transmissions.send.assert_called_with(**expected_kargs) python-sparkpost-1.3.10/test/django/test_message.py000066400000000000000000000144631414251126600224310ustar00rootroot00000000000000from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.core.mail.message import EmailMessage from django.utils.functional import empty from sparkpost.django.message import SparkPostMessage from .utils import at_least_version def reconfigure_settings(**new_settings): old_settings = settings._wrapped settings._wrapped = empty settings.configure(default_settings=old_settings, **new_settings) reconfigure_settings( DEFAULT_CHARSET='utf-8' ) base_options = dict( subject='Test', body='Testing', from_email='test@from.com', to=['recipient@example.com'] ) def message(**options): options.update(base_options) email_message = EmailMessage(**options) return SparkPostMessage(email_message) def multipart_message(**options): options.update(base_options) email_message = EmailMultiAlternatives(**options) email_message.attach_alternative('

Testing

', 'text/html') return SparkPostMessage(email_message) base_expected = dict( recipients=['recipient@example.com'], from_email='test@from.com', subject='Test', text='Testing' ) def test_minimal(): assert message() == base_expected def test_multipart(): expected = dict( html='

Testing

' ) expected.update(base_expected) assert multipart_message() == expected def test_cc_bcc(): expected = dict( cc=['ccone@example.com'], bcc=['bccone@example.com'] ) expected.update(base_expected) options = dict(cc=['ccone@example.com'], bcc=['bccone@example.com']) assert message(**options) == expected def test_attachment(): email_message = EmailMessage(**base_options) email_message.attach('file.txt', 'test content', 'text/plain') actual = SparkPostMessage(email_message) expected = dict( attachments=[ { 'name': 'file.txt', 'data': 'dGVzdCBjb250ZW50', 'type': 'text/plain' } ] ) expected.update(base_expected) assert actual == expected def test_attachment_unicode(): email_message = EmailMessage(**base_options) email_message.attach('file.txt', u'test content', 'text/plain') actual = SparkPostMessage(email_message) expected = dict( attachments=[ { 'name': 'file.txt', 'data': 'dGVzdCBjb250ZW50', 'type': 'text/plain' } ] ) expected.update(base_expected) assert actual == expected def test_attachment_guess_mimetype(): email_message = EmailMessage(**base_options) email_message.attach('file.txt', 'test content') actual = SparkPostMessage(email_message) expected = dict( attachments=[ { 'name': 'file.txt', 'data': 'dGVzdCBjb250ZW50', 'type': 'text/plain' } ] ) expected.update(base_expected) assert actual == expected def test_attachment_guess_mimetype_fallback(): email_message = EmailMessage(**base_options) email_message.attach('file', 'test content') actual = SparkPostMessage(email_message) expected = dict( attachments=[ { 'name': 'file', 'data': 'dGVzdCBjb250ZW50', 'type': 'application/octet-stream' } ] ) expected.update(base_expected) assert actual == expected def test_content_subtype(): email_message = EmailMessage( to=['to@example.com'], from_email='test@from.com', body='

Testing

' ) email_message.content_subtype = 'html' actual = SparkPostMessage(email_message) expected = dict( recipients=['to@example.com'], from_email='test@from.com', html='

Testing

' ) assert actual == expected def test_template(): email_message = EmailMessage( to=['to@example.com'], from_email='test@from.com' ) email_message.template = 'template-id' actual = SparkPostMessage(email_message) expected = dict( recipients=['to@example.com'], from_email='test@from.com', template='template-id' ) assert actual == expected def test_campaign(): email_message = EmailMessage(**base_options) email_message.campaign = 'campaign-id' actual = SparkPostMessage(email_message) expected = dict( campaign='campaign-id' ) expected.update(base_expected) assert actual == expected def test_substitution_data(): email_message = EmailMessage( to=[ { "address": "to@example.com", "substitution_data": { "key": "value" } } ], from_email='test@from.com' ) email_message.template = 'template-id' email_message.substitution_data = {"key2": "value2"} actual = SparkPostMessage(email_message) expected = dict( recipients=[ { "address": "to@example.com", "substitution_data": { "key": "value" } } ], from_email='test@from.com', template='template-id', substitution_data={"key2": "value2"} ) assert actual == expected if at_least_version('1.8'): def test_reply_to(): expected = dict( reply_to='replyone@example.com,replytwo@example.com' ) expected.update(base_expected) assert message(reply_to=['replyone@example.com', 'replytwo@example.com']) == expected def test_extra_headers(): email_message = EmailMessage(**base_options) email_message.extra_headers['FOO'] = 'bar' actual = SparkPostMessage(email_message) expected = dict( custom_headers={'FOO': 'bar'}, ) expected.update(base_expected) assert actual == expected def test_transactional(): email_message = EmailMessage(**base_options) import json msys_api = json.dumps({'options': {'transactional': True}}) email_message.extra_headers['X-MSYS-API'] = msys_api actual = SparkPostMessage(email_message) expected = dict( custom_headers={'X-MSYS-API': msys_api}, transactional=True, ) expected.update(base_expected) assert actual == expected python-sparkpost-1.3.10/test/django/utils.py000066400000000000000000000002561414251126600211010ustar00rootroot00000000000000from distutils.version import StrictVersion from django import get_version def at_least_version(version): return StrictVersion(get_version()) > StrictVersion(version) python-sparkpost-1.3.10/test/test_base.py000066400000000000000000000061451414251126600204530ustar00rootroot00000000000000import pytest import responses from sparkpost.base import Resource from sparkpost.exceptions import SparkPostAPIException fake_base_uri = 'https://fake-base.com' fake_api_key = 'fake-api-key' fake_resource_key = 'fake-resource-key' fake_uri = "%s/%s" % (fake_base_uri, fake_resource_key) def create_resource(): resource = Resource(fake_base_uri, fake_api_key) resource.key = fake_resource_key return resource def test_uri(): resource = create_resource() assert resource.uri == fake_uri @responses.activate def test_success_request(): responses.add( responses.GET, fake_uri, status=200, content_type='application/json', body='{}' ) resource = create_resource() results = resource.request('GET', resource.uri) assert results == {} @responses.activate def test_success_request_with_results(): responses.add( responses.GET, fake_uri, status=200, content_type='application/json', body='{"results": []}' ) resource = create_resource() results = resource.request('GET', resource.uri) assert results == [] @responses.activate def test_fail_request(): responses.add( responses.GET, fake_uri, status=500, content_type='application/json', body='{"errors": [{"message": "failure", "description": "desc"}]}' ) resource = create_resource() with pytest.raises(SparkPostAPIException): resource.request('GET', resource.uri) @responses.activate def test_fail_wrongjson_request(): responses.add( responses.GET, fake_uri, status=500, content_type='application/json', body='{"errors": ["Error!"]}' ) resource = create_resource() with pytest.raises(SparkPostAPIException): resource.request('GET', resource.uri) @responses.activate def test_fail_nojson_request(): responses.add( responses.GET, fake_uri, status=500, content_type='application/json', body='{"errors": ' ) resource = create_resource() with pytest.raises(SparkPostAPIException): resource.request('GET', resource.uri) @responses.activate def test_fail_no_errors(): responses.add( responses.GET, fake_uri, status=500, content_type='application/json', body='no errors' ) resource = create_resource() with pytest.raises(SparkPostAPIException): resource.request('GET', resource.uri) def test_fail_get(): resource = create_resource() with pytest.raises(NotImplementedError): resource.get() def test_fail_list(): resource = create_resource() with pytest.raises(NotImplementedError): resource.list() def test_fail_create(): resource = create_resource() with pytest.raises(NotImplementedError): resource.create() def test_fail_update(): resource = create_resource() with pytest.raises(NotImplementedError): resource.update() def test_fail_delete(): resource = create_resource() with pytest.raises(NotImplementedError): resource.delete() python-sparkpost-1.3.10/test/test_init.py000066400000000000000000000002741414251126600205010ustar00rootroot00000000000000import pytest from sparkpost import SparkPost from sparkpost.exceptions import SparkPostException def test_no_api_key(): with pytest.raises(SparkPostException): SparkPost() python-sparkpost-1.3.10/test/test_metrics.py000066400000000000000000000032751414251126600212100ustar00rootroot00000000000000import pytest import responses from sparkpost import SparkPost from sparkpost.exceptions import SparkPostAPIException @responses.activate def test_success_campaigns(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/metrics/campaigns', status=200, content_type='application/json', body='{"results": {"campaigns": []}}' ) sp = SparkPost('fake-key') results = sp.metrics.campaigns.list() assert results == [] @responses.activate def test_fail_campaigns(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/metrics/campaigns', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.metrics.campaigns.list() @responses.activate def test_success_domains(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/metrics/domains', status=200, content_type='application/json', body='{"results": {"domains": []}}' ) sp = SparkPost('fake-key') results = sp.metrics.domains.list() assert results == [] @responses.activate def test_fail_domains(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/metrics/domains', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.metrics.domains.list() python-sparkpost-1.3.10/test/test_recipient_lists.py000066400000000000000000000103561414251126600227400ustar00rootroot00000000000000import pytest import responses from sparkpost import SparkPost from sparkpost import RecipientLists from sparkpost.exceptions import SparkPostAPIException def test_translate_keys_with_id(): t = RecipientLists('uri', 'key') results = t._translate_keys(id='test_id') assert results['id'] == 'test_id' @responses.activate def test_success_create(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/recipient-lists', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.recipient_lists.create() assert results == 'yay' @responses.activate def test_fail_create(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/recipient-lists', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.recipient_lists.create() @responses.activate def test_success_update(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/recipient-lists/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.recipient_lists.update('foobar', name='foobar') assert results == 'yay' @responses.activate def test_fail_update(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/recipient-lists/foobar', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.recipient_lists.update('foobar', name='foobar') @responses.activate def test_success_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/recipient-lists/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.recipient_lists.delete('foobar') assert results == 'yay' @responses.activate def test_fail_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/recipient-lists/foobar', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.recipient_lists.delete('foobar') @responses.activate def test_success_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/recipient-lists/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.recipient_lists.get('foobar') assert results == "yay" @responses.activate def test_success_get_with_recipients(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/recipient-lists/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.recipient_lists.get('foobar', True) assert results == "yay" @responses.activate def test_fail_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/recipient-lists/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.recipient_lists.get('foobar') @responses.activate def test_success_list(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/recipient-lists', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') response = sp.recipient_lists.list() assert response == "yay" python-sparkpost-1.3.10/test/test_suppression_list.py000066400000000000000000000107071414251126600231650ustar00rootroot00000000000000import pytest import responses from sparkpost import SparkPost from sparkpost.exceptions import SparkPostAPIException @responses.activate def test_success_list(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/suppression-list', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.suppression_list.list() assert results == 'yay' @responses.activate def test_fail_list(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/suppression-list', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.suppression_list.list() @responses.activate def test_success_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=200, content_type='application/json', body='{"results": []}' ) sp = SparkPost('fake-key') results = sp.suppression_list.get('foobar') assert results == [] @responses.activate def test_fail_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.suppression_list.get('foobar') @responses.activate def test_success_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=204, content_type='application/json' ) sp = SparkPost('fake-key') results = sp.suppression_list.delete('foobar') assert results is True @responses.activate def test_fail_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.suppression_list.delete('foobar') @responses.activate def test_success_upsert(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.suppression_list._upsert({"email": "foobar"}) assert results == 'yay' @responses.activate def test_success_create(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.suppression_list.create({"email": "foobar"}) assert results == 'yay' @responses.activate def test_success_update(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.suppression_list.update({"email": "foobar"}) assert results == 'yay' @responses.activate def test_success_upsert_bulk(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/suppression-list', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.suppression_list._upsert([ {"email": "foobar1"}, {"email": "foobar2"} ]) assert results == 'yay' @responses.activate def test_fail_upsert(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/suppression-list/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.suppression_list._upsert({"email": "foobar"}) python-sparkpost-1.3.10/test/test_templates.py000066400000000000000000000132261414251126600215350ustar00rootroot00000000000000try: from urllib.parse import urlparse # TODO: select exception to catch here except: # noqa: E722 from urlparse import urlparse import pytest import responses from sparkpost import SparkPost from sparkpost import Templates from sparkpost.exceptions import SparkPostAPIException def test_translate_keys_with_id(): t = Templates('uri', 'key') results = t._translate_keys(id='test_id') assert results['id'] == 'test_id' @responses.activate def test_success_create(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/templates', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.templates.create() assert results == 'yay' @responses.activate def test_fail_create(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/templates', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.templates.create() @responses.activate def test_success_update(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/templates/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.templates.update('foobar', name='foobar') assert results == 'yay' @responses.activate def test_fail_update(): responses.add( responses.PUT, 'https://api.sparkpost.com/api/v1/templates/foobar', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.templates.update('foobar', name='foobar') @responses.activate def test_success_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/templates/foobar', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.templates.delete('foobar') assert results == 'yay' @responses.activate def test_fail_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/templates/foobar', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.templates.delete('foobar') @responses.activate def test_success_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/templates/foobar', status=200, content_type='application/json', body='{"results": {}}' ) sp = SparkPost('fake-key') results = sp.templates.get('foobar') assert results == {} @responses.activate def test_success_get_with_is_draft(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/templates/foobar?draft=true', match_querystring=True, status=200, content_type='application/json', body='{"results": {}}' ) sp = SparkPost('fake-key') results = sp.templates.get('foobar', True) assert results == {} @responses.activate def test_fail_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/templates/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.templates.get('foobar') @responses.activate def test_success_list(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/templates', status=200, content_type='application/json', body='{"results": []}' ) sp = SparkPost('fake-key') response = sp.templates.list() assert response == [] @responses.activate def test_success_preview(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/templates/foobar/preview', status=200, content_type='application/json', body='{"results": {}}' ) sp = SparkPost('fake-key') results = sp.templates.preview('foobar', {}) assert responses.calls[0].request.body == '{"substitution_data": {}}' assert results == {} @responses.activate def test_success_preview_with_draft(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/templates/foobar/preview?draft=true', match_querystring=True, status=200, content_type='application/json', body='{"results": {}}' ) sp = SparkPost('fake-key') results = sp.templates.preview('foobar', {}, True) parsed = urlparse(responses.calls[0].request.url) assert parsed.query == 'draft=true' assert results == {} @responses.activate def test_fail_preview(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/templates/foobar/preview', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.templates.preview('foobar', {}) python-sparkpost-1.3.10/test/test_transmissions.py000066400000000000000000000310041414251126600224450ustar00rootroot00000000000000import base64 import json import os import tempfile import warnings import pytest import responses import six from mock import patch from sparkpost import SparkPost from sparkpost import Transmissions from sparkpost.exceptions import SparkPostAPIException, SparkPostException def test_translate_keys_with_list(): t = Transmissions('uri', 'key') results = t._translate_keys(recipient_list='test') assert 'use_draft_template' not in results['content'] assert 'headers' not in results['content'] assert results['recipients'] == {'list_id': 'test'} def test_translate_keys_with_recips(): t = Transmissions('uri', 'key') results = t._translate_keys(recipients=['test', {'key': 'value'}, 'foobar']) assert results['recipients'] == [{'address': {'email': 'test'}}, {'key': 'value'}, {'address': {'email': 'foobar'}}] def test_exceptions_for_recipients(): t = Transmissions('uri', 'key') with pytest.raises(SparkPostException): t._translate_keys(recipients='test') def test_translate_keys_with_unicode_recips(): t = Transmissions('uri', 'key') results = t._translate_keys(recipients=[u'unicode_email@example.com', 'str_email@example.com']) assert results['recipients'] == [ {'address': {'email': 'unicode_email@example.com'}}, {'address': {'email': 'str_email@example.com'}} ] def test_translate_keys_for_email_parsing(): t = Transmissions('uri', 'key') results = t._translate_keys(recipients=['hansel@example.com', 'Gretel ']) assert results['recipients'] == [ {'address': {'email': 'hansel@example.com'}}, {'address': {'name': 'Gretel', 'email': 'gretel@example.com'}} ] def test_translate_keys_for_from_email(): t = Transmissions('uri', 'key') results = t._translate_keys(from_email='Testing ') assert results['content']['from'] == { 'name': 'Testing', 'email': 'testing@example.com' } @responses.activate def test_campaign_id(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.transmission.send(campaign="test") assert results == 'yay' def test_format_header_to(): t = Transmissions('uri', 'key') formatted = t._format_header_to(recipient={ 'address': {'email': 'primary@example.com'} }) assert formatted == 'primary@example.com' formatted = t._format_header_to(recipient={ 'address': {'name': 'Testing', 'email': 'primary@example.com'} }) assert formatted == '"Testing" ' def test_cc_with_sub_data(): t = Transmissions('uri', 'key') results = t._translate_keys( recipients=[{ 'address': {'email': 'primary@example.com'}, 'substitution_data': {'fake': 'data'} }], cc=['ccone@example.com'] ) assert results['recipients'] == [ { 'address': {'email': 'primary@example.com'}, 'substitution_data': {'fake': 'data'} }, { 'address': { 'email': 'ccone@example.com', 'header_to': 'primary@example.com' }, 'substitution_data': {'fake': 'data'} } ] def test_translate_keys_with_cc(): t = Transmissions('uri', 'key') results = t._translate_keys(recipients=['primary@example.com'], cc=['ccone@example.com']) assert results['recipients'] == [ {'address': {'email': 'primary@example.com'}}, {'address': {'email': 'ccone@example.com', 'header_to': 'primary@example.com'}}, ] assert results['content']['headers'] == { 'CC': 'ccone@example.com' } def test_translate_keys_with_multiple_cc(): t = Transmissions('uri', 'key') results = t._translate_keys(recipients=['primary@example.com'], cc=['ccone@example.com', 'cctwo@example.com']) assert results['recipients'] == [ {'address': {'email': 'primary@example.com'}}, {'address': {'email': 'ccone@example.com', 'header_to': 'primary@example.com'}}, {'address': {'email': 'cctwo@example.com', 'header_to': 'primary@example.com'}}, ] assert results['content']['headers'] == { 'CC': 'ccone@example.com,cctwo@example.com' } def test_translate_keys_with_bcc(): t = Transmissions('uri', 'key') results = t._translate_keys(recipients=['primary@example.com'], bcc=['bccone@example.com']) assert results['recipients'] == [ {'address': {'email': 'primary@example.com'}}, {'address': {'email': 'bccone@example.com', 'header_to': 'primary@example.com'}}, ] def test_translate_keys_with_inline_css(): t = Transmissions('uri', 'key') results = t._translate_keys(inline_css=True) assert results['options'].get('inline_css') is True def test_translate_keys_with_email_rfc822(): t = Transmissions('uri', 'key') # Build data using implicit cat, as max line length is enforced eml = ( 'To: wilma \n', 'From: fred \n', 'Subject: Bedrock declaration\n', 'MIME-Version: 1.0\n', 'Content-Type: text/plain; charset=utf-8; format=flowed\n', 'Content-Transfer-Encoding: 7bit\nContent-Language: en-GB\n', '\n', 'When in the Course of human events we yell yabba dabba doo.', ) # Just a selection. Don't test attribs with irregular naming non_rfc822_attrs = { 'headers': 'foo', 'reply_to': 'foo', 'subject': 'foo', 'html': 'foo', 'text': 'foo', } # Demonstrate that email_rfc822 overrides other content attributes test_content = {'email_rfc822': eml} test_content.update(non_rfc822_attrs) results = t._translate_keys(**test_content) for i in non_rfc822_attrs: assert results['content'].get(i) is None assert results['content'].get('email_rfc822') is not None @responses.activate def test_success_send(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = sp.transmission.send() assert results == 'yay' @responses.activate def test_success_send_with_attachments(): try: # Let's compare unicode for Python 2 / 3 compatibility test_content = six.u("Hello \nWorld\n") (_, temp_file_path) = tempfile.mkstemp() with open(temp_file_path, "w") as temp_file: temp_file.write(test_content) responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') attachment = { "name": "test.txt", "type": "text/plain", "filename": temp_file_path } results = sp.transmission.send(attachments=[attachment]) request_params = json.loads(responses.calls[0].request.body) content = base64.b64decode( request_params["content"]["attachments"][0]["data"]) # Let's compare unicode for Python 2 / 3 compatibility assert test_content == content.decode("ascii") assert results == 'yay' attachment = { "name": "test.txt", "type": "text/plain", "data": base64.b64encode( test_content.encode("ascii")).decode("ascii") } results = sp.transmission.send(attachments=[attachment]) request_params = json.loads(responses.calls[1].request.body) content = base64.b64decode( request_params["content"]["attachments"][0]["data"]) # Let's compare unicode for Python 2 / 3 compatibility assert test_content == content.decode("ascii") assert results == 'yay' finally: os.unlink(temp_file_path) @responses.activate def test_success_send_with_inline_images(): current_dir = os.path.abspath(os.path.dirname(__file__)) image_path = os.path.join(current_dir, 'assets', 'sparkpostdev.png') with open(image_path, "rb") as image: encoded_image = base64.b64encode(image.read()).decode("ascii") responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') image_data = { "name": "sparkpostdev", "type": "image/png", "filename": image_path } results = sp.transmission.send(inline_images=[image_data]) request_params = json.loads(responses.calls[0].request.body) content = request_params["content"]["inline_images"][0]["data"] assert encoded_image == content assert results == 'yay' image_data = { "name": "sparkpostdev", "type": "image/png", "data": encoded_image } results = sp.transmission.send(inline_images=[image_data]) request_params = json.loads(responses.calls[1].request.body) content = request_params["content"]["inline_images"][0]["data"] assert content == encoded_image assert results == 'yay' @responses.activate def test_fail_send(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.transmission.send() @responses.activate def test_success_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions/foobar', status=200, content_type='application/json', body='{"results": {"transmission": {}}}' ) sp = SparkPost('fake-key') results = sp.transmission.get('foobar') assert results == {} @responses.activate def test_fail_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.transmission.get('foobar') @responses.activate @patch.object(warnings, 'warn') def test_success_list(mock_warn): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": []}' ) sp = SparkPost('fake-key') response = sp.transmission.list() assert mock_warn.called assert response == [] @responses.activate def test_success_list_with_params(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions?template_id=abcd', status=200, content_type='application/json', body='{"results": []}', match_querystring=True ) sp = SparkPost('fake-key') response = sp.transmission.list(template_id='abcd') assert response == [] @responses.activate def test_success_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/transmissions/foobar', status=200, content_type='application/json', body='{}' ) sp = SparkPost('fake-key') results = sp.transmission.delete('foobar') assert results == {} @responses.activate def test_fail_delete(): responses.add( responses.DELETE, 'https://api.sparkpost.com/api/v1/transmissions/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "resource not found", "description": "Resource not found:transmission id foobar"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') sp.transmission.delete('foobar') python-sparkpost-1.3.10/test/tornado/000077500000000000000000000000001414251126600175705ustar00rootroot00000000000000python-sparkpost-1.3.10/test/tornado/__init__.py000066400000000000000000000000001414251126600216670ustar00rootroot00000000000000python-sparkpost-1.3.10/test/tornado/test_tornado.py000066400000000000000000000124211414251126600226470ustar00rootroot00000000000000import base64 import json import os import tempfile import pytest import six from sparkpost.tornado import SparkPost, SparkPostAPIException from tornado import ioloop from .utils import AsyncClientMock responses = AsyncClientMock() @responses.activate def test_success_send(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') results = ioloop.IOLoop().run_sync(sp.transmission.send) assert results == 'yay' @responses.activate def test_success_send_with_attachments(): try: # Let's compare unicode for Python 2 / 3 compatibility test_content = six.u("Hello \nWorld\n") (_, temp_file_path) = tempfile.mkstemp() with open(temp_file_path, "w") as temp_file: temp_file.write(test_content) responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": "yay"}' ) sp = SparkPost('fake-key') attachment = { "name": "test.txt", "type": "text/plain", "filename": temp_file_path } def send(): return sp.transmission.send(attachments=[attachment]) results = ioloop.IOLoop().run_sync(send) request_params = json.loads(responses.calls[0].request.body) content = base64.b64decode( request_params["content"]["attachments"][0]["data"]) # Let's compare unicode for Python 2 / 3 compatibility assert test_content == content.decode("ascii") assert results == 'yay' attachment = { "name": "test.txt", "type": "text/plain", "data": base64.b64encode( test_content.encode("ascii")).decode("ascii") } def send(): return sp.transmission.send(attachments=[attachment]) results = ioloop.IOLoop().run_sync(send) request_params = json.loads(responses.calls[1].request.body) content = base64.b64decode( request_params["content"]["attachments"][0]["data"]) # Let's compare unicode for Python 2 / 3 compatibility assert test_content == content.decode("ascii") assert results == 'yay' finally: os.unlink(temp_file_path) @responses.activate def test_fail_send(): responses.add( responses.POST, 'https://api.sparkpost.com/api/v1/transmissions', status=500, content_type='application/json', body=""" {"errors": [{"message": "You failed", "description": "More Info"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') ioloop.IOLoop().run_sync(sp.transmission.send) @responses.activate def test_success_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions/foobar', status=200, content_type='application/json', body='{"results": {"transmission": {}}}' ) sp = SparkPost('fake-key') def send(): return sp.transmission.get('foobar') results = ioloop.IOLoop().run_sync(send, timeout=3) assert results == {} @responses.activate def test_fail_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions/foobar', status=404, content_type='application/json', body=""" {"errors": [{"message": "cant find", "description": "where you go"}]} """ ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') def send(): return sp.transmission.get('foobar') ioloop.IOLoop().run_sync(send, timeout=3) @responses.activate def test_nocontent_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions', status=204, content_type='application/json', body='' ) sp = SparkPost('fake-key') response = ioloop.IOLoop().run_sync(sp.transmission.list) assert response is True @responses.activate def test_brokenjson_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results":' ) with pytest.raises(SparkPostAPIException): sp = SparkPost('fake-key') ioloop.IOLoop().run_sync(sp.transmission.list) @responses.activate def test_noresults_get(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"ok": false}' ) sp = SparkPost('fake-key') response = ioloop.IOLoop().run_sync(sp.transmission.list) assert response == {"ok": False} @responses.activate def test_success_list(): responses.add( responses.GET, 'https://api.sparkpost.com/api/v1/transmissions', status=200, content_type='application/json', body='{"results": []}' ) sp = SparkPost('fake-key') response = ioloop.IOLoop().run_sync(sp.transmission.list) assert response == [] python-sparkpost-1.3.10/test/tornado/utils.py000066400000000000000000000032511414251126600213030ustar00rootroot00000000000000from __future__ import print_function from collections import namedtuple from responses import RequestsMock from tornado import ioloop from tornado.concurrent import Future from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError Request = namedtuple("Request", ["url", "method", "headers", "body"]) class ResponseGenerator(object): def get_adapter(self, url): return self def build_response(self, request, response): resp = HTTPResponse(request, response.status, headers=response.headers, effective_url=request.url, error=None, buffer="") resp._body = response.data f = Future() f.content = None if response.status < 200 or response.status >= 300: resp.error = HTTPError(response.status, response=resp) ioloop.IOLoop().current().add_callback(f.set_exception, resp.error) else: ioloop.IOLoop().current().add_callback(f.set_result, resp) return f class AsyncClientMock(RequestsMock): def start(self): import mock def unbound_on_send(client, request, callback=None, **kwargs): if not isinstance(request, HTTPRequest): request = Request(request, kwargs.get("method", "GET"), kwargs.get("headers", []), kwargs.get("body", "")) return self._on_request(ResponseGenerator(), request) self._patcher = mock.patch('tornado.httpclient.AsyncHTTPClient.fetch', unbound_on_send) self._patcher.start() def stop(self): self._patcher.stop() python-sparkpost-1.3.10/tox.ini000066400000000000000000000005611414251126600164600ustar00rootroot00000000000000[tox] envlist = {py27,py34}-django{17,18}, py35-django{18,19} [testenv] deps = -rtest-requirements.txt django17: Django>=1.7,<1.8 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 commands = py.test test/ [testenv:py35-django19] commands = flake8 sparkpost test py.test --cov-report term-missing --cov-report html --cov sparkpost test/