├── .codeclimate.yml ├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── examples ├── account_info.py ├── api_version.py ├── jobs_create.py ├── jobs_delete.py ├── jobs_download.py ├── jobs_parse.py ├── jobs_results.py ├── jobs_search.py ├── jobs_start.py ├── jobs_status.py ├── poe_confirm.py └── single_check.py ├── neverbounce_sdk ├── __init__.py ├── account.py ├── auth.py ├── bulk.py ├── core.py ├── exceptions.py ├── poe.py ├── single.py └── utils.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_account_api.py ├── test_api_errors.py ├── test_bulk_api.py ├── test_core.py ├── test_download.py ├── test_poe.py ├── test_sanity.py └── test_single_check_api.py └── tox.ini /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: false 5 | config: 6 | languages: 7 | - python 8 | fixme: 9 | enabled: true 10 | pep8: 11 | enabled: true 12 | ratings: 13 | paths: 14 | - "neverbounce_sdk/**.py" 15 | exclude_paths: 16 | - tests/ 17 | - examples/ 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * NeverBounceApi-Python version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pypi credentials 2 | .pypirc 3 | # 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # pyenv python configuration file 62 | .python-version 63 | 64 | # Editor and OS cruft 65 | *.swp? 66 | *~ 67 | **/.idea 68 | **/.DS_STORE 69 | 70 | .env 71 | venv/ 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - python: 2.7 5 | env: TOXENV=py27 6 | - python: 3.4 7 | env: TOXENV=py34 8 | - python: 3.5 9 | env: TOXENV=py35 10 | - python: 3.6 11 | env: TOXENV=py36 12 | - python: pypy 13 | env: TOXENV=pypy 14 | - python: pypy3 15 | env: TOXENV=pypy3 16 | - python: 3.7 17 | env: TOXENV=py37 18 | sudo: required 19 | dist: xenial 20 | - python: 3.6 21 | env: TOXENV=flake8 22 | install: 23 | - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then pip install virtualenv==15.2.0 tox==3.1.3; fi 24 | - pip install tox 25 | script: tox 26 | notifications: 27 | email: false 28 | slack: 29 | secure: dXqqWrZYZh44jMYZMy21vpU5KqHcYhjIO90yrJH3cyuWaWRMMtPe8CIgOBHXCmPsx6BAa0VD5T/DFVvtU5Vrs2R1rFcZW0rfXElpaHHx43Kkx893lY8QjEy713syB+QSwgO0gD1Js9Ri03sAg628zO8dDIXMCPYth6kL3oROj8xgvSmjQbASYNZz6FQPzvodZqWLe0tcbxvpyizJd6eHZJmEqXcJzFHl1VqLkTd9BoKuvCPDbwzJsRlGbt7u1H7Ae/p9edBK2flwX7lJChqKLk4V3urjNv1vXmuo8Cfwx5s6sGvIaZG5OazsAcxhghJE0HSFXCz2TXBj7hR1n4FRC1NnG7jH2R8F6hnUUYc9NAzTxVzYYYFLE8t622hKYVq9Pt0Seo0Dc0UGo3SQJjqBNuDaZL18wx0fkBh4x7w1rsetyIYRUvq2bKlyRgMVw6MLU+1Lu+b//4hplH7gi1PIowfZ3lbBzZQsbMyCraAXiW5p8OG9LIV+AZnqdoazQGMfOpCiB4alPtb4lBDbiuGkF6L64iAiYsuPrLbzRQZui8a+o6NyKsEt/6UHt7+kqLI4PC03nd60qN4Hb9p9w/VpeTZ+WmM0I/bpbAa6dI4upOwia2f6rfUAaHe9VJ4jMr9Q8XNTCYB/mx2+S2Nm81jU+mH0NYlSL3/CC3AtehfKDRM= 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017, NeverBounce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | 4 | recursive-include tests * 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | 8 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | find . -name '*.egg-info' -exec rm -fr {} + 36 | find . -name '*.egg' -exec rm -f {} + 37 | 38 | clean-pyc: ## remove Python file artifacts 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '__pycache__' -exec rm -fr {} + 43 | 44 | clean-test: ## remove test and coverage artifacts 45 | rm -fr .tox/ 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | 49 | lint: ## check style with flake8 50 | flake8 neverbounce_sdk tests examples 51 | 52 | test: ## run tests quickly with the default Python 53 | py.test -v 54 | 55 | test-all: ## run tests on every Python version with tox 56 | tox 57 | 58 | coverage: ## check code coverage quickly with the default Python 59 | coverage run --source neverbounce_sdk -m pytest 60 | coverage report -m 61 | coverage html 62 | $(BROWSER) htmlcov/index.html 63 | 64 | release: clean ## package and upload a release 65 | python setup.py sdist 66 | python setup.py bdist_wheel 67 | twine upload --config-file=./.pypirc dist/* 68 | 69 | dist: clean ## builds source and wheel package 70 | python setup.py sdist 71 | python setup.py bdist_wheel 72 | ls -l dist 73 | 74 | install: clean ## install the package to the active Python's site-packages 75 | python setup.py install 76 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ********************* 2 | NeverBounceApi-Python 3 | ********************* 4 | 5 | .. image:: https://neverbounce-marketing.s3.amazonaws.com/neverbounce_color_600px.png 6 | :target: https://neverbounce.com 7 | :width: 600 8 | :align: center 9 | :alt: NeverBounce Logo 10 | 11 | |travisci| |codeclimate| 12 | 13 | Welcome to NeverBounce's Python SDK! We hope that it will aid you in consuming 14 | our service. Please report any bugs to the github issue tracker and make sure 15 | you read the documentation before use. 16 | 17 | Installation 18 | ------------ 19 | 20 | The preferred method of installation is with ``pip`` in a virtual environment:: 21 | 22 | pip install neverbounce_sdk 23 | 24 | If you must use ``easy_install``, you may. If you'd like to install for local 25 | development:: 26 | 27 | git clone git@github.com:NeverBounce/NeverBounceApi-Python.git 28 | cd NeverBounceApi-Python 29 | pip install -e . 30 | 31 | This will install ``neverbounce_sdk`` into the active environment in editable 32 | mode. 33 | 34 | 35 | Usage 36 | ----- 37 | 38 | **The API username and secret key used to authenticate V3 API requests will not work to authenticate V4 API requests.** If you are attempting to authenticate your request with the 8 character username or 12-16 character secret key the request will return an `auth_failure` error. The API key used for the V4 API will look like the following: `secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`. To create new V4 API credentials please go [here](https://app.neverbounce.com/apps/custom-integration/new). 39 | 40 | The NeverBounce Python SDK provides a simple interface by which to interact 41 | with NeverBounce's email verification API version 4. To get up and running, make sure 42 | you have your API token on hand:: 43 | 44 | import neverbounce_sdk 45 | api_key = 'secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 46 | client = neverbounce_sdk.client(api_key=api_key,timeout=30) 47 | 48 | And now you're ready to use the client. You can check your account 49 | information:: 50 | 51 | info = client.account_info() 52 | # info is {'billing_type': 'default', 'credits': 12345, ... } 53 | 54 | You can verify single emails:: 55 | 56 | resp = client.single_check('test@example.com') 57 | resp['result'] # 'invalid' 58 | resp['execution_time'] # 285 59 | 60 | And you can create, query the status of, and control email verification bulk 61 | jobs:: 62 | 63 | emails = [ 64 | {'email': 'tomato@veggies.com'}, # must have an 'email' key 65 | {'email': 'cucumber@veggies.com', 66 | 'best_when': 'cold'}, # may contain "metadata" 67 | ] 68 | job = client.jobs_create(emails) 69 | 70 | # all state-changing methods return a status object 71 | resp = client.jobs_parse(job['id'], auto_start=False) 72 | assert resp['status'] == 'success' 73 | 74 | client.jobs_start(job['id']) 75 | progress = client.jobs_status(job['id']) 76 | print(progress) # dict with keys 'job_status', 'started', 'percent_complete', etc 77 | 78 | When creating a job, you may attach "metadata" in the form of additional keys 79 | to each object (python ``dict``) included in the job input listing. Note that 80 | these additional keys will be *broadcasted*; i.e., every row of the result set 81 | for the job will contain an entry for every key - if the value of the key was 82 | not specified in the input, it will be the empty string in the job processing's 83 | output. 84 | 85 | All API operations return dictionaries with information about the execution of 86 | the operation or the results of the operation, whichever is more appropriate. 87 | The only exceptions are the ``client.search`` and ``client.results`` functions. 88 | The response generated by these API endpoints is paginated; therefore these 89 | functions return custom iterators that allow you to iterate across the API's 90 | pagination:: 91 | 92 | all_my_jobs = client.jobs_search() 93 | type(all_my_jobs) # neverbounce_sdk.bulk.ResultIter 94 | for job in all_my_jobs: 95 | # process job 96 | # this loop will make API calls behind the scenes, so be careful! 97 | if all_my_jobs.page > 10: 98 | break 99 | 100 | The ``ResultIter`` will pull down pages behind the scenes, so be careful! A 101 | ``ResultIter`` will expose the raw API response as a ``data`` attribute, the 102 | current page number as ``page``, and the total number of pages as ``total_pages``, 103 | so you can use these attributes to implement finer-grained control over result 104 | iteration. Additionally, the methods ``raw_search`` and ``raw_results`` of the 105 | client object will return the raw API response (this is the same as the ``data`` 106 | attribute of the ``ResultIter`` object). 107 | 108 | Behind the scenes the client uses ``requests``, and if you would like to 109 | explicitly provide a ``requests.Session``, you may do so:: 110 | 111 | from requests import Session 112 | api_key = 'secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 113 | session = Session() 114 | client = neverbounce_sdk.client(api_key=api_key, session=session) 115 | 116 | And all outgoing HTTP requests will be routed through the session object's 117 | ``request`` method, taking advantage of ``requests.Session``'s connection pooling. 118 | You may provide any custom object that provides a ``request`` interface with the 119 | same signature as that provided by ``requests.Session`` and a ``close`` method. 120 | 121 | Finally, the client may be used a context manager. If a session is provided, 122 | it will be used for all connections in the ``with`` block; if not, a session will 123 | be created. Either way, a session associated with a client is **always** 124 | closed at the end of the context block. :: 125 | 126 | with neverbounce_sdk.client() as client: 127 | client.api_key = 'secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 128 | 129 | # the client creates a session behind the scenes 130 | assert client.session is not None 131 | 132 | # do other stuff with the client 133 | 134 | # and then removes it at the end of the block 135 | assert client.session is None 136 | 137 | 138 | See Also 139 | -------- 140 | 141 | Documentation for each function of the client object is available through 142 | Python's built-in ``help`` function, e.g.:: 143 | 144 | >>> help(client.create) # brings up a ton of information about the create 145 | ... # function's arguments and options 146 | 147 | Many of the inputs and outputs of the client object's functions map fairly 148 | closely to NeverBounce's raw v4 API, reading through the `official API 149 | docs` will be 150 | valuable in conjunction with using the built-in online help. 151 | 152 | .. |travisci| image:: https://travis-ci.org/NeverBounce/NeverBounceApi-Python.svg?branch=master 153 | :target: https://travis-ci.org/NeverBounce/NeverBounceApi-Python 154 | :alt: Build Status 155 | 156 | .. |codeclimate| image:: https://codeclimate.com/github/NeverBounce/NeverBounceApi-Python/badges/gpa.svg 157 | :target: https://codeclimate.com/github/NeverBounce/NeverBounceApi-Python 158 | :alt: Code Climate 159 | -------------------------------------------------------------------------------- /examples/account_info.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Get account info 11 | info = client.account_info() 12 | print(info) 13 | -------------------------------------------------------------------------------- /examples/api_version.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key, api_version='v4.2') 9 | 10 | # Get account info 11 | info = client.account_info() 12 | print(info) 13 | -------------------------------------------------------------------------------- /examples/jobs_create.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Create array of data 11 | inputData = [ 12 | { 13 | 'id': '12345', 14 | 'email': 'support@neverbounce.com', 15 | 'name': 'Fred McValid' 16 | }, 17 | { 18 | 'id': '12346', 19 | 'email': 'invalid@neverbounce.com', 20 | 'name': 'Bob McInvalid' 21 | } 22 | ] 23 | 24 | # Create Job 25 | resp = client.jobs_create( 26 | input=inputData, 27 | filename="Created from Python Wrapper.csv", 28 | # auto_parse=True, 29 | # auto_start=True, 30 | # as_sample=False, 31 | # from_url=False, 32 | # historical_data=True 33 | ) 34 | print(resp['job_id']) 35 | -------------------------------------------------------------------------------- /examples/jobs_delete.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Delete job 11 | resp = client.jobs_delete(job_id=289022) 12 | print(resp) 13 | -------------------------------------------------------------------------------- /examples/jobs_download.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | f = open('results.csv', mode='wb') 11 | 12 | # Jobs download 13 | resp = client.jobs_download(job_id=289022, fd=f) 14 | f.close() 15 | -------------------------------------------------------------------------------- /examples/jobs_parse.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Parse job 11 | resp = client.jobs_parse(job_id=289022) 12 | print(resp) 13 | -------------------------------------------------------------------------------- /examples/jobs_results.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Get job's results 11 | jobs = client.jobs_results( 12 | job_id=289022 13 | # page=1, # Page to start from 14 | # items_per_page=10, # Number of items per page 15 | ) 16 | for job in jobs: 17 | print(job) 18 | -------------------------------------------------------------------------------- /examples/jobs_search.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Get jobs 11 | jobs = client.jobs_search( 12 | # job_id=10000, # Filter jobs based on id 13 | # filename='Book1.csv', # Filter jobs based on filename 14 | # job_status='complete', # Show completed jobs only 15 | # page=1, # Page to start from 16 | # items_per_page=10, # Number of items per page 17 | ) 18 | for job in jobs: 19 | print(job) 20 | -------------------------------------------------------------------------------- /examples/jobs_start.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Start job 11 | resp = client.jobs_start(job_id=289022) 12 | print(resp) 13 | -------------------------------------------------------------------------------- /examples/jobs_status.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Get job status 11 | resp = client.jobs_status(job_id=289022) 12 | print(resp) 13 | -------------------------------------------------------------------------------- /examples/poe_confirm.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key) 9 | 10 | # Confirm frontend widget 11 | resp = client.poe_confirm( 12 | email='support@neverbounce.com', 13 | transaction_id='NBPOE-TXN-5942940c09669', 14 | confirmation_token='e3173fdbbdce6bad26522dae792911f2', 15 | result='valid' 16 | ) 17 | print(resp['token_confirmed']) 18 | -------------------------------------------------------------------------------- /examples/single_check.py: -------------------------------------------------------------------------------- 1 | import neverbounce_sdk 2 | 3 | # Load api key from .env in project root 4 | with open(".env", "r") as dotenv: 5 | api_key = dotenv.read().replace('\n', '') 6 | 7 | # Create sdk client 8 | client = neverbounce_sdk.client(api_key=api_key, api_version='v4.2') 9 | 10 | # Verify email 11 | verification = client.single_check( 12 | email='support@neverbounce.com', 13 | address_info=True, 14 | credits_info=True, 15 | historical_data=False, 16 | timeout=10 # Timeout in seconds 17 | ) 18 | print(verification) 19 | -------------------------------------------------------------------------------- /neverbounce_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "NeverBounce Team" 3 | __email__ = 'support@neverbounce.com' 4 | __version__ = '4.3.0' 5 | 6 | from .auth import * # noqa: F403 7 | from .exceptions import * # noqa: F403 8 | from .utils import * # noqa: F403 9 | 10 | from .account import AccountMixin 11 | from .bulk import JobRunnerMixin 12 | from .core import APICore 13 | from .poe import POEMixin 14 | from .single import SingleMixin 15 | 16 | __all__ = (auth.__all__ + # noqa: F405 17 | exceptions.__all__ + # noqa: F405 18 | utils.__all__ + # noqa: F405 19 | ['NeverBounceAPIClient', 'client']) 20 | 21 | 22 | class NeverBounceAPIClient(AccountMixin, 23 | SingleMixin, 24 | JobRunnerMixin, 25 | POEMixin, 26 | APICore): 27 | pass 28 | 29 | 30 | def client(*args, **kwargs): 31 | """ Factory function (alias) for NeverBounceAPIClient objects """ 32 | return NeverBounceAPIClient(*args, **kwargs) 33 | -------------------------------------------------------------------------------- /neverbounce_sdk/account.py: -------------------------------------------------------------------------------- 1 | """ 2 | API support for endpoints located at API_ROOT/account 3 | """ 4 | from .utils import urlforversion 5 | 6 | 7 | class AccountMixin(object): 8 | __doc__ = __doc__ 9 | 10 | def account_info(self): 11 | """Provides information about the authenticated caller's account. 12 | 13 | Returns: 14 | A ``dict`` 15 | 16 | See also: 17 | https://developers.neverbounce.com/v4.0/reference#account-info 18 | """ 19 | endpoint = urlforversion(self.api_version, 'account', 'info') 20 | resp = self._make_request('GET', endpoint) 21 | self._check_response(resp) 22 | return resp.json() 23 | -------------------------------------------------------------------------------- /neverbounce_sdk/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | NeverBounce API Authentication 3 | """ 4 | from requests.auth import AuthBase 5 | 6 | __all__ = ['StaticTokenAuth'] 7 | 8 | 9 | class StaticTokenAuth(AuthBase): 10 | """Uses a static token to authenticate with NeverBounce's API v4 11 | 12 | Args: 13 | api_key (str): A static API token for authentication 14 | """ 15 | 16 | def __init__(self, api_key): 17 | self.api_key = api_key 18 | self.api_key_d = dict(key=api_key) 19 | 20 | def __call__(self, request): 21 | # Apparently this will work no matter what HTTP method is used, so... 22 | # this is simplest, just do this. NOTE: at this point in time, if the 23 | # request is a POST/PATCH/PUT, the body is already encoded and there is 24 | # no non-ugly way to just attach the token to it 25 | request.prepare_url(request.url, self.api_key_d) 26 | return request 27 | -------------------------------------------------------------------------------- /neverbounce_sdk/bulk.py: -------------------------------------------------------------------------------- 1 | from .utils import urlforversion 2 | 3 | __all__ = ['JobRunnerMixin'] 4 | 5 | _segmentation_options = { 6 | 'valids', 7 | 'invalids', 8 | 'catchalls', 9 | 'unknowns', 10 | 'disposables', 11 | 'include_duplicates', 12 | 'only_duplicates', 13 | 'only_bad_syntax' 14 | } 15 | 16 | _appends_options = { 17 | 'bad_syntax', 18 | 'free_email_host', 19 | 'role_account', 20 | 'addr', 21 | 'alias', 22 | 'host', 23 | 'subdomain', 24 | 'domain', 25 | 'tld', 26 | 'fqdn', 27 | 'network', 28 | 'has_dns_info', 29 | 'has_mail_server', 30 | 'mail_server_reachable', 31 | 'email_status_int', 32 | 'email_status' 33 | } 34 | 35 | _job_status = { 36 | 'under_review', 37 | 'queued', 38 | 'failed', 39 | 'complete', 40 | 'running', 41 | 'parsing', 42 | 'waiting', 43 | 'parsing', 44 | 'uploading' 45 | } 46 | 47 | 48 | class ResultIter(object): 49 | """Utility class for iterating through a paginated API. """ 50 | page_end = object() 51 | 52 | def __init__(self, method, *args, **kwargs): 53 | self._method = method 54 | self.data = method(*args, **kwargs) 55 | self._update() 56 | 57 | def _update(self): 58 | self._results = iter(self.data['results']) 59 | self.page = int(self.data['query']['page']) 60 | self.total_pages = int(self.data['total_pages']) 61 | self.total_results = int(self.data['total_results']) 62 | 63 | def get_next_page(self): 64 | query = {} 65 | for key, val in self.data['query'].items(): 66 | if key in _job_status or key in ('page', 'items_per_page'): 67 | query[key] = int(val) 68 | else: 69 | query[key] = val 70 | 71 | query['page'] += 1 72 | self.data = self._method(**query) 73 | self._update() 74 | 75 | def __next__(self): 76 | # traverse pages 77 | rval = next(self._results, self.page_end) 78 | if rval is self.page_end: 79 | self.get_next_page() 80 | # if this raises StopIteration, then we're done 81 | return next(self._results) 82 | return rval 83 | 84 | # Python 2 COMPAT 85 | next = __next__ 86 | 87 | def __iter__(self): 88 | return self 89 | 90 | 91 | class JobRunnerMixin(object): 92 | """ 93 | Mixin class that exposes methods of interacting with the NeverBounce 94 | bulk API endpoints. 95 | """ 96 | 97 | def raw_search(self, 98 | job_id=None, filename=None, job_status=None, 99 | page=1, items_per_page=10, **extra_query): 100 | """Direct interface to the jobs/search endpoint. See the documentation 101 | for :py:class:``search`` for more.""" 102 | data = dict(page=page, items_per_page=items_per_page) 103 | 104 | if job_id: 105 | data['job_id'] = job_id 106 | 107 | if filename: 108 | data['filename'] = filename 109 | 110 | if job_status: 111 | if job_status not in _job_status: 112 | msg = ('unknown argument {} for `job_status` in `search`; ' 113 | 'must be one of {}'.format(job_status, _job_status)) 114 | raise ValueError(msg) 115 | data['job_status'] = job_status 116 | 117 | data.update(extra_query) 118 | 119 | endpoint = urlforversion(self.api_version, 'jobs', 'search') 120 | resp = self._make_request('GET', endpoint, params=data) 121 | self._check_response(resp) 122 | return resp.json() 123 | 124 | def raw_results(self, job_id, page=1, items_per_page=10, **extra_query): 125 | """Direct interface to the jobs/results endpoint. See the documentation 126 | fro ``results`` for more.""" 127 | data = dict(job_id=job_id, 128 | page=page, 129 | items_per_page=items_per_page) 130 | 131 | data.update(extra_query) 132 | 133 | endpoint = urlforversion(self.api_version, 'jobs', 'results') 134 | resp = self._make_request('GET', endpoint, params=data) 135 | self._check_response(resp) 136 | return resp.json() 137 | 138 | def jobs_search(self, **kwargs): 139 | """ 140 | This function wraps the ``raw_search`` method in a custom results 141 | iterator. Iteration is over the items of the ``results`` object 142 | returned by the API. The iterator object will automatically fetch 143 | consecutive pages from the API; the page to start with and page size 144 | may be controlled by the ``page`` and ``items_per_page`` keyword-only 145 | arguments, which are passed directly to ``raw_search``. 146 | 147 | Keyword Arguments: 148 | job_id (int): 149 | If given, match search results against this job id. Default is 150 | None. 151 | 152 | filename (str): 153 | If given, return all results with exactly this filename. 154 | Default is None. 155 | 156 | job_status (str): 157 | If given, filter the results to only include the category of 158 | job given by ``show_only``. Allowable categories are: 159 | 160 | under_review 161 | queued 162 | failed 163 | complete 164 | running 165 | parsing 166 | waiting 167 | parsing 168 | uploading 169 | 170 | 171 | Default is ``None`` (perform no category filtering). 172 | 173 | page (int): 174 | The search results page to start from. 175 | 176 | items_per_page (int): 177 | How many items to include per page of results. 178 | 179 | Returns: 180 | An instance of ``_result_iter`` 181 | 182 | See Also: 183 | https://developers.neverbounce.com/v4.0/reference#jobs-search 184 | """ 185 | return ResultIter(self.raw_search, **kwargs) 186 | 187 | def jobs_results(self, job_id, **kwargs): 188 | """ 189 | This function wraps the ``raw_results`` method in a custom results 190 | iterator. Iteration is over the items of the ``results`` object 191 | returned by the API. The iterator object will automatically fetch 192 | consecutive pages from the API; the page to start with and page size 193 | may be controlled by the ``page`` and ``items_per_page`` keyword-only 194 | arguments, which are passed directly to ``raw_results``. 195 | 196 | Arguments: 197 | job_id (int): 198 | The numeric id of the job to get results for. 199 | 200 | Returns: 201 | An instance of ``_result_iter`` 202 | 203 | See Also: 204 | https://developers.neverbounce.com/v4.0/reference#jobs-results 205 | """ 206 | return ResultIter(self.raw_results, job_id, **kwargs) 207 | 208 | def jobs_create(self, input, from_url=False, filename=None, 209 | auto_parse=False, auto_start=False, as_sample=False, 210 | historical_data=True, allow_manual_review=None, 211 | callback_url=None, callback_headers=None): 212 | """ 213 | Creates a bulk job. 214 | 215 | Arguments: 216 | input (str or list): 217 | The input may be either a string URL to a remote location from 218 | which to read input objects, or a list of mappings denoting 219 | emails. Each mapping should contain an ``email`` key and may 220 | contain arbitrary other keys as metadata. 221 | 222 | from_url (bool): 223 | If ``True``, consider ``input`` a remote URL. Default is 224 | ``False``. 225 | 226 | filename (str): 227 | If given, will be associated with the job as metadata. Default 228 | is ``None``. 229 | 230 | auto_parse (bool): 231 | If ``True``, begin parsing the job immediately upon receipt. 232 | Default is ``False``. 233 | 234 | auto_start (bool): 235 | If ``True``, begin processing the job immediately upon parsing. 236 | Default is ``False``. 237 | 238 | as_sample (bool): 239 | If ``True``, run only a sample of the given input and return an 240 | estimation of the job's total bounce rate. 241 | 242 | historical_data (bool): If ``True``, return historical data. 243 | Default is ``True``. 244 | 245 | allow_manual_review (bool): 246 | If ``True``, allows job to fall to manual review. 247 | Default is ``None``. 248 | 249 | callback_url (str): 250 | If given, callbacks will be send to the specified URL. Default 251 | is ``None``. 252 | 253 | callback_headers (dict): 254 | If given, callbacks with headers will be send to the URL 255 | specified in "callback_url". 256 | Default is ``None``. 257 | 258 | Returns: 259 | A ``dict`` with keys ``status``, ``job_id``, and 260 | ``execution_time``. 261 | 262 | See Also: 263 | https://developers.neverbounce.com/v4.0/reference#jobs-create 264 | """ 265 | endpoint = urlforversion(self.api_version, 'jobs', 'create') 266 | 267 | data = dict(input=input, 268 | auto_parse=int(auto_parse), 269 | auto_start=int(auto_start), 270 | run_sample=int(as_sample)) 271 | 272 | data['request_meta_data'] = { 273 | 'leverage_historical_data': int(historical_data) 274 | } 275 | 276 | data['input_location'] = 'remote_url' if from_url else 'supplied' 277 | if filename is not None: 278 | data['filename'] = filename 279 | 280 | if allow_manual_review is not None: 281 | data['allow_manual_review'] = int(allow_manual_review) 282 | 283 | if callback_url is not None: 284 | data['callback_url'] = callback_url 285 | 286 | if callback_headers is not None: 287 | data['callback_headers'] = callback_headers 288 | 289 | resp = self._make_request('POST', endpoint, json=data) 290 | self._check_response(resp) 291 | 292 | return resp.json() 293 | 294 | def jobs_parse(self, job_id, auto_start=False): 295 | """ 296 | This endpoint allows you to parse a job created with auto_parse 297 | disabled. You cannot reparse a list once it's been parsed. 298 | 299 | Arguments: 300 | job_id (int): 301 | the job's numeric ID 302 | 303 | auto_start (bool): 304 | Whether or not to begin immediately processing the job 305 | following parsing. 306 | 307 | Returns: 308 | A ``dict`` with keys ``status``, ``queue_id``, and 309 | ``execution_time``. 310 | 311 | See Also: 312 | https://developers.neverbounce.com/v4.0/reference#jobs-parse 313 | """ 314 | endpoint = urlforversion(self.api_version, 'jobs', 'parse') 315 | data = dict(job_id=job_id, auto_start=int(auto_start)) 316 | resp = self._make_request('POST', endpoint, json=data) 317 | self._check_response(resp) 318 | return resp.json() 319 | 320 | def jobs_start(self, job_id, run_sample=False, allow_manual_review=None): 321 | """ 322 | This endpoint allows you to start a job created or parsed with 323 | auto_start disabled. Once the list has been started the credits will be 324 | deducted and the process cannot be stopped or restarted. 325 | 326 | Arguments: 327 | job_id (int): 328 | the job's numeric ID 329 | 330 | run_sample (bool): 331 | Whether or not to run a sample of this job's contents and 332 | return an estimate of the job's bounce rate 333 | 334 | allow_manual_review (bool): 335 | If ``True``, allows job to fall to manual review. 336 | Default is ``None``. 337 | 338 | Returns: 339 | A ``dict`` with keys ``status``, ``queue_id``, and 340 | ``execution_time``. 341 | 342 | See Also: 343 | https://developers.neverbounce.com/v4.0/reference#jobs-start 344 | """ 345 | endpoint = urlforversion(self.api_version, 'jobs', 'start') 346 | data = dict(job_id=job_id, run_sample=int(run_sample)) 347 | 348 | if allow_manual_review is not None: 349 | data['allow_manual_review'] = int(allow_manual_review) 350 | 351 | resp = self._make_request('POST', endpoint, json=data) 352 | self._check_response(resp) 353 | return resp.json() 354 | 355 | def jobs_status(self, job_id): 356 | """ 357 | Returns a status object (a dict) with a number of keys relevant to the 358 | status of the job given by ``job_id``. 359 | 360 | Arguments: 361 | job_id (int): the job's numeric id 362 | 363 | Returns: 364 | A ``dict`` with keys indicating various aspects of the job's 365 | progression. 366 | 367 | See also: 368 | https://developers.neverbounce.com/v4.0/reference#jobs-status 369 | """ 370 | endpoint = urlforversion(self.api_version, 'jobs', 'status') 371 | resp = self._make_request('GET', endpoint, params=dict(job_id=job_id)) 372 | self._check_response(resp) 373 | return resp.json() 374 | 375 | def jobs_download(self, job_id, fd, 376 | segmentation=('valids', 'invalids', 377 | 'catchalls', 'unknowns'), 378 | appends=(), 379 | yes_no_representation='int', 380 | line_feed_type='unix'): 381 | r""" 382 | Download the full results of job ``job_id`` as a CSV file into the 383 | file-like object given by ``fd``. 384 | 385 | Arguments: 386 | 387 | job_id (int): 388 | the integer ID of the job to download 389 | 390 | fd (file): 391 | an open file or file-like object to write the download to 392 | 393 | segmentation (list[str]): 394 | A list of string values declaring what subset of the full 395 | results to download. Possible values are: 396 | 397 | * valids 398 | * invalids 399 | * catchalls 400 | * unknowns 401 | * disposables 402 | * include_duplicates 403 | * only_duplicates 404 | * only_bad_syntax 405 | 406 | appends (list[str]): 407 | A list of fields to append to the downloaded CSV. Valid options 408 | are: 409 | 410 | * bad_syntax 411 | * free_email_host 412 | * role_account 413 | * addr 414 | * alias 415 | * host 416 | * subdomain 417 | * domain 418 | * tld 419 | * fqdn 420 | * network 421 | * has_dns_info 422 | * has_mail_server 423 | * mail_server_reachable 424 | * email_status_int 425 | * email_status 426 | 427 | yes_no_representation (str): 428 | Sets the characters used to represent Boolean values in the 429 | generated CSV file. The following table gives each option's 430 | alias, the "raw" form actually sent in the request, and the 431 | meaning: 432 | 433 | .. table:: Boolean Value Representations 434 | :widths: auto 435 | 436 | =========== =============== ========== 437 | Alias Token Meaning 438 | =========== =============== ========== 439 | int BIN_1_0 1/0 440 | upper BIN_Y_N Y/N 441 | lower BIN_y_n y/n 442 | lowercase BIN_yes_no yes/no 443 | capitalcase BIN_Yes_No Yes/No 444 | bool BIN_true_false true/false 445 | =========== =============== ========== 446 | 447 | You may use either the alias or the token as the setting, but 448 | only one setting may be given. 449 | 450 | line_feed_type (str): 451 | Sets the characters to use as line-ending in the generated CSV 452 | file. As with ``yes_no_representation``, you may use a 453 | convenience alias or the API's token, given in the following 454 | table: 455 | 456 | .. table:: Line Feed Characters 457 | :widths: auto 458 | 459 | =========== =============== ========== 460 | Alias Token Meaning 461 | =========== =============== ========== 462 | unix LINEFEED_0A \n 463 | windows LINEFEED_0D0A \r\n 464 | appleII LINEFEED_0D \r 465 | spooled LINEFEED_0A \n\r 466 | =========== =============== ========== 467 | 468 | Returns: 469 | ``None`` 470 | 471 | See Also: 472 | https://developers.neverbounce.com/v4.0/reference#jobs-download 473 | """ 474 | data = dict(job_id=job_id) 475 | 476 | def add_opts(opts, allowable): 477 | # does a small bit of bookeeping to make sure we're returning 478 | # useful errors if an option is given with a typo 479 | for opt in opts: 480 | if opt not in allowable: 481 | msg = ('{} is not a recognized option for the download' 482 | 'endpoint'.format(opt)) 483 | raise ValueError(msg) 484 | data[opt] = 1 485 | 486 | add_opts(segmentation, _segmentation_options) 487 | add_opts(appends, _appends_options) 488 | 489 | def set_setting(arg, argname, map_): 490 | # confer note on add_opts; these are just conveniences 491 | if arg in map_.values(): 492 | data[argname] = arg 493 | else: 494 | try: 495 | data[argname] = map_[arg] 496 | except KeyError: 497 | msg = '{} is not a recognizable value for {}'.format( 498 | arg, argname 499 | ) 500 | raise ValueError(msg) 501 | 502 | set_setting(yes_no_representation, 'binary_operators_type', { 503 | 'int': 'BIN_1_0', 504 | 'upper': 'BIN_Y_N', 505 | 'lower': 'BIN_y_n', 506 | 'lowercase': 'BIN_yes_no', 507 | 'capitalcase': 'BIN_Yes_No', 508 | 'bool': 'BIN_true_false' 509 | }) 510 | 511 | set_setting(line_feed_type, 'line_feed_type', { 512 | 'unix': 'LINEFEED_0A', # \n 513 | 'windows': 'LINEFEED_0D0A', # \r\n 514 | 'appleII': 'LINEFEED_0D', # \r 515 | 'spooled': 'LINEFEED_0A' # \n\r 516 | }) 517 | 518 | endpoint = urlforversion(self.api_version, 'jobs', 'download') 519 | # the return val is (possibly) streaming; remember to set stream 520 | resp = self._make_request('POST', endpoint, json=data, stream=True) 521 | 522 | # returns json if there's an error, octet-stream if all is good 523 | if resp.headers['Content-Type'] == 'application/json': 524 | self._check_response(resp) 525 | 526 | # write the streaming csv file to fd 527 | for chunk in resp.iter_content(chunk_size=128): 528 | fd.write(chunk) 529 | 530 | def jobs_delete(self, job_id): 531 | """ 532 | Permanently delete the job with id ``job_id`` 533 | 534 | Arguments: 535 | job_id (int): The job's numeric ID. 536 | 537 | Returns: 538 | A ``dict`` with keys ``status`` and ``execution_time`` 539 | 540 | See Also: 541 | https://developers.neverbounce.com/v4.0/reference#jobs-delete 542 | """ 543 | endpoint = urlforversion(self.api_version, 'jobs', 'delete') 544 | resp = self._make_request('GET', endpoint, params=dict(job_id=job_id)) 545 | self._check_response(resp) 546 | return resp.json() 547 | -------------------------------------------------------------------------------- /neverbounce_sdk/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core API support methods 3 | """ 4 | import requests 5 | 6 | from . import __version__ as VERSION, API_VERSION 7 | from .auth import StaticTokenAuth 8 | from .exceptions import _status_to_exception, GeneralException 9 | 10 | 11 | class APICore(object): 12 | """ 13 | Core helpers for authenticating and interacting with the Neverbounce API 14 | """ 15 | 16 | def __init__(self, 17 | api_key=None, 18 | session=None, 19 | timeout=30, 20 | api_version=API_VERSION): 21 | self.api_version = api_version 22 | self.api_key = api_key 23 | self.session = session 24 | self.timeout = timeout 25 | 26 | @property 27 | def api_key(self): 28 | """ 29 | If self.session is set and self.auth is not, falls back to 30 | ``self.session.auth``. 31 | """ 32 | if self.session and not self._api_key: 33 | return self.session.api_key 34 | return self._api_key 35 | 36 | @api_key.setter 37 | def api_key(self, val): 38 | if val is None: 39 | self._api_key = None 40 | elif isinstance(val, StaticTokenAuth): 41 | self._api_key = val 42 | else: 43 | self._api_key = StaticTokenAuth(val) 44 | 45 | @api_key.deleter 46 | def api_key(self): 47 | self._api_key = None 48 | 49 | @property 50 | def timeout(self): 51 | if self.session and not self._timeout: 52 | return self.session.timeout 53 | return self._timeout 54 | 55 | @timeout.setter 56 | def timeout(self, val): 57 | if val is None: 58 | self._timeout = None 59 | else: 60 | self._timeout = val 61 | 62 | @timeout.deleter 63 | def timeout(self): 64 | self._timeout = None 65 | 66 | def _make_request(self, method, url, *args, **kwargs): 67 | """ 68 | Looks for an underlying Session and uses that if available, else 69 | defaults to the main requests interface 70 | """ 71 | # no try/except; any errors that occur down here need to propogate 72 | # prefer an ``auth`` from invoker, and remember self.auth falls back to 73 | # self.session.auth if self._auth is not set and self.session is 74 | headers = kwargs.pop('headers', {}) 75 | user_agent = 'NeverBounceAPI-Python/{}'.format(VERSION) 76 | headers.update({'User-Agent': user_agent}) 77 | kwargs['headers'] = headers 78 | 79 | if self.timeout: 80 | kwargs['timeout'] = self.timeout 81 | 82 | if not kwargs.get('auth'): 83 | kwargs.update({'auth': self.api_key}) 84 | if self.session: 85 | return self.session.request(method, url, *args, **kwargs) 86 | return requests.request(method, url, *args, **kwargs) 87 | 88 | def _check_response(self, resp): 89 | """Checks a response for errors and throws if they any present""" 90 | # first check that there were no errors on the wire 91 | resp.raise_for_status() 92 | 93 | try: 94 | data = resp.json() 95 | except Exception: 96 | raise GeneralException('The response from NeverBounce was ' + 97 | 'unable to be parsed as json. Try the ' + 98 | 'request again, if this error persists ' + 99 | 'let us know at support@neverbounce.com.' + 100 | '\n\n(Internal error)') 101 | 102 | # now make sure we have a sensible response 103 | try: 104 | api_status = data['status'] 105 | if api_status != 'success': 106 | api_message = data['message'] 107 | except KeyError: 108 | # no status field in the API's return object 109 | # complain about it cause that's weird 110 | raise GeneralException('The response from server is incomplete. ' + 111 | 'Either a status code was not included ' + 112 | 'or the an error was returned without an ' + 113 | 'error message. Try the request again, ' + 114 | 'if this error persists let us know at ' + 115 | 'support@neverbounce.com.' + 116 | '\n\n(Internal error [status ' + 117 | str(resp.status_code) + ': ' + 118 | resp.text + '])') 119 | 120 | # if everything is good, we're done 121 | if api_status == 'success': 122 | return resp 123 | # if not, construct and raise the appropriate exception 124 | try: 125 | exc = _status_to_exception[api_status] 126 | except KeyError: 127 | # this is an uknown failure from upstream; letting the KeyError 128 | # propgate would be weird to the user: use a generic error 129 | exc = GeneralException 130 | 131 | message = data.get('message') 132 | execution_time = data.get('execution_time') 133 | 134 | # if the problem is with authentication, rewrite the error message to 135 | # make more sense in the current context 136 | if api_status == 'auth_failure': 137 | message = ('We were unable to authenticate your request. ' + 138 | 'Make sure NeverBounceAPIClient.api_key is set. ' + 139 | 'The following information was supplied: ' + 140 | '{0}\n\n(auth_failure)'.format(api_message)) 141 | else: 142 | message = ('We were unable to complete your request. ' + 143 | 'The following information was supplied: ' + 144 | '{0}\n\n({1})'.format(api_message, api_status)) 145 | 146 | raise exc(message, execution_time) 147 | 148 | def __enter__(self): 149 | """ 150 | When entering a context, if no session is set, use 151 | requests.session() as default. Return self as the context manager 152 | """ 153 | if self.session is None: 154 | self.session = requests.session() 155 | return self 156 | 157 | def __exit__(self, *args): 158 | """When exiting a context, close and clear the session""" 159 | self.session.close() 160 | self.session = None 161 | -------------------------------------------------------------------------------- /neverbounce_sdk/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | NeverBounce API exceptions and warnings 3 | """ 4 | 5 | __all__ = ['GeneralException', 6 | 'AuthFailure', 7 | 'ThrottleTriggered', 8 | 'BadReferrer'] 9 | 10 | 11 | class GeneralException(Exception): 12 | """ 13 | A non recoverable API error occurred check the message for details 14 | """ 15 | 16 | def __init__(self, message, execution_time=None): 17 | self.execution_time = execution_time 18 | self.message = message 19 | super(GeneralException, self).__init__(message, execution_time) 20 | 21 | 22 | class AuthFailure(GeneralException): 23 | """ 24 | The API credentials used are bad, have you reset them recently? 25 | """ 26 | 27 | 28 | class ThrottleTriggered(GeneralException): 29 | """ 30 | Too many requests in a short amount of time, try again shortly or adjust 31 | your rate limit settings for this application in the dashboard 32 | """ 33 | 34 | 35 | class BadReferrer(GeneralException): 36 | """ 37 | The script is being used from an unauthorized source, you may need to 38 | adjust your app's settings to allow it to be used from here 39 | """ 40 | 41 | 42 | _status_to_exception = { 43 | 'general_failure': GeneralException, 44 | 'auth_failure': AuthFailure, 45 | 'throttle_triggered': ThrottleTriggered, 46 | 'bad_referrer': BadReferrer 47 | } 48 | -------------------------------------------------------------------------------- /neverbounce_sdk/poe.py: -------------------------------------------------------------------------------- 1 | """ 2 | API support for endpoints located at API_ROOT/poe 3 | """ 4 | from .utils import urlforversion 5 | 6 | 7 | class POEMixin(object): 8 | __doc__ = __doc__ 9 | 10 | def poe_confirm(self, email, transaction_id, confirmation_token, result): 11 | """Allows confirmation of client side verification (javscript widget) 12 | 13 | Arguments: 14 | email (str): the email address that was verified 15 | transaction_id: the transaction_id provided by the javascript 16 | widget 17 | confirmation_token: the confirmation_token provided by the 18 | javascript widget 19 | result: the verification result provided by the javascript widget 20 | 21 | Returns: 22 | A ``dict`` 23 | 24 | See Also: 25 | https://developers.neverbounce.com/v4.0/reference#widget-poe-confirm 26 | """ 27 | endpoint = urlforversion(self.api_version, 'poe', 'confirm') 28 | params = dict(email=email, 29 | transaction_id=transaction_id, 30 | confirmation_token=confirmation_token, 31 | result=result) 32 | resp = self._make_request('GET', endpoint, params=params) 33 | self._check_response(resp) 34 | return resp.json() 35 | -------------------------------------------------------------------------------- /neverbounce_sdk/single.py: -------------------------------------------------------------------------------- 1 | """ 2 | API support for endpoints located at API_ROOT/single 3 | """ 4 | from .utils import urlforversion 5 | 6 | 7 | class SingleMixin(object): 8 | __doc__ = __doc__ 9 | 10 | def single_check(self, email, 11 | address_info=False, 12 | credits_info=False, 13 | historical_data=True, 14 | timeout=30): 15 | """Provides verification for a single email. 16 | 17 | Arguments: 18 | email (str): the email address to verify 19 | address_info (bool): If ``True``, return extra information about 20 | the address. Default is ``False``. 21 | credits_info (bool): If ``True``, return extra information about 22 | the account and how many credits remain. Default is ``False``. 23 | historical_data (bool): If ``True``, return historical data. 24 | Default is ``True``. 25 | timeout (int): Set a timeout for the verification. Once this limit 26 | is reached the API will give up verifying the email and return 27 | it as an "Unknown". This is enforced by the API, NOT the local 28 | request library (for setting request timeouts use the 29 | ``timeout`` parameter on the client). Default is ``30``. 30 | 31 | Returns: 32 | A ``dict`` 33 | 34 | See Also: 35 | https://developers.neverbounce.com/v4.0/reference#single-check 36 | """ 37 | endpoint = urlforversion(self.api_version, 'single', 'check') 38 | params = dict(email=email, 39 | # convert boolean flags to 0 or 1 40 | address_info=int(address_info), 41 | credits_info=int(credits_info), 42 | timeout=timeout) 43 | historical_data_key = 'request_meta_data[leverage_historical_data]' 44 | params[historical_data_key] = int(historical_data) 45 | resp = self._make_request('GET', endpoint, params=params) 46 | self._check_response(resp) 47 | return resp.json() 48 | -------------------------------------------------------------------------------- /neverbounce_sdk/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions and constants used in the codebase 3 | """ 4 | 5 | __all__ = [ 6 | 'API_ROOT', 7 | 'API_VERSION', 8 | 'urlfor', 9 | 'urlforversion' 10 | ] 11 | 12 | API_ROOT = 'https://api.neverbounce.com' 13 | API_VERSION = 'v4.2' 14 | 15 | 16 | def urlfor(*parts): 17 | """Returns the API endpoint base url (i.e. does not handle URL params)""" 18 | return urlforversion(API_VERSION, *parts) 19 | 20 | 21 | def urlforversion(api_version, *parts): 22 | """Returns the API endpoint base url (i.e. does not handle URL params)""" 23 | endpoint = '/'.join(parts) 24 | return '{}/{}/{}'.format(API_ROOT, api_version, endpoint) 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.10 2 | Babel==2.4.0 3 | certifi==2017.4.17 4 | chardet==3.0.4 5 | cookies==2.2.1 6 | coverage==4.4.1 7 | decorator==4.1.1 8 | docutils==0.13.1 9 | flake8==3.3.0 10 | idna==2.5 11 | imagesize==0.7.1 12 | ipython==6.1.0 13 | ipython-genutils==0.2.0 14 | jedi==0.10.2 15 | Jinja2==2.9.6 16 | MarkupSafe==1.0 17 | mccabe==0.6.1 18 | pexpect==4.2.1 19 | pickleshare==0.7.4 20 | pkginfo==1.4.1 21 | pluggy==0.4.0 22 | prompt-toolkit==1.0.14 23 | ptyprocess==0.5.2 24 | py==1.4.34 25 | pycodestyle==2.3.1 26 | pyflakes==1.5.0 27 | Pygments==2.2.0 28 | pytest==3.1.3 29 | pytest-cov==2.5.1 30 | pytz==2017.2 31 | requests==2.20.0 32 | requests-toolbelt==0.8.0 33 | responses==0.5.1 34 | simplegeneric==0.8.1 35 | six==1.10.0 36 | snowballstemmer==1.2.1 37 | Sphinx==1.6.3 38 | sphinxcontrib-websupport==1.0.1 39 | tox==2.7.0 40 | tqdm==4.14.0 41 | traitlets==4.3.2 42 | twine==1.9.1 43 | urllib3==1.23 44 | virtualenv==15.1.0 45 | wcwidth==0.1.7 46 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | exclude = docs 6 | 7 | [aliases] 8 | test = pytest 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup, find_packages 4 | from codecs import open 5 | from os import path 6 | from re import search, M 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | 10 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | def read(*parts): 14 | # intentionally *not* adding an encoding option to open 15 | # see here: https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 16 | return open(path.join(here, *parts), 'r').read() 17 | 18 | def find_version(*file_paths): 19 | version_file = read(*file_paths) 20 | version_match = search(r"^__version__ = ['\"]([^'\"]*)['\"]", 21 | version_file, M) 22 | if version_match: 23 | return version_match.group(1) 24 | raise RuntimeError("Unable to find version string.") 25 | 26 | setup( 27 | name='neverbounce-sdk', 28 | version=find_version('neverbounce_sdk/__init__.py'), 29 | description="Official Python SDK for the NeverBounce API", 30 | long_description=long_description, 31 | author="NeverBounce Team", 32 | author_email='support@neverbounce.com', 33 | url='https://github.com/NeverBounce/NeverBounceApi-Python', 34 | packages=find_packages(include=['neverbounce_sdk']), 35 | include_package_data=True, 36 | install_requires=[ 37 | 'requests', 38 | ], 39 | license='MIT', 40 | zip_safe=False, 41 | keywords=['neverbounce', 'api', 'email', 'verification', 'cleaning'], 42 | classifiers=[ 43 | 'Development Status :: 5 - Production/Stable', 44 | 'Intended Audience :: Developers', 45 | 'Topic :: Communications :: Email', 46 | 'License :: OSI Approved :: MIT License', 47 | 'Natural Language :: English', 48 | "Programming Language :: Python :: 2", 49 | 'Programming Language :: Python :: 2.7', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: 3.3', 52 | 'Programming Language :: Python :: 3.4', 53 | 'Programming Language :: Python :: 3.5', 54 | 'Programming Language :: Python :: 3.6', 55 | 'Programming Language :: Python :: 3.7', 56 | ], 57 | test_suite='tests', 58 | tests_require=['pytest'], 59 | ) 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for neverbounce.""" 4 | -------------------------------------------------------------------------------- /tests/test_account_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the Accounts endpoints 3 | """ 4 | import responses 5 | 6 | import neverbounce_sdk 7 | from neverbounce_sdk import urlforversion 8 | 9 | 10 | @responses.activate 11 | def test_account_info(): 12 | client = neverbounce_sdk.client() 13 | client.api_key = 'static key' 14 | 15 | # this is the exepcted response 16 | responses.add(responses.GET, 17 | urlforversion('v4.2', 'account', 'info'), 18 | status=200, 19 | json={'status': 'success'}) 20 | 21 | info = client.account_info() 22 | assert info == {'status': 'success'} 23 | assert len(responses.calls) == 1 24 | assert (responses.calls[0].request.url == 25 | 'https://api.neverbounce.com/v4.2/account/info?key=static+key') 26 | -------------------------------------------------------------------------------- /tests/test_api_errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the client's API Error handling 3 | """ 4 | import pytest 5 | import responses 6 | 7 | import neverbounce_sdk 8 | from neverbounce_sdk import (urlforversion, 9 | GeneralException) 10 | from neverbounce_sdk.exceptions import _status_to_exception 11 | 12 | 13 | @pytest.mark.parametrize('err', list(_status_to_exception)) 14 | @responses.activate 15 | def test_account_info(err): 16 | # this is the exepcted response: we've picked a random simple API target 17 | # just so we can mock out some exceptions and see how the client handles 18 | # them 19 | responses.add(responses.GET, 20 | urlforversion('v4.2', 'account', 'info'), 21 | status=200, 22 | json={'status': err, 'message': 'the-message'}) 23 | 24 | with pytest.raises(_status_to_exception[err]) as exc: 25 | neverbounce_sdk.client().account_info() 26 | 27 | if err == 'auth_failure': 28 | assert 'We were unable to authenticate your request'\ 29 | in str(exc.value) 30 | else: 31 | assert 'We were unable to complete your request.' in str(exc.value) 32 | assert err in str(exc.value) 33 | 34 | 35 | @responses.activate 36 | def test_non_json_response(): 37 | responses.add(responses.GET, 38 | urlforversion('v4.2', 'account', 'info'), 39 | status=200, 40 | # empty dict; client should complain that there's no 'status' 41 | # key 42 | body='Not Json') 43 | 44 | with pytest.raises(GeneralException) as exc: 45 | neverbounce_sdk.client().account_info() 46 | assert ('The response from NeverBounce was unable ' 47 | + 'to be parsed as json. Try the request ' 48 | + 'again, if this error persists' 49 | + ' let us know at support@neverbounce.com.' 50 | + '\\n\\n(Internal error)') in str(exc.value) 51 | 52 | 53 | @responses.activate 54 | def test_weird_response_no_status_raises(): 55 | responses.add(responses.GET, 56 | urlforversion('v4.2', 'account', 'info'), 57 | status=200, 58 | # empty dict; client should complain that there's no 'status' 59 | # key 60 | json={'message': 'Something went wrong'}) 61 | 62 | with pytest.raises(GeneralException) as exc: 63 | neverbounce_sdk.client().account_info() 64 | assert ('The response from server is incomplete. ' 65 | + 'Either a status code was not included ' 66 | + 'or the an error was returned without an ' 67 | + 'error message. Try the request again, ' 68 | + 'if this error persists let us know at ' 69 | + 'support@neverbounce.com.' 70 | + '\\n\\n(Internal error [status 200: ' 71 | + '{"message": "Something went wrong"}])') in str(exc.value) 72 | -------------------------------------------------------------------------------- /tests/test_bulk_api.py: -------------------------------------------------------------------------------- 1 | """Test the API endpoints located at /jobs (except /jobs/download)""" 2 | from __future__ import unicode_literals 3 | import json 4 | 5 | import pytest 6 | import responses 7 | 8 | import neverbounce_sdk 9 | from neverbounce_sdk import urlforversion 10 | from neverbounce_sdk.bulk import _job_status 11 | 12 | 13 | @pytest.fixture 14 | def client(): 15 | api_key = 'secret key' 16 | return neverbounce_sdk.client(api_key=api_key) 17 | 18 | 19 | def test_search(client, monkeypatch): 20 | # Tests that the iteration wrapper works and that client.search correctly 21 | # calls client.raw_search 22 | expected_results = [{'data': val} for val in 'abc123'] 23 | 24 | def _search(**kwargs): 25 | kwargs.update(dict(job_id=None, filename=None, job_status=None, 26 | page=0, items_per_page=10)) 27 | return dict(results=expected_results, 28 | total_pages=1, 29 | total_results=1, 30 | query=kwargs) 31 | 32 | monkeypatch.setattr(client, 'raw_search', _search) 33 | results = client.jobs_search() 34 | for res, exp in zip(iter(results), expected_results): 35 | assert res == exp 36 | 37 | 38 | def test_results(client, monkeypatch): 39 | # Tests that the iteration wrapper works and that client.results correctly 40 | # calls client.raw_results 41 | expected_results = [{'data': val} for val in 'abc123'] 42 | 43 | def _results(job_id=0, **kwargs): 44 | kwargs.update(dict(filename=None, show_only=None, 45 | page=0, items_per_page=10)) 46 | return dict(results=expected_results, 47 | total_pages=1, 48 | total_results=1, 49 | query=kwargs) 50 | 51 | monkeypatch.setattr(client, 'raw_results', _results) 52 | results = client.jobs_results(0) 53 | for res, exp in zip(iter(results), expected_results): 54 | assert res == exp 55 | 56 | 57 | @responses.activate 58 | def test_raw_result_interface(client): 59 | responses.add(responses.GET, 60 | urlforversion('v4.2', 'jobs', 'results'), 61 | status=200, 62 | json={'status': 'success'}) 63 | 64 | client.raw_results(123) 65 | for arg in ('job_id=123', 'page=1', 'items_per_page=10'): 66 | assert arg in responses.calls[0].request.url 67 | 68 | 69 | @responses.activate 70 | def test_raw_search_interface(client): 71 | responses.add(responses.GET, 72 | urlforversion('v4.2', 'jobs', 'search'), 73 | status=200, 74 | json={'status': 'success'}) 75 | 76 | # defaults 77 | client.raw_search() 78 | request_url = responses.calls[0].request.url 79 | for arg in ('job_id', 'filename') + tuple(_job_status): 80 | assert arg not in request_url 81 | for arg in ('page=1', 'items_per_page=10'): 82 | assert arg in request_url 83 | 84 | client.raw_search(job_id=123, filename='test.csv', job_status='complete') 85 | request_url = responses.calls[1].request.url 86 | for arg in ('page=1', 'items_per_page=10', 'job_id=123', 87 | 'filename=test.csv', 'job_status=complete'): 88 | assert arg in request_url 89 | 90 | with pytest.raises(ValueError): 91 | client.raw_search(job_status='some unknown value OH NO') 92 | 93 | 94 | @responses.activate 95 | def test_create(client): 96 | responses.add(responses.POST, 97 | urlforversion('v4.2', 'jobs', 'create'), 98 | json={'status': 'success'}, 99 | status=200) 100 | 101 | raw_args = dict(input=['test@example.com'], 102 | input_location='supplied', 103 | auto_parse=0, auto_start=0, run_sample=0) 104 | 105 | client.jobs_create(['test@example.com']) 106 | called_with = json.loads(responses.calls[0].request.body.decode('UTF-8')) 107 | assert 'filename' not in called_with 108 | for k, v in raw_args.items(): 109 | assert called_with[k] == v 110 | 111 | new_raw_args = raw_args.copy() 112 | new_raw_args['filename'] = 'testfile.csv' 113 | client.jobs_create(['test@example.com'], filename='testfile.csv') 114 | called_with = json.loads(responses.calls[1].request.body.decode('UTF-8')) 115 | for k, v in raw_args.items(): 116 | assert called_with[k] == v 117 | 118 | 119 | @responses.activate 120 | def test_parse(client): 121 | responses.add(responses.POST, 122 | urlforversion('v4.2', 'jobs', 'parse'), 123 | json={'status': 'success'}, 124 | status=200) 125 | 126 | client.jobs_parse(123) 127 | called_with = json.loads(responses.calls[0].request.body.decode('UTF-8')) 128 | expected_args = dict(job_id=123, auto_start=0) 129 | for k, v in expected_args.items(): 130 | assert called_with[k] == v 131 | 132 | 133 | @responses.activate 134 | def test_start(client): 135 | responses.add(responses.POST, 136 | urlforversion('v4.2', 'jobs', 'start'), 137 | json={'status': 'success'}, 138 | status=200) 139 | 140 | client.jobs_start(123) 141 | called_with = json.loads(responses.calls[0].request.body.decode('UTF-8')) 142 | expected_args = dict(job_id=123, run_sample=0) 143 | for k, v in expected_args.items(): 144 | assert called_with[k] == v 145 | 146 | 147 | @responses.activate 148 | def test_status(client): 149 | responses.add(responses.GET, 150 | urlforversion('v4.2', 'jobs', 'status'), 151 | json={'status': 'success'}, 152 | status=200) 153 | 154 | client.jobs_status(123) 155 | assert 'job_id=123' in responses.calls[0].request.url 156 | 157 | 158 | @responses.activate 159 | def test_delete(client): 160 | responses.add(responses.GET, 161 | urlforversion('v4.2', 'jobs', 'delete'), 162 | json={'status': 'success'}, 163 | status=200) 164 | 165 | client.jobs_delete(123) 166 | assert 'job_id=123' in responses.calls[0].request.url 167 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the core functionality of the NeverBounce API SDK 3 | """ 4 | from requests import Session 5 | 6 | import neverbounce_sdk 7 | from neverbounce_sdk import NeverBounceAPIClient, StaticTokenAuth 8 | 9 | 10 | def test_client_function(): 11 | """neverbounce_sdk.client() should return a NeverBounceAPIClient()""" 12 | client = neverbounce_sdk.client() 13 | assert isinstance(client, NeverBounceAPIClient) 14 | 15 | 16 | def test_default_client_auth_is_None(): 17 | """A newly created client must be configured with auth""" 18 | client = neverbounce_sdk.client() 19 | assert client.api_key is None 20 | 21 | 22 | def test_setting_auth_with_string(): 23 | """You may pass a string api_key directly to the NeverBounceAPIClient.api_key 24 | property""" 25 | client = neverbounce_sdk.client() 26 | # sets the StaticTokenAuth and auth.api_key 27 | client.api_key = 'this is a string!' 28 | assert client.api_key is not None 29 | assert isinstance(client.api_key, StaticTokenAuth) 30 | assert client.api_key.api_key == 'this is a string!' 31 | 32 | 33 | def test_setting_auth_with_StaticTokenAuth(): 34 | """You may instantiate and pass a StaticTokenAuth object directly to the 35 | NeverBounceAPIClient.api_key property""" 36 | client = neverbounce_sdk.client() 37 | client.api_key = StaticTokenAuth('secret token') 38 | assert client.api_key is not None 39 | assert isinstance(client.api_key, StaticTokenAuth) 40 | assert client.api_key.api_key == 'secret token' 41 | 42 | 43 | def test_clearing_client_auth(): 44 | """You may clear the NeverBounceAPIClient.api_key propery by setting it to 45 | None or the ``del`` statement; they are equivalent""" 46 | client = neverbounce_sdk.client() 47 | # by directly setting the property to None 48 | client.api_key = 'something' 49 | assert client.api_key is not None 50 | client.api_key = None 51 | assert client.api_key is None 52 | # by "deleting" the property 53 | client.api_key = 'something' 54 | assert client.api_key is not None 55 | del client.api_key 56 | assert client.api_key is None 57 | 58 | 59 | def test_default_client_session_is_none(): 60 | """A newly created client has no default session""" 61 | client = neverbounce_sdk.client() 62 | assert client.session is None 63 | 64 | 65 | def test_client_as_context_manager_has_default_requests_session(): 66 | """When used as context manager, default client uses requests.Session""" 67 | with neverbounce_sdk.client() as client: 68 | assert client.session is not None 69 | assert isinstance(client.session, Session) 70 | 71 | 72 | def test_setting_custom_session_recognized_in_context_manager(): 73 | """When used as a context manager, a client should not override a custom 74 | session with the default requests one""" 75 | custom_session = Session() 76 | with neverbounce_sdk.client(session=custom_session) as client: 77 | assert client.session is custom_session 78 | # session is cleared upon exiting a context 79 | assert client.session is None 80 | 81 | 82 | def test_client_auth_fallback_with_custom_session_present(): 83 | """The NeverBounceAPIClient will fall back to using a session auth if a 84 | session is present but no auth""" 85 | custom_session = Session() 86 | custom_session.api_key = StaticTokenAuth('secret token!') 87 | client = neverbounce_sdk.client(session=custom_session) 88 | # is: should be the same object 89 | assert client.session is custom_session 90 | assert client.api_key is custom_session.api_key 91 | 92 | custom_auth = StaticTokenAuth('different token!') 93 | client = neverbounce_sdk.client(session=custom_session, 94 | api_key=custom_auth) 95 | assert client.session is custom_session 96 | assert client.api_key is custom_auth 97 | 98 | 99 | def test_default_client_timeout_is_default(): 100 | """A newly created client must be configured with default timeout""" 101 | client = neverbounce_sdk.client() 102 | assert client.timeout == 30 103 | 104 | 105 | def test_setting_timeout_with_None(): 106 | """You may set timeout to None in the NeverBounceAPIClient.timeout 107 | property""" 108 | client = neverbounce_sdk.client() 109 | client.timeout = None 110 | assert client.timeout is None 111 | 112 | 113 | def test_setting_timeout_with_integer(): 114 | """You may set timeout to an integer in the NeverBounceAPIClient.timeout 115 | property""" 116 | client = neverbounce_sdk.client() 117 | client.timeout = 5 118 | assert client.timeout == 5 119 | 120 | 121 | def test_default_client_api_version_is_default(): 122 | """A newly created client must be configured with default api_version""" 123 | client = neverbounce_sdk.client() 124 | assert client.api_version == "v4.2" 125 | 126 | 127 | def test_setting_api_version(): 128 | """You may set API version NeverBounceAPIClient.api_version property""" 129 | client = neverbounce_sdk.client("api_key", None, 1, "some_version") 130 | assert client.api_version == "some_version" 131 | 132 | 133 | def test_setting_api_version_by_name(): 134 | """You may set API version NeverBounceAPIClient.api_version property""" 135 | client = neverbounce_sdk.client(api_version="some_other_version") 136 | assert client.api_version == "some_other_version" 137 | -------------------------------------------------------------------------------- /tests/test_download.py: -------------------------------------------------------------------------------- 1 | """Test the API endpoints located at /jobs""" 2 | from __future__ import unicode_literals 3 | import json 4 | 5 | import pytest 6 | import responses 7 | 8 | import neverbounce_sdk 9 | from neverbounce_sdk import urlforversion, GeneralException 10 | 11 | 12 | @pytest.fixture 13 | def client(): 14 | api_key = 'secret key' 15 | return neverbounce_sdk.client(api_key=api_key) 16 | 17 | 18 | @pytest.fixture 19 | def tempfile(tmpdir): 20 | return tmpdir.join('test.csv') 21 | 22 | 23 | @responses.activate 24 | def test_download_defaults(client, tempfile): 25 | responses.add(responses.POST, 26 | urlforversion('v4.2', 'jobs', 'download'), 27 | body=r'data\ndata', 28 | status=200, 29 | content_type='application/octet-stream') 30 | 31 | client.jobs_download(123, tempfile, line_feed_type='LINEFEED_0A') 32 | assert tempfile.read() == r'data\ndata' 33 | 34 | called_with = json.loads(responses.calls[0].request.body.decode('UTF-8')) 35 | default_args = { 36 | 'line_feed_type': 'LINEFEED_0A', 37 | 'binary_operators_type': 'BIN_1_0', 38 | 'valids': 1, 39 | 'invalids': 1, 40 | 'catchalls': 1, 41 | 'unknowns': 1, 42 | 'job_id': 123 43 | } 44 | 45 | for k, v in default_args.items(): 46 | assert called_with[k] == v 47 | 48 | 49 | @responses.activate 50 | def test_download_upstream_error(client, tempfile): 51 | responses.add(responses.POST, 52 | urlforversion('v4.2', 'jobs', 'download'), 53 | status=200, 54 | json={'status': 'general_failure', 55 | 'message': 'Something went wrong'}) 56 | 57 | with pytest.raises(GeneralException): 58 | client.jobs_download(123, tempfile) 59 | 60 | 61 | def test_malformed_download_options(client, tempfile): 62 | with pytest.raises(ValueError): 63 | client.jobs_download(123, tempfile, segmentation=('not an opt',)) 64 | with pytest.raises(ValueError): 65 | client.jobs_download(123, tempfile, appends=('not an opt',)) 66 | with pytest.raises(ValueError): 67 | client.jobs_download(123, tempfile, yes_no_representation='frowns') 68 | with pytest.raises(ValueError): 69 | client.jobs_download(123, tempfile, line_feed_type='emojis') 70 | -------------------------------------------------------------------------------- /tests/test_poe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the POE endpoints 3 | """ 4 | import responses 5 | 6 | import neverbounce_sdk 7 | from neverbounce_sdk import urlforversion 8 | 9 | 10 | @responses.activate 11 | def test_poe_confirm(): 12 | # this is the exepcted response 13 | responses.add(responses.GET, 14 | urlforversion('v4.2', 'poe', 'confirm'), 15 | status=200, 16 | json={'status': 'success'}) 17 | 18 | with neverbounce_sdk.client(api_key='static key') as client: 19 | info = client.poe_confirm( 20 | email='support@neverbounce.com', 21 | transaction_id='NBPOE-TXN-5942940c09669', 22 | confirmation_token='e3173fdbbdce6bad26522dae792911f2', 23 | result='valid') 24 | 25 | assert info == {'status': 'success'} 26 | assert len(responses.calls) == 1 27 | url = responses.calls[0].request.url 28 | for urlchunk in ('https://api.neverbounce.com/v4.2/poe/confirm', 29 | 'email=support%40neverbounce.com', 30 | 'transaction_id=NBPOE-TXN-5942940c09669', 31 | 'confirmation_token=e3173fdbbdce6bad26522dae792911f2', 32 | 'result=valid'): 33 | assert urlchunk in url 34 | -------------------------------------------------------------------------------- /tests/test_sanity.py: -------------------------------------------------------------------------------- 1 | """ This test should never fail; if it does, something is misconfigured in 2 | the build or dev environment""" 3 | 4 | 5 | def test_sanity_check(): 6 | test_sanity_check.__doc__ = __doc__ 7 | return True 8 | -------------------------------------------------------------------------------- /tests/test_single_check_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the Single endpoints 3 | """ 4 | import responses 5 | 6 | import neverbounce_sdk 7 | from neverbounce_sdk import urlforversion, urlfor 8 | 9 | 10 | @responses.activate 11 | def test_single_check(): 12 | # this is the exepcted response 13 | responses.add(responses.GET, 14 | urlfor('single', 'check'), 15 | status=200, 16 | json={'status': 'success'}) 17 | 18 | with neverbounce_sdk.client(api_key='static key') as client: 19 | info = client.single_check('test@example.com', credits_info=True) 20 | 21 | assert info == {'status': 'success'} 22 | assert len(responses.calls) == 1 23 | url = responses.calls[0].request.url 24 | for urlchunk in ('https://api.neverbounce.com/v4.2/single/check', 25 | 'email=test%40example.com', 26 | 'address_info=0', 27 | 'credits_info=1', 28 | 'timeout=30', 29 | 'key=static+key'): 30 | assert urlchunk in url 31 | 32 | 33 | @responses.activate 34 | def test_single_check_with_specific_version(): 35 | # this is the exepcted response 36 | responses.add(responses.GET, 37 | urlforversion('v4.2', 'single', 'check'), 38 | status=200, 39 | json={'status': 'success'}) 40 | 41 | with neverbounce_sdk.client(api_key='abc', api_version="v4.2") as client: 42 | info = client.single_check('test@example.com', credits_info=True) 43 | 44 | assert info == {'status': 'success'} 45 | assert len(responses.calls) == 1 46 | url = responses.calls[0].request.url 47 | for urlchunk in ('https://api.neverbounce.com/v4.2/single/check', 48 | 'email=test%40example.com', 49 | 'address_info=0', 50 | 'credits_info=1', 51 | 'timeout=30', 52 | 'key=abc'): 53 | assert urlchunk in url 54 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27 4 | py33 5 | py34 6 | py35 7 | py36 8 | py37 9 | pypy 10 | pypy3 11 | flake8 12 | 13 | [travis] 14 | python = 15 | "3.7-dev": py37 16 | 3.6: py36 17 | 3.5: py35 18 | 3.4: py34 19 | 3.3: py33 20 | 2.7: py27 21 | "pypy-5.7.1": pypy 22 | "pypy3.5-5.8.0": pypy3 23 | 24 | [testenv:flake8] 25 | basepython=python 26 | deps=flake8 27 | commands=flake8 neverbounce_sdk/ tests/ 28 | 29 | [testenv] 30 | setenv = 31 | PYTHONPATH = {toxinidir} 32 | deps = 33 | requests 34 | responses 35 | pytest 36 | commands = 37 | pip install --upgrade pip 38 | py.test --basetemp={envtmpdir} 39 | --------------------------------------------------------------------------------