diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60ecbb9..31776c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 5ba17c4..ce1e2ce 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,26 @@ pip install emailable ## Usage -The library needs to be configured with your account's API key which is available in your [Emailable Dashboard](https://app.emailable.com/api). +### Authentication -### Setup +The Emailable API requires either an API key or an access token for +authentication. API keys can be created and managed in the +[Emailable Dashboard](https://app.emailable.com/api). + +An API key can be set globally for the Emailable client: + +```python +client = emailable.Client('your_api_key') +``` + +Or, you can specify an `api_key` or an `access_token` with each request: ```python -import emailable +# set api_key at request time +client.verify(api_key='your_api_key') -client = emailable.Client('live_...') +# set access_token at request time +client.verify(access_token='your_access_token') ``` ### Verification @@ -36,7 +48,7 @@ response = client.verify('evan@emailable.com') response.state => 'deliverable' -# additional parameters are available. see API docs for additional info. +# additional parameters are available. see API docs for more info. client.verify('evan@emailable.com', smtp=False, accept_all=True, timeout=25) ``` diff --git a/emailable/client.py b/emailable/client.py index 8faff96..986f110 100644 --- a/emailable/client.py +++ b/emailable/client.py @@ -6,14 +6,19 @@ class Client: - def __init__(self, api_key): + def __init__(self, api_key=None): self.api_key = api_key self.base_url = 'https://api.emailable.com/v1/' - def verify(self, email, smtp=True, accept_all=False, timeout=None): + def verify(self, + email, + smtp=True, + accept_all=False, + timeout=None, + api_key=None, + access_token=None): options = { 'params': { - 'api_key': self.api_key, 'email': email, 'smtp': str(smtp).lower(), 'accept_all': str(accept_all).lower(), @@ -22,12 +27,11 @@ def verify(self, email, smtp=True, accept_all=False, timeout=None): } url = self.base_url + 'verify' - return self.__request('get', url, options) + return self.__request('get', url, options, api_key or access_token) - def batch(self, emails, params={}): + def batch(self, emails, params={}, api_key=None, access_token=None): options = { 'params': { - **{'api_key': self.api_key}, **params }, 'json': { @@ -35,32 +39,32 @@ def batch(self, emails, params={}): } } url = self.base_url + 'batch' - return self.__request('post', url, options) + return self.__request('post', url, options, api_key or access_token) - def batch_status(self, batch_id, simulate=None): + def batch_status(self, + batch_id, + simulate=None, + api_key=None, + access_token=None): options = { 'params': { - 'api_key': self.api_key, 'id': batch_id, 'simulate': simulate } } url = self.base_url + 'batch' - return self.__request('get', url, options) - - def account(self): - options = { - 'params': { - 'api_key': self.api_key - } - } + return self.__request('get', url, options, api_key or access_token) + def account(self, api_key=None, access_token=None): url = self.base_url + 'account' - return self.__request('get', url, options) + return self.__request('get', url, {}, api_key or access_token) - def __request(self, method, url, options): + def __request(self, method, url, options, key_or_token): response = None + options['headers'] = { + 'Authorization': f'Bearer {key_or_token or self.api_key}' + } try: response = requests.request(method, url, **options) response.raise_for_status() diff --git a/tests/test_authentication.py b/tests/test_authentication.py new file mode 100644 index 0000000..2ec54d5 --- /dev/null +++ b/tests/test_authentication.py @@ -0,0 +1,41 @@ +from unittest import TestCase +import emailable + +class TestAuthentication(TestCase): + + def setUp(self): + self.api_key = 'test_7aff7fc0142c65f86a00' + self.email = 'evan@emailable.com' + self.emails = ['evan@emailable.com', 'jarrett@emailable.com'] + + def test_invalid_api_key_authentication(self): + client = emailable.Client('test_7aff7fc0141c65f86a00') + self.assertRaises( + emailable.AuthError, + client.verify, + 'evan@emailable.com' + ) + + def test_missing_api_key_authentication(self): + client = emailable.Client() + self.assertRaises( + emailable.AuthError, + client.verify, + 'evan@emailable.com' + ) + + def test_global_api_key_authentication(self): + client = emailable.Client(self.api_key) + self.assertIsNotNone(client.verify(self.email).domain) + batch_id = client.batch(self.emails).id + self.assertIsNotNone(batch_id) + self.assertIsNotNone(client.batch_status(batch_id).id) + self.assertIsNotNone(client.account().available_credits) + + def test_request_time_api_key_authentication(self): + client = emailable.Client() + self.assertIsNotNone(client.verify(self.email, api_key=self.api_key).domain) + batch_id = client.batch(self.emails, api_key=self.api_key).id + self.assertIsNotNone(batch_id) + self.assertIsNotNone(client.batch_status(batch_id, api_key=self.api_key).id) + self.assertIsNotNone(client.account(api_key=self.api_key).available_credits) diff --git a/tests/test_client.py b/tests/test_client.py index c014e0a..b584d08 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,22 +7,6 @@ def setUp(self): self.client = emailable.Client('test_7aff7fc0142c65f86a00') time.sleep(0.5) - def test_invalid_api_key(self): - client = emailable.Client('test_7aff7fc0141c65f86a00') - self.assertRaises( - emailable.AuthError, - client.verify, - 'evan@emailable.com' - ) - - def test_missing_api_key(self): - self.client.api_key = None - self.assertRaises( - emailable.AuthError, - self.client.verify, - 'evan@emailable.com' - ) - def test_verify_returns_response(self): response = self.client.verify('johndoe+tag@emailable.com') self.assertIsInstance(response, emailable.Response)