From ab387e79f9f426fe2c3f27f18efeb10ff7d7d3e6 Mon Sep 17 00:00:00 2001 From: Evan Berquist Date: Fri, 30 May 2025 14:36:51 -0400 Subject: [PATCH 1/5] modify client to take key and token auth at request time --- README.md | 13 ++++++++++- emailable/client.py | 38 ++++++++++++++++++++++---------- tests/test_authentication.py | 42 ++++++++++++++++++++++++++++++++++++ tests/test_client.py | 16 -------------- 4 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 tests/test_authentication.py diff --git a/README.md b/README.md index 5ba17c4..52d00b0 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,23 @@ The library needs to be configured with your account's API key which is availabl ### Setup +When using an API key you can initialize the client to use it for all future +requests. + ```python import emailable client = emailable.Client('live_...') ``` +Alternatively, you can pass an OAuth access token to any of the endpoints. See +[here](https://emailable.com/docs/api/#oauth) for more details. + +```python +client = emailable.Client() +client.verify('evan@emailable.com', access_token=) +``` + ### Verification ```python @@ -36,7 +47,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..44781ad 100644 --- a/emailable/client.py +++ b/emailable/client.py @@ -6,14 +6,22 @@ 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 = { + 'headers': { + 'Authorization': f'Bearer {self.api_key or api_key or access_token}' + }, 'params': { - 'api_key': self.api_key, 'email': email, 'smtp': str(smtp).lower(), 'accept_all': str(accept_all).lower(), @@ -24,10 +32,12 @@ def verify(self, email, smtp=True, accept_all=False, timeout=None): url = self.base_url + 'verify' return self.__request('get', url, options) - def batch(self, emails, params={}): + def batch(self, emails, params={}, api_key=None, access_token=None): options = { + 'headers': { + 'Authorization': f'Bearer {self.api_key or api_key or access_token}' + }, 'params': { - **{'api_key': self.api_key}, **params }, 'json': { @@ -37,10 +47,16 @@ def batch(self, emails, params={}): url = self.base_url + 'batch' return self.__request('post', url, options) - def batch_status(self, batch_id, simulate=None): + def batch_status(self, + batch_id, + simulate=None, + api_key=None, + access_token=None): options = { + 'headers': { + 'Authorization': f'Bearer {self.api_key or api_key or access_token}' + }, 'params': { - 'api_key': self.api_key, 'id': batch_id, 'simulate': simulate } @@ -49,11 +65,11 @@ def batch_status(self, batch_id, simulate=None): url = self.base_url + 'batch' return self.__request('get', url, options) - def account(self): + def account(self, api_key=None, access_token=None): options = { - 'params': { - 'api_key': self.api_key - } + 'headers': { + 'Authorization': f'Bearer {self.api_key or api_key or access_token}' + }, } url = self.base_url + 'account' diff --git a/tests/test_authentication.py b/tests/test_authentication.py new file mode 100644 index 0000000..01b2e26 --- /dev/null +++ b/tests/test_authentication.py @@ -0,0 +1,42 @@ +from unittest import TestCase +import emailable +import time + +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) From 5936aca762cc922fd44958be16bfbaa2a2193ef5 Mon Sep 17 00:00:00 2001 From: Evan Berquist Date: Fri, 30 May 2025 14:48:58 -0400 Subject: [PATCH 2/5] remove unneed time import --- tests/test_authentication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 01b2e26..2ec54d5 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -1,6 +1,5 @@ from unittest import TestCase import emailable -import time class TestAuthentication(TestCase): From 55331165d1f22c5c4ba85c8f8f43119e8b1148d0 Mon Sep 17 00:00:00 2001 From: Evan Berquist Date: Fri, 30 May 2025 15:04:01 -0400 Subject: [PATCH 3/5] update readme --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 52d00b0..ce1e2ce 100644 --- a/README.md +++ b/README.md @@ -18,25 +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). -When using an API key you can initialize the client to use it for all future -requests. +An API key can be set globally for the Emailable client: ```python -import emailable - -client = emailable.Client('live_...') +client = emailable.Client('your_api_key') ``` -Alternatively, you can pass an OAuth access token to any of the endpoints. See -[here](https://emailable.com/docs/api/#oauth) for more details. +Or, you can specify an `api_key` or an `access_token` with each request: ```python -client = emailable.Client() -client.verify('evan@emailable.com', access_token=) +# set api_key at request time +client.verify(api_key='your_api_key') + +# set access_token at request time +client.verify(access_token='your_access_token') ``` ### Verification From 7d477f3b43529447d46707bbcb3d83fc7b2c4ac8 Mon Sep 17 00:00:00 2001 From: Evan Berquist Date: Mon, 2 Jun 2025 09:49:59 -0400 Subject: [PATCH 4/5] prioritize key or token before global key --- .github/workflows/ci.yml | 2 +- emailable/client.py | 28 ++++++++-------------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60ecbb9..09cc6cd 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.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/emailable/client.py b/emailable/client.py index 44781ad..986f110 100644 --- a/emailable/client.py +++ b/emailable/client.py @@ -18,9 +18,6 @@ def verify(self, api_key=None, access_token=None): options = { - 'headers': { - 'Authorization': f'Bearer {self.api_key or api_key or access_token}' - }, 'params': { 'email': email, 'smtp': str(smtp).lower(), @@ -30,13 +27,10 @@ def verify(self, } 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={}, api_key=None, access_token=None): options = { - 'headers': { - 'Authorization': f'Bearer {self.api_key or api_key or access_token}' - }, 'params': { **params }, @@ -45,7 +39,7 @@ def batch(self, emails, params={}, api_key=None, access_token=None): } } 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, @@ -53,9 +47,6 @@ def batch_status(self, api_key=None, access_token=None): options = { - 'headers': { - 'Authorization': f'Bearer {self.api_key or api_key or access_token}' - }, 'params': { 'id': batch_id, 'simulate': simulate @@ -63,20 +54,17 @@ def batch_status(self, } url = self.base_url + 'batch' - return self.__request('get', url, options) + return self.__request('get', url, options, api_key or access_token) def account(self, api_key=None, access_token=None): - options = { - 'headers': { - 'Authorization': f'Bearer {self.api_key or api_key or access_token}' - }, - } - 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() From 1d38d5f09182fa9f35b76fe33341c38e4ee897bc Mon Sep 17 00:00:00 2001 From: Evan Berquist Date: Mon, 2 Jun 2025 09:59:12 -0400 Subject: [PATCH 5/5] remove 3.8 from ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09cc6cd..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.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4