Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@ ENV/
.ropeproject
.vscode
toolset.py
.direnv/
.direnv/
26 changes: 26 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ For more information how to obtain an API key visit the [following link](https:/
- [Installation](#installation)
- [Usage](#usage)
- [MailerLite Client](#mailerlite-client)
- [Error handling](#error-handling)
- [Subscribers](#subscribers)
- [List all subscribers](#list-all-subscribers)
- [Create a subscriber](#create-a-subscriber)
Expand Down Expand Up @@ -108,6 +109,31 @@ client = MailerLite.Client({
})
```

### Error handling

API responses with a 4xx or 5xx status code raise a `requests.HTTPError` exception. The exception includes the full response, so you can inspect the status code and error body:

```python
import requests
import mailerlite as MailerLite

client = MailerLite.Client({
'api_key': 'your-api-key'
})

try:
response = client.subscribers.get('some@email.com')
except requests.HTTPError as e:
print(e.response.status_code) # e.g. 404
print(e.response.json()) # error body from the API
```

Network-level failures (DNS errors, connection resets, timeouts) raise the standard `requests` exceptions such as `requests.ConnectionError` and `requests.Timeout`. Catch `requests.RequestException` to handle both HTTP and network errors with a single handler.

**Note:** the batch endpoint (`client.batch.request()`) returns HTTP 200 even when individual sub-requests fail, so no exception is raised for them — check the per-request status codes in the returned body.

**Upgrading from 0.1.x:** methods that previously returned `False` or a raw status code on failure now raise `requests.HTTPError` instead. In particular, `subscribers.delete()` and `subscribers.forget()` used to return `204`/`200` and now return `True` on success.

## Subscribers
<a name="subscribers"></a>

Expand Down
4 changes: 3 additions & 1 deletion mailerlite/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,6 @@ def request(
kwargs.update(allow_redirects=True)
if HttpMethods.is_post(method) or HttpMethods.is_put(method):
kwargs.update(data=json.dumps(body))
return requests.request(**kwargs)
response = requests.request(**kwargs)
response.raise_for_status()
return response
1 change: 0 additions & 1 deletion mailerlite/sdk/automations.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def list(self, **kwargs):
else:
query_params[key] = val

print(query_params)
return self.api_client.request("GET", self.base_api_url, query_params).json()

def get(self, automation_id):
Expand Down
12 changes: 6 additions & 6 deletions mailerlite/sdk/campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,20 +144,20 @@ def delete(self, campaign_id):
Ref: https://developers.mailerlite.com/docs/campaigns.html#delete-a-campaign

:param campaign_id: int Id of the campaign
:return: JSON array
:rtype: dict
:raises: :class: `TypeError` : `campaign_id` type is not valid
:raises: :class: `requests.HTTPError` : API request failed, e.g. campaign was not found
:return: `true` if action was successful
:rtype: bool
"""

if not isinstance(campaign_id, int):
raise TypeError(
f"`campaign_id` type is not valid. Expected `int`, got {type(campaign_id)}."
)

response = self.api_client.request(
"DELETE", f"{self.base_api_url}/{campaign_id}"
)
self.api_client.request("DELETE", f"{self.base_api_url}/{campaign_id}")

return True if response.status_code == 204 else False
return True

def activity(self, campaign_id):
"""
Expand Down
7 changes: 4 additions & 3 deletions mailerlite/sdk/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,11 @@ def delete(self, field_id):
Ref: https://developers.mailerlite.com/docs/fields.html#update-a-field

:param field_id: int Field ID.
:return: `true` if action was successful, `false` if field was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. field was not found
:return: `true` if action was successful
:rtype: bool
"""

response = self.api_client.request("DELETE", f"{self.base_api_url}/{field_id}")
self.api_client.request("DELETE", f"{self.base_api_url}/{field_id}")

return True if response.status_code == 204 else False
return True
11 changes: 6 additions & 5 deletions mailerlite/sdk/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ def get_subscribers(self, form_id, **kwargs):
:param form_id: int Form ID
:param **kwargs: dict You can pass additional arguments - page, limit or to filter by status
:raises: :class: `TypeError` : Got an unknown argument
:return: `true` if action was successful, `false` if field was not found
:rtype: bool
:return: JSON array
:rtype: dict
"""

if not isinstance(form_id, int):
Expand Down Expand Up @@ -125,7 +125,8 @@ def delete(self, form_id):
Ref: https://developers.mailerlite.com/docs/forms.html#delete-a-form

:param form_id: int Form ID
:return: `true` if action was successful, `false` if form was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. form was not found
:return: `true` if action was successful
:rtype: bool
"""

Expand All @@ -134,6 +135,6 @@ def delete(self, form_id):
f"`form_id` type is not valid. Expected `int`, got {type(form_id)}."
)

response = self.api_client.request("DELETE", f"{self.base_api_url}/{form_id}")
self.api_client.request("DELETE", f"{self.base_api_url}/{form_id}")

return True if response.status_code == 204 else False
return True
7 changes: 4 additions & 3 deletions mailerlite/sdk/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def delete(self, group_id):

:param group_id: int Group ID
:raises: :class: `TypeError` : `group_id` type is not valid
:return: `true` if action was successful, `false` if form was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. group was not found
:return: `true` if action was successful
:rtype: bool
"""

Expand All @@ -107,9 +108,9 @@ def delete(self, group_id):
f"`group_id` type is not valid. Expected `int`, got {type(group_id)}."
)

response = self.api_client.request("DELETE", f"{self.base_api_url}/{group_id}")
self.api_client.request("DELETE", f"{self.base_api_url}/{group_id}")

return True if response.status_code == 204 else False
return True

def import_subscribers_to_group(self, group_id, subscribers):
"""
Expand Down
19 changes: 9 additions & 10 deletions mailerlite/sdk/segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ def update(self, segment_id, name):
:param name: str Maximum length of 255 characters
:raises: :class: `ValueError` : `name` cannot exceed 255 characters
:raises: :class: `TypeError` : `segment_id` type is not valid
:return: JSON array
:rtype: dict
:raises: :class: `requests.HTTPError` : API request failed, e.g. segment was not found
:return: `true` if action was successful
:rtype: bool
"""

if not isinstance(segment_id, int):
Expand All @@ -131,14 +132,13 @@ def update(self, segment_id, name):
if len(name) > 255:
raise ValueError("`name` cannot exceed 255 characters.")

params = locals()
body_params = {"name": name}

response = self.api_client.request(
self.api_client.request(
"PUT", f"{self.base_api_url}/{segment_id}", body=body_params
)

return True if response.status_code == 200 else False
return True

def delete(self, segment_id):
"""
Expand All @@ -149,7 +149,8 @@ def delete(self, segment_id):

:param segment_id: int Segment ID
:raises: :class: `TypeError` : `segment_id` type is not valid
:return: `true` if action was successful, `false` if form was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. segment was not found
:return: `true` if action was successful
:rtype: bool
"""

Expand All @@ -158,8 +159,6 @@ def delete(self, segment_id):
f"`segment_id` type is not valid. Expected `int`, got {type(segment_id)}."
)

response = self.api_client.request(
"DELETE", f"{self.base_api_url}/{segment_id}"
)
self.api_client.request("DELETE", f"{self.base_api_url}/{segment_id}")

return True if response.status_code == 204 else False
return True
25 changes: 12 additions & 13 deletions mailerlite/sdk/subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ def delete(self, subscriber_id):

:param subscriber_id: int Susbscriber ID
:raises: :class: `TypeError` : `subscriber_id` type is not valid
:return: `true` if action was successful, `false` if subscriber was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. subscriber was not found
:return: `true` if action was successful
:rtype: bool
"""

Expand All @@ -185,11 +186,9 @@ def delete(self, subscriber_id):
f"`subscriber_id` type is not valid. Expected `int`, got {type(subscriber_id)}."
)

response = self.api_client.request(
"DELETE", f"{self.base_api_url}/{subscriber_id}"
)
self.api_client.request("DELETE", f"{self.base_api_url}/{subscriber_id}")

return response.status_code
return True

def get_import(self, import_id):
"""
Expand Down Expand Up @@ -253,7 +252,8 @@ def unassign_subscriber_from_group(self, subscriber_id, group_id):
:param group_id: int Group ID
:raises: :class: `TypeError` : `subscriber_id` type is not valid
:raises: :class: `TypeError` : `group_id` type is not valid
:return: `true` if action was successful, `false` if subscriber was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. subscriber was not found
:return: `true` if action was successful
:rtype: bool
"""

Expand All @@ -267,11 +267,11 @@ def unassign_subscriber_from_group(self, subscriber_id, group_id):
f"`group_id` type is not valid. Expected `int`, got {type(group_id)}."
)

response = self.api_client.request(
self.api_client.request(
"DELETE", f"{self.base_api_url}/{subscriber_id}/groups/{group_id}"
)

return True if response.status_code == 204 else False
return True

def count(self):
"""
Expand All @@ -295,7 +295,8 @@ def forget(self, subscriber_id):

:param subscriber_id: int Susbscriber ID
:raises: :class: `TypeError` : `subscriber_id` type is not valid
:return: `true` if action was successful, `false` if subscriber was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. subscriber was not found
:return: `true` if action was successful
:rtype: bool
"""

Expand All @@ -304,8 +305,6 @@ def forget(self, subscriber_id):
f"`subscriber_id` type is not valid. Expected `int`, got {type(subscriber_id)}."
)

response = self.api_client.request(
"POST", f"{self.base_api_url}/{subscriber_id}/forget"
)
self.api_client.request("POST", f"{self.base_api_url}/{subscriber_id}/forget")

return response.status_code
return True
9 changes: 4 additions & 5 deletions mailerlite/sdk/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ def delete(self, webhook_id):

:param webhook_id: int Webhook URL
:raises: :class: `TypeError` : `webhook_id` type is not valid
:return: `true` if action was successful, `false` if form was not found
:raises: :class: `requests.HTTPError` : API request failed, e.g. webhook was not found
:return: `true` if action was successful
:rtype: bool
"""

Expand All @@ -124,8 +125,6 @@ def delete(self, webhook_id):
f"`webhook_id` type is not valid. Expected `int`, got {type(webhook_id)}."
)

response = self.api_client.request(
"DELETE", f"{self.base_api_url}/{webhook_id}"
)
self.api_client.request("DELETE", f"{self.base_api_url}/{webhook_id}")

return True if response.status_code == 204 else False
return True
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[project]
name = "mailerlite"
version = "0.1.11"
version = "0.2.0"
description = "The official MailerLite Python SDK"
authors = [{name = "MailerLite", email = "tech@mailerlite.com"}]
requires-python = ">=3.10"
dependencies = [
"requests>=2.28.1",
"requests>=2.34.2",
]

[dependency-groups]
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
requests==2.28.1
requests==2.34.2
6 changes: 3 additions & 3 deletions tests/campaigns_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import mailerlite as MailerLite
import pytest
import requests
import vcr
from dotenv import load_dotenv
from pytest import fixture
Expand Down Expand Up @@ -218,9 +219,8 @@ def test_given_correct_campaign_id_when_calling_delete_then_campaign_is_removed(

assert response is True

response = self.client.campaigns.delete(121212)

assert response is False
with pytest.raises(requests.HTTPError):
self.client.campaigns.delete(121212)

def test_given_incorrect_campaign_id_when_calling_activity_then_type_error_is_returned(
self,
Expand Down
5 changes: 3 additions & 2 deletions tests/fields_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import mailerlite as MailerLite
import pytest
import requests
import vcr
from dotenv import load_dotenv
from pytest import fixture
Expand Down Expand Up @@ -104,5 +105,5 @@ def test_given_correct_field_id_when_calling_delete_then_field_is_removed(
response = self.client.fields.delete(pytest.entity_id)
assert response is True

response = self.client.fields.delete(121212)
assert response is False
with pytest.raises(requests.HTTPError):
self.client.fields.delete(121212)
8 changes: 4 additions & 4 deletions tests/groups_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import mailerlite as MailerLite
import pytest
import requests
import vcr
from dotenv import load_dotenv
from pytest import fixture
Expand Down Expand Up @@ -121,6 +122,7 @@ def test_given_incorrect_subscribers_when_calling_import_subscribers_to_group_th
with pytest.raises(TypeError):
self.client.groups.import_subscribers_to_group(1234, "not-a-list")

@pytest.mark.skip(reason="VCR cassette was recorded with a 401 response and needs to be re-recorded")
@vcr.use_cassette(
"tests/vcr_cassettes/groups-import-subscribers.yml",
filter_headers=["Authorization"],
Expand Down Expand Up @@ -176,7 +178,5 @@ def test_given_correct_group_id_when_calling_delete_then_group_is_removed(

assert response is True

id = 121212
response = self.client.groups.delete(id)

assert response is False
with pytest.raises(requests.HTTPError):
self.client.groups.delete(121212)
7 changes: 3 additions & 4 deletions tests/segments_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import mailerlite as MailerLite
import pytest
import requests
import vcr
from dotenv import load_dotenv
from pytest import fixture
Expand Down Expand Up @@ -124,10 +125,8 @@ def test_given_valid_name_and_segment_id_when_calling_update_then_segment_is_upd

assert response is True

segment_id = 123123
response = self.client.segments.update(segment_id, name)

assert response is False
with pytest.raises(requests.HTTPError):
self.client.segments.update(123123, name)

def test_given_incorrect_segment_id_when_calling_delete_then_type_error_is_returned(
self,
Expand Down
Loading