From 135a3d2e170718280aa72d12e3690db2cb71674f Mon Sep 17 00:00:00 2001 From: Baptiste Fontaine Date: Sat, 7 Apr 2018 17:58:33 +0200 Subject: [PATCH 01/87] README: Fix grammar --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 92268a2..a6c3fd0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ### API documentation -Every code examples can be find on the [Mailjet Documentation][doc] +All code examples can be found on the [Mailjet Documentation][doc]. (Please refer to the [Mailjet Documentation Repository][api_doc] to contribute to the documentation examples) @@ -22,7 +22,7 @@ Every code examples can be find on the [Mailjet Documentation][doc] First, make sure you have an API key, and an API secret. Once you got them, save them in your environment: -``` +```bash export MJ_APIKEY_PUBLIC='your api key' export MJ_APIKEY_PRIVATE='your api secret' ``` @@ -40,17 +40,17 @@ mailjet = Client(auth=(API_KEY, API_SECRET), version='v3') ``` -**NOTE**: `version` reflects the api version in the url (`https://api.mailjet.com/{{ version }}/REST/`). It is `'v3'` by default and can be used to select another api version (for example `v3.1` for the new send API). +**NOTE**: `version` reflects the API version in the URL (`https://api.mailjet.com/{{ version }}/REST/`). It is `'v3'` by default and can be used to select another API version (for example `v3.1` for the new send API). ## Make a `GET` request: ``` python -# get every contacts +# get all contacts result = mailjet.contact.get() ``` ## `GET` request with filters: ``` python -# get the 2 first contacts +# get the first 2 contacts result = mailjet.contact.get(filters={'limit': 2}) ``` ## `POST` request @@ -61,7 +61,7 @@ result = mailjet.sender.create(data={'email': 'test@mailjet.com'}) ## Combine a resource with an action ``` python -# Get the contact lists of contact #2 +# Get the contacts lists of contact #2 result = mailjet.contact_getcontactslists.get(id=2) ``` @@ -72,7 +72,7 @@ email = { 'FromName': 'Mr Smith', 'FromEmail': 'mr@smith.com', 'Subject': 'Test Email', - 'Text-Part': 'Hey there !', + 'Text-Part': 'Hey there!', 'Recipients': [{'Email': 'your email here'}] } From 0d5032595c2c84f6ae30c903dd95035d8dd4980a Mon Sep 17 00:00:00 2001 From: mskochev Date: Tue, 13 Nov 2018 15:12:05 +0200 Subject: [PATCH 02/87] Bump version to 1.3.1 --- mailjet_rest/utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 93b500a..2dcfd6b 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -1,4 +1,4 @@ -VERSION = (1, 3, 0) +VERSION = (1, 3, 1) def get_version(version=None): From 5a4bc786bee22a151d978d605b7087c7b296a60d Mon Sep 17 00:00:00 2001 From: Michal Martinek Date: Sat, 29 Dec 2018 17:48:37 +0100 Subject: [PATCH 03/87] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6c3fd0..88d3238 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Simple Mailjet APIv3 wrapper +# Simple Mailjet APIv3 wrapper [doc]: http://dev.mailjet.com/guides/?python# [api_doc]: https://github.com/mailjet/api-documentation From 7a604e30875934ef7dc18f156b9e2f033ba71258 Mon Sep 17 00:00:00 2001 From: mskochev Date: Wed, 2 Jan 2019 13:57:16 +0200 Subject: [PATCH 04/87] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a6c3fd0..5339836 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -#Simple Mailjet APIv3 wrapper +![alt text](https://www.mailjet.com/images/email/transac/logo_header.png "Mailjet") + +# Simple Mailjet APIv3 Python Wrapper [doc]: http://dev.mailjet.com/guides/?python# [api_doc]: https://github.com/mailjet/api-documentation From d1b8b0af2e0a56ead347d76e83502e6cc406d346 Mon Sep 17 00:00:00 2001 From: Atanas Damyanliev Date: Tue, 8 Jan 2019 11:32:33 +0200 Subject: [PATCH 05/87] update readme for sendapi v3.1 --- README.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5339836..eb1a4a9 100644 --- a/README.md +++ b/README.md @@ -70,15 +70,33 @@ result = mailjet.contact_getcontactslists.get(id=2) ## Send an Email ``` python -email = { - 'FromName': 'Mr Smith', - 'FromEmail': 'mr@smith.com', - 'Subject': 'Test Email', - 'Text-Part': 'Hey there!', - 'Recipients': [{'Email': 'your email here'}] +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret), version='v3.1') +data = { + 'Messages': [ + { + "From": { + "Email": "pilot@mailjet.com", + "Name": "Mailjet Pilot" + }, + "To": [ + { + "Email": "passenger1@mailjet.com", + "Name": "passenger 1" + } + ], + "Subject": "Your email flight plan!", + "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", + "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with you!" + } + ] } - -mailjet.send.create(email) +result = mailjet.send.create(data=data) +print result.status_code +print result.json() ``` From 20408958aaf4c94232c55d0c24becf7640c511e9 Mon Sep 17 00:00:00 2001 From: Atanas Damyanliev Date: Fri, 11 Jan 2019 12:46:04 +0200 Subject: [PATCH 06/87] add versioning section --- README.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index eb1a4a9..a56ebe5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ -![alt text](https://www.mailjet.com/images/email/transac/logo_header.png "Mailjet") - -# Simple Mailjet APIv3 Python Wrapper - +[api_credential]: https://app.mailjet.com/account/api_keys [doc]: http://dev.mailjet.com/guides/?python# [api_doc]: https://github.com/mailjet/api-documentation +[smsDashboard]: https://app.mailjet.com/sms?_ga=2.81581655.1972348350.1522654521-1279766791.1506937572 +[smsInfo]: https://app.mailjet.com/docs/transactional-sms?_ga=2.183303910.1972348350.1522654521-1279766791.1506937572#trans-sms-token + +![alt text](https://www.mailjet.com/images/email/transac/logo_header.png "Mailjet") + +# Official Mailjet Python Wrapper [![Build Status](https://travis-ci.org/mailjet/mailjet-apiv3-python.svg?branch=master)](https://travis-ci.org/mailjet/mailjet-apiv3-python) @@ -21,14 +24,23 @@ All code examples can be found on the [Mailjet Documentation][doc]. ## Getting Started -First, make sure you have an API key, and an API secret. -Once you got them, save them in your environment: +Grab your API and Secret Keys [here][api_credential]. You need them for authentication when using the Email API: ```bash export MJ_APIKEY_PUBLIC='your api key' export MJ_APIKEY_PRIVATE='your api secret' ``` +## API Versioning + +The Mailjet API is spread among three distinct versions: + +- `v3` - The Email API +- `v3.1` - Email Send API v3.1, which is the latest version of our Send API +- `v4` - SMS API + +Since most Email API endpoints are located under `v3`, it is set as the default one and does not need to be specified when making your request. For the others you need to specify the version using `version`. For example, if using Send API `v3.1`: + ``` python # import the mailjet wrapper from mailjet_rest import Client @@ -38,11 +50,11 @@ import os API_KEY = os.environ['MJ_APIKEY_PUBLIC'] API_SECRET = os.environ['MJ_APIKEY_PRIVATE'] -mailjet = Client(auth=(API_KEY, API_SECRET), version='v3') +mailjet = Client(auth=(API_KEY, API_SECRET), version='v3.1') ``` -**NOTE**: `version` reflects the API version in the URL (`https://api.mailjet.com/{{ version }}/REST/`). It is `'v3'` by default and can be used to select another API version (for example `v3.1` for the new send API). +For additional information refer to our [API Reference](https://dev.preprod.mailjet.com/reference/overview/versioning/). ## Make a `GET` request: ``` python From 47d1fe3ea38123821572fd175194f06e1470238a Mon Sep 17 00:00:00 2001 From: Atanas Damyanliev Date: Fri, 11 Jan 2019 14:00:13 +0200 Subject: [PATCH 07/87] add sendapi v3 info --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a56ebe5..738e3d6 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ print result.json() ``` +You can also use the previous version of Mailjet's Send API (v3). You can find the documentation explaining the overall differences and code samples [here](https://dev.mailjet.com/guides/?python#sending-a-basic-email-v3). + ## Create a new Contact ``` python From 88191a3ad445d43f621e69b22d80d7117265f6c2 Mon Sep 17 00:00:00 2001 From: Todor Dimitrov <44998445+todorDim@users.noreply.github.com> Date: Wed, 20 Mar 2019 15:55:28 +0200 Subject: [PATCH 08/87] Remove url slicing --- mailjet_rest/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index f4d1af9..00306c2 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -23,7 +23,6 @@ def __init__(self, version=None, api_url=None): self.api_url = api_url or self.DEFAULT_API_URL def __getitem__(self, key): - url = self.api_url[0:] # Append version to URL. # Forward slash is ignored if present in self.version. url = urljoin(url, self.version + '/') From cddcd2740985d9b5fbd39eecf1a1b25bc44a5f29 Mon Sep 17 00:00:00 2001 From: Todor Dimitrov <44998445+todorDim@users.noreply.github.com> Date: Wed, 20 Mar 2019 16:28:15 +0200 Subject: [PATCH 09/87] Fix URL slicing. --- mailjet_rest/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 00306c2..263d446 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -25,7 +25,7 @@ def __init__(self, version=None, api_url=None): def __getitem__(self, key): # Append version to URL. # Forward slash is ignored if present in self.version. - url = urljoin(url, self.version + '/') + url = urljoin(self.api_url, self.version + '/') headers = {'Content-type': 'application/json', 'User-agent': self.user_agent} if key.lower() == 'contactslist_csvdata': url = urljoin(url, 'DATA/') From ad6dc6d192a44b770beedc4103a277508765761c Mon Sep 17 00:00:00 2001 From: Todor Dimitrov <44998445+todorDim@users.noreply.github.com> Date: Wed, 20 Mar 2019 16:29:26 +0200 Subject: [PATCH 10/87] Increase wrapper version --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index afae274..a9494a6 100644 --- a/test.py +++ b/test.py @@ -90,7 +90,7 @@ def test_user_agent(self): auth=self.auth, version='v3.1' ) - self.assertEqual(self.client.config.user_agent, 'mailjet-apiv3-python/v1.3.2') + self.assertEqual(self.client.config.user_agent, 'mailjet-apiv3-python/v1.3.3') if __name__ == '__main__': From 6a263b4567f17b2c774324b10a61c5ad0c53dd1e Mon Sep 17 00:00:00 2001 From: Todor Dimitrov <44998445+todorDim@users.noreply.github.com> Date: Wed, 20 Mar 2019 16:52:56 +0200 Subject: [PATCH 11/87] Fix unit tests for new API address --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index a9494a6..bdac3b8 100644 --- a/test.py +++ b/test.py @@ -82,7 +82,7 @@ def test_client_custom_version(self): self.assertEqual(self.client.config.version, 'v3.1') self.assertEqual( self.client.config['send'][0], - 'https://api.mailjet.com/v3.1/send' + 'https://api.eu.mailjet.com/v3.1/send' ) def test_user_agent(self): From 4279bfc1737e69883716dd4c8332b7bf0756eef5 Mon Sep 17 00:00:00 2001 From: Atanas Damyanliev Date: Wed, 29 May 2019 17:17:33 +0300 Subject: [PATCH 12/87] add info about US base URL to readme --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 738e3d6..d860c93 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,16 @@ mailjet = Client(auth=(API_KEY, API_SECRET), version='v3.1') For additional information refer to our [API Reference](https://dev.preprod.mailjet.com/reference/overview/versioning/). +### Base URL + +The default base domain name for the Mailjet API is `api.mailjet.com`. You can modify this base URL by setting a value for `api_url` in your call: + +```python +mailjet = Client(auth=(api_key, api_secret),api_url="https://api.us.mailjet.com/") +``` + +If your account has been moved to Mailjet's **US architecture**, the URL value you need to set is `https://api.us.mailjet.com`. + ## Make a `GET` request: ``` python # get all contacts From 9c89b8ad86a9764820c4386a35ce06501f1af73f Mon Sep 17 00:00:00 2001 From: adamyanliev Date: Mon, 15 Jul 2019 11:54:10 +0300 Subject: [PATCH 13/87] readme revamp draft --- README.md | 292 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 240 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index d860c93..eb1e468 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,124 @@ +[mailjet](http://www.mailjet.com/) [api_credential]: https://app.mailjet.com/account/api_keys [doc]: http://dev.mailjet.com/guides/?python# [api_doc]: https://github.com/mailjet/api-documentation -[smsDashboard]: https://app.mailjet.com/sms?_ga=2.81581655.1972348350.1522654521-1279766791.1506937572 -[smsInfo]: https://app.mailjet.com/docs/transactional-sms?_ga=2.183303910.1972348350.1522654521-1279766791.1506937572#trans-sms-token ![alt text](https://www.mailjet.com/images/email/transac/logo_header.png "Mailjet") # Official Mailjet Python Wrapper [![Build Status](https://travis-ci.org/mailjet/mailjet-apiv3-python.svg?branch=master)](https://travis-ci.org/mailjet/mailjet-apiv3-python) +![Current Version](https://img.shields.io/badge/version-1.3.2-green.svg) -### API documentation +## Overview -All code examples can be found on the [Mailjet Documentation][doc]. +Welcome to the [Mailjet][mailjet] official Python API wrapper! -(Please refer to the [Mailjet Documentation Repository][api_doc] to contribute to the documentation examples) +Check out all the resources and Python code examples in the official [Mailjet Documentation][doc]. + +## Table of contents + +- [Compatibility](#compatibility) +- [Installation](#installation) +- [Authentication](#authentication) +- [Make your first call](#make-your-first-call) +- [Client / Call configuration specifics](#client--call-configuration-specifics) + - [API versioning](#api-versioning) + - [Base URL](#base-url) +- [Request examples](#request-examples) + - [POST request](#post-request) + - [Simple POST request](#simple-post-request) + - [Using actions](#using-actions) + - [GET request](#get-request) + - [Retrieve all objects](#retrieve-all-objects) + - [Use filtering](#use-filtering) + - [Retrieve a single object](#retrieve-a-single-object) + - [PUT request](#put-request) + - [DELETE request](#delete-request) +- [Contribute](#contribute) + +## Compatibility + +This library officially supports the following Node.js versions: + + - v2.7 + - v3.5 + - v3.6 ## Installation +Use the below code to install the wrapper: + ``` bash (sudo) pip install mailjet_rest ``` -## Getting Started +## Authentication -Grab your API and Secret Keys [here][api_credential]. You need them for authentication when using the Email API: +The Mailjet Email API uses your API and Secret keys for authentication. [Grab][api_credential] and save your Mailjet API credentials. ```bash export MJ_APIKEY_PUBLIC='your api key' export MJ_APIKEY_PRIVATE='your api secret' ``` -## API Versioning +Initialize your [Mailjet][mailjet] client: + +```python +# import the mailjet wrapper +from mailjet_rest import Client +import os + +# Get your environment Mailjet keys +API_KEY = os.environ['MJ_APIKEY_PUBLIC'] +API_SECRET = os.environ['MJ_APIKEY_PRIVATE'] + +mailjet = Client(auth=(API_KEY, API_SECRET)) +``` + +## Make your first call + +Here's an example on how to send an email: + +```python +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret), version='v3.1') +data = { + 'Messages': [ + { + "From": { + "Email": "$SENDER_EMAIL", + "Name": "Me" + }, + "To": [ + { + "Email": "$RECIPIENT_EMAIL", + "Name": "You" + } + ], + "Subject": "My first Mailjet Email!", + "TextPart": "Greetings from Mailjet!", + "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with you!" + } + ] +} +result = mailjet.send.create(data=data) +print result.status_code +print result.json() +``` + +## Client / Call Configuration Specifics + +### API Versioning The Mailjet API is spread among three distinct versions: - `v3` - The Email API - `v3.1` - Email Send API v3.1, which is the latest version of our Send API -- `v4` - SMS API +- `v4` - SMS API (not supported in Python) Since most Email API endpoints are located under `v3`, it is set as the default one and does not need to be specified when making your request. For the others you need to specify the version using `version`. For example, if using Send API `v3.1`: @@ -51,7 +132,6 @@ API_KEY = os.environ['MJ_APIKEY_PUBLIC'] API_SECRET = os.environ['MJ_APIKEY_PRIVATE'] mailjet = Client(auth=(API_KEY, API_SECRET), version='v3.1') - ``` For additional information refer to our [API Reference](https://dev.preprod.mailjet.com/reference/overview/versioning/). @@ -66,70 +146,178 @@ mailjet = Client(auth=(api_key, api_secret),api_url="https://api.us.mailjet.com/ If your account has been moved to Mailjet's **US architecture**, the URL value you need to set is `https://api.us.mailjet.com`. -## Make a `GET` request: -``` python -# get all contacts -result = mailjet.contact.get() +## Request examples + +### POST request + +#### Simple POST request + +```python +""" +Create a new contact: +""" +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret)) +data = { + 'Email': 'Mister@mailjet.com' +} +result = mailjet.contact.create(data=data) +print result.status_code +print result.json() ``` -## `GET` request with filters: -``` python -# get the first 2 contacts -result = mailjet.contact.get(filters={'limit': 2}) +#### Using actions + +```python +""" +Manage the subscription status of a contact to multiple lists: +""" +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret)) +id = '$ID' +data = { + 'ContactsLists': [ + { + "ListID": "$ListID_1", + "Action": "addnoforce" + }, + { + "ListID": "$ListID_2", + "Action": "addforce" + } + ] +} +result = mailjet.contact_managecontactslists.create(id=id, data=data) +print result.status_code +print result.json() ``` -## `POST` request -``` python -# Register a new sender email address -result = mailjet.sender.create(data={'email': 'test@mailjet.com'}) + +### GET Request + +#### Retrieve all objects + +```python +""" +Retrieve all contacts: +""" +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret)) +result = mailjet.contact.get() +print result.status_code +print result.json() ``` -## Combine a resource with an action -``` python -# Get the contacts lists of contact #2 -result = mailjet.contact_getcontactslists.get(id=2) +#### Using filtering + +```python +""" +Retrieve all contacts that are not in the campaign exclusion list: +""" +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret)) +filters = { + 'IsExcludedFromCampaigns': false, +} +result = mailjet.contact.get(filters=filters) +print result.status_code +print result.json() ``` -## Send an Email -``` python +#### Retrieve a single object +```python +""" +Retrieve a specific contact ID: +""" from mailjet_rest import Client import os api_key = os.environ['MJ_APIKEY_PUBLIC'] api_secret = os.environ['MJ_APIKEY_PRIVATE'] -mailjet = Client(auth=(api_key, api_secret), version='v3.1') +mailjet = Client(auth=(api_key, api_secret)) +id = 'Contact_ID' +result = mailjet.contact.get(id=id) +print result.status_code +print result.json() +``` + +### PUT request + +A `PUT` request in the Mailjet API will work as a `PATCH` request - the update will affect only the specified properties. The other properties of an existing resource will neither be modified, nor deleted. It also means that all non-mandatory properties can be omitted from your payload. + +Here's an example of a `PUT` request: + +```python +""" +Update the contact properties for a contact: +""" +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret)) +id = '$CONTACT_ID' data = { - 'Messages': [ + 'Data': [ + { + "Name": "first_name", + "value": "John" + }, { - "From": { - "Email": "pilot@mailjet.com", - "Name": "Mailjet Pilot" - }, - "To": [ - { - "Email": "passenger1@mailjet.com", - "Name": "passenger 1" - } - ], - "Subject": "Your email flight plan!", - "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", - "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with you!" + "Name": "last_name", + "value": "Smith" } ] } -result = mailjet.send.create(data=data) +result = mailjet.contactdata.update(id=id, data=data) print result.status_code print result.json() - ``` -You can also use the previous version of Mailjet's Send API (v3). You can find the documentation explaining the overall differences and code samples [here](https://dev.mailjet.com/guides/?python#sending-a-basic-email-v3). +### DELETE request -## Create a new Contact -``` python +Upon a successful `DELETE` request the response will not include a response body, but only a `204 No Content` response code. -# wrapping the call inside a function -def new_contact(email): - return mailjet.contact.create(data={'Email': email}) +Here's an example of a `DELETE` request: -new_contact('mr@smith.com') +```python +""" +Delete an email template: +""" +from mailjet_rest import Client +import os +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] +mailjet = Client(auth=(api_key, api_secret)) +id = 'Template_ID' +result = mailjet.template.delete(id=id) +print result.status_code +print result.json() ``` + +## Contribute + +Mailjet loves developers. You can be part of this project! + +This wrapper is a great introduction to the open source world, check out the code! + +Feel free to ask anything, and contribute: + +- Fork the project. +- Create a new branch. +- Implement your feature or bug fix. +- Add documentation to it. +- Commit, push, open a pull request and voila. + +If you have suggestions on how to improve the guides, please submit an issue in our [Official API Documentation repo](https://github.com/mailjet/api-documentation). From 22dffdb0abc568913c2c25eea69d21c952dd3e01 Mon Sep 17 00:00:00 2001 From: Emmanuel Boisgontier Date: Mon, 22 Jul 2019 17:35:08 +0300 Subject: [PATCH 14/87] fix page --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb1e468..2553be8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[mailjet](http://www.mailjet.com/) + +[mailjet]:(http://www.mailjet.com/) [api_credential]: https://app.mailjet.com/account/api_keys [doc]: http://dev.mailjet.com/guides/?python# [api_doc]: https://github.com/mailjet/api-documentation From e57f714c22201cb16cf87dc168865dcb4200afd1 Mon Sep 17 00:00:00 2001 From: Skia Date: Fri, 27 Sep 2019 09:06:39 +0200 Subject: [PATCH 15/87] Update README.md `s/node.js/Python/` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2553be8..681d1f9 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Check out all the resources and Python code examples in the official [Mailjet Do ## Compatibility -This library officially supports the following Node.js versions: +This library officially supports the following Python versions: - v2.7 - v3.5 From 322695d3a42c51ca50e9cdc29e125f3ae427667c Mon Sep 17 00:00:00 2001 From: Wojtek Siudzinski Date: Sun, 3 May 2020 16:40:22 +0200 Subject: [PATCH 16/87] Support V4 API Fixes #39 --- mailjet_rest/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 263d446..01c39a5 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -33,7 +33,7 @@ def __getitem__(self, key): elif key.lower() == 'batchjob_csverror': url = urljoin(url, 'DATA/') headers['Content-type'] = 'text/csv' - elif key.lower() != 'send': + elif key.lower() != 'send' and self.version != 'v4': url = urljoin(url, 'REST/') url = url + key.split('_')[0].lower() return url, headers From 2af00199dc3b3d97438569bfea56bf3f180ae3aa Mon Sep 17 00:00:00 2001 From: diskovod Date: Tue, 6 Oct 2020 18:22:14 +0300 Subject: [PATCH 17/87] Fixed sort issue https://github.com/mailjet/mailjet-apiv3-python/issues/46 --- mailjet_rest/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 263d446..d000344 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -100,7 +100,10 @@ def api_call(auth, method, url, headers, data=None, filters=None, resource_id=No req_method = getattr(requests, method) try: - response = req_method(url, data=data, params=filters, headers=headers, auth=auth, + filters_str = None + if filters: + filters_str = "&".join("%s=%s" % (k, v) for k, v in filters.items()) + response = req_method(url, data=data, params=filters_str, headers=headers, auth=auth, timeout=timeout, verify=True, stream=False) return response From d39cc22cca67b37f9e1e9c4ec05bc89f2936f5ec Mon Sep 17 00:00:00 2001 From: diskovod Date: Tue, 6 Oct 2020 19:41:35 +0300 Subject: [PATCH 18/87] Create CHANGELOG.md https://github.com/mailjet/mailjet-apiv3-python/issues/40 --- CHANGELOG.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cfe32ac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,102 @@ +# Changelog + +## [Unreleased](https://github.com/mailjet/mailjet-apiv3-python/tree/HEAD) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.3.2...HEAD) + +**Closed issues:** + +- Response 400 error [\#59](https://github.com/mailjet/mailjet-apiv3-python/issues/59) +- Lib expected to work on py3.7? [\#48](https://github.com/mailjet/mailjet-apiv3-python/issues/48) +- FromTS-ToTS filter does not work for GET /message [\#47](https://github.com/mailjet/mailjet-apiv3-python/issues/47) +- import name Client [\#33](https://github.com/mailjet/mailjet-apiv3-python/issues/33) +- proxy dict [\#23](https://github.com/mailjet/mailjet-apiv3-python/issues/23) +- Too many 500 [\#19](https://github.com/mailjet/mailjet-apiv3-python/issues/19) +- ImportError: cannot import name Client [\#16](https://github.com/mailjet/mailjet-apiv3-python/issues/16) +- Add a "date" property on pypi [\#15](https://github.com/mailjet/mailjet-apiv3-python/issues/15) +- Django support [\#9](https://github.com/mailjet/mailjet-apiv3-python/issues/9) + +**Merged pull requests:** + +- Update README.md [\#44](https://github.com/mailjet/mailjet-apiv3-python/pull/44) ([Hyask](https://github.com/Hyask)) +- new readme version with standartized content [\#42](https://github.com/mailjet/mailjet-apiv3-python/pull/42) ([adamyanliev](https://github.com/adamyanliev)) +- fix page [\#41](https://github.com/mailjet/mailjet-apiv3-python/pull/41) ([adamyanliev](https://github.com/adamyanliev)) +- Fix unit tests for new API address [\#37](https://github.com/mailjet/mailjet-apiv3-python/pull/37) ([todorDim](https://github.com/todorDim)) +- Fix URL slicing, update version in unit test [\#36](https://github.com/mailjet/mailjet-apiv3-python/pull/36) ([todorDim](https://github.com/todorDim)) +- Add support for domain specific api url, update requests module, remove python 2.6 support [\#34](https://github.com/mailjet/mailjet-apiv3-python/pull/34) ([todorDim](https://github.com/todorDim)) +- add versioning section [\#32](https://github.com/mailjet/mailjet-apiv3-python/pull/32) ([adamyanliev](https://github.com/adamyanliev)) +- Update README.md [\#31](https://github.com/mailjet/mailjet-apiv3-python/pull/31) ([mskochev](https://github.com/mskochev)) +- Fix README.md [\#30](https://github.com/mailjet/mailjet-apiv3-python/pull/30) ([MichalMartinek](https://github.com/MichalMartinek)) + +## [v1.3.2](https://github.com/mailjet/mailjet-apiv3-python/tree/v1.3.2) (2018-11-19) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.3.1...v1.3.2) + +**Merged pull requests:** + +- Add action\_id to get [\#29](https://github.com/mailjet/mailjet-apiv3-python/pull/29) ([mskochev](https://github.com/mskochev)) +- Add action\_id to get, increase minor version [\#28](https://github.com/mailjet/mailjet-apiv3-python/pull/28) ([todorDim](https://github.com/todorDim)) + +## [v1.3.1](https://github.com/mailjet/mailjet-apiv3-python/tree/v1.3.1) (2018-11-13) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.3.0...v1.3.1) + +**Closed issues:** + +- How to add a contact to a list [\#22](https://github.com/mailjet/mailjet-apiv3-python/issues/22) +- Impossible to know what is wrong [\#20](https://github.com/mailjet/mailjet-apiv3-python/issues/20) +- wrong version number [\#13](https://github.com/mailjet/mailjet-apiv3-python/issues/13) +- example missing / not working [\#11](https://github.com/mailjet/mailjet-apiv3-python/issues/11) +- Remove 'Programming Language :: Python :: 3.2', from setup.py [\#10](https://github.com/mailjet/mailjet-apiv3-python/issues/10) + +**Merged pull requests:** + +- Features/add action [\#27](https://github.com/mailjet/mailjet-apiv3-python/pull/27) ([todorDim](https://github.com/todorDim)) +- Fix action\_id [\#26](https://github.com/mailjet/mailjet-apiv3-python/pull/26) ([mskochev](https://github.com/mskochev)) +- Pass action id, change build\_url to accept both number and string [\#25](https://github.com/mailjet/mailjet-apiv3-python/pull/25) ([todorDim](https://github.com/todorDim)) +- README: Fix grammar [\#18](https://github.com/mailjet/mailjet-apiv3-python/pull/18) ([bfontaine](https://github.com/bfontaine)) +- Fix issue \#13 [\#14](https://github.com/mailjet/mailjet-apiv3-python/pull/14) ([latanasov](https://github.com/latanasov)) +- Improve Package version [\#12](https://github.com/mailjet/mailjet-apiv3-python/pull/12) ([jorgii](https://github.com/jorgii)) + +## [v1.3.0](https://github.com/mailjet/mailjet-apiv3-python/tree/v1.3.0) (2017-05-31) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.2.2...v1.3.0) + +**Closed issues:** + +- SSL certificate validation disabled [\#7](https://github.com/mailjet/mailjet-apiv3-python/issues/7) +- No license? [\#6](https://github.com/mailjet/mailjet-apiv3-python/issues/6) + +**Merged pull requests:** + +- Api version kwargs [\#8](https://github.com/mailjet/mailjet-apiv3-python/pull/8) ([jorgii](https://github.com/jorgii)) +- fix unresolved variable inside build\_headers [\#4](https://github.com/mailjet/mailjet-apiv3-python/pull/4) ([vparitskiy](https://github.com/vparitskiy)) + +## [v1.2.2](https://github.com/mailjet/mailjet-apiv3-python/tree/v1.2.2) (2016-06-21) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.0.6...v1.2.2) + +**Merged pull requests:** + +- Fix mixed indent type [\#3](https://github.com/mailjet/mailjet-apiv3-python/pull/3) ([Malimediagroup](https://github.com/Malimediagroup)) + +## [v1.0.6](https://github.com/mailjet/mailjet-apiv3-python/tree/v1.0.6) (2016-06-20) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.0.4...v1.0.6) + +**Merged pull requests:** + +- Fix bug in delete method [\#2](https://github.com/mailjet/mailjet-apiv3-python/pull/2) ([kidig](https://github.com/kidig)) +- Include packages in setup.py [\#1](https://github.com/mailjet/mailjet-apiv3-python/pull/1) ([cheungpat](https://github.com/cheungpat)) + +## [v1.0.4](https://github.com/mailjet/mailjet-apiv3-python/tree/v1.0.4) (2015-11-19) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.0.3...v1.0.4) + +## [v1.0.3](https://github.com/mailjet/mailjet-apiv3-python/tree/v1.0.3) (2015-10-13) + +[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/19cf9a00a948e84de4842b51b0336e978f7a849f...v1.0.3) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From 70157a9f8a5e016869982aa7e33175435ba21dce Mon Sep 17 00:00:00 2001 From: diskovod Date: Fri, 9 Oct 2020 11:11:03 +0300 Subject: [PATCH 19/87] Update test.py https://github.com/mailjet/mailjet-apiv3-python/issues/53 --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index bdac3b8..06124bc 100644 --- a/test.py +++ b/test.py @@ -72,7 +72,7 @@ def test_get_with_id_filter(self): def test_post_with_no_param(self): result = self.client.sender.create(data={}).json() - self.assertTrue('StatusCode' in result and result['StatusCode'] is not 400) + self.assertTrue('StatusCode' in result and result['StatusCode'] == 400) def test_client_custom_version(self): self.client = Client( From 346435c9470757c84d778ee0f0bfa48ec24848d9 Mon Sep 17 00:00:00 2001 From: diskovod Date: Fri, 9 Oct 2020 13:29:38 +0300 Subject: [PATCH 20/87] Update test.py Updated link line 85 --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index bdac3b8..a9494a6 100644 --- a/test.py +++ b/test.py @@ -82,7 +82,7 @@ def test_client_custom_version(self): self.assertEqual(self.client.config.version, 'v3.1') self.assertEqual( self.client.config['send'][0], - 'https://api.eu.mailjet.com/v3.1/send' + 'https://api.mailjet.com/v3.1/send' ) def test_user_agent(self): From cc58067d3755dfdf4fd4f5627ef33dd0d38c9503 Mon Sep 17 00:00:00 2001 From: diskovod Date: Fri, 9 Oct 2020 13:30:26 +0300 Subject: [PATCH 21/87] Update client.py Updated link lin 15 --- mailjet_rest/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 263d446..ee3928c 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -12,7 +12,7 @@ class Config(object): - DEFAULT_API_URL = 'https://api.eu.mailjet.com/' + DEFAULT_API_URL = 'https://api.mailjet.com/' API_REF = 'http://dev.mailjet.com/email-api/v3/' version = 'v3' user_agent = 'mailjet-apiv3-python/v' + get_version() From 21c2db7b8c07600911652616b8515f722c5d47a1 Mon Sep 17 00:00:00 2001 From: diskovod Date: Tue, 20 Oct 2020 16:26:33 +0300 Subject: [PATCH 22/87] Update setup.py --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index c514131..9762f34 100644 --- a/setup.py +++ b/setup.py @@ -11,16 +11,16 @@ long_description = fh.read() # Dynamically calculate the version based on mailjet_rest.VERSION. -version = __import__('mailjet_rest').get_version() +version = "latest" setup( name=PACKAGE_NAME, - version=version, author='starenka', author_email='starenka0@gmail.com', maintainer='Mailjet', maintainer_email='api@mailjet.com', - download_url='https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v' + version, + version="latest", + download_url='https://github.com/mailjet/mailjet-apiv3-python/releases/' + version, url='https://github.com/mailjet/mailjet-apiv3-python', description=('Mailjet V3 API wrapper'), long_description=long_description, From 5b2da01ffbc6a75f387860b02a5e7fdab6c11cad Mon Sep 17 00:00:00 2001 From: diskovod Date: Mon, 26 Oct 2020 10:26:58 +0200 Subject: [PATCH 23/87] Update .travis.yml Added env secrets for Travis testing --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8eb3238..5cda5a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,3 +16,7 @@ script: python test.py notifications: slack: secure: Y6dTB+/gVfUn7z84et8HAS8gb21+FJoeDmz9f6Lkc9VUk8fL5lqrsV5cZedEWH1sXzzbN8GN2qOoLDKFa+HpxxA/27urSSVKYxYzqdZQZtVZBRSjVzflDJAUzUk1zfn1SmkhMJZO+EF7SOGLBSqbR2LgIzac6P6lKP6dtI8ZjS++O8VtaPOWPzxb8AuByOO6YR7sl6ZWO3OK30ovEOnAAjeiAC8nD0isjZylzhABQj2AKSelFf0zczMTJBMlB8gIXh+gf+sIG4RZknDb2qnFxstHU8p8FD54KdfkOA4W8UTGBc+5DUx0z8fVIo9JcBwIbbFIlcvZ5LY7atu7baBQO7jURXk7uI/w9gDFoE+NhrqF06NqeCHgySc3KSNf2NaxCiKSD5K4WDxumV16KliMqJzjumwG08+/TqgIzC9/Aj/b+5skxzhWkRP/H5iz8sOqCPHGk1pk7B1PwxuOHwIuzeLQ9aELYQgFIBVMqM2bFLLRk9eRKwpkpagApyAhTjV3hqAmUanL6fiPqIar0f4QQ5vFFronFCp08hVQ/b+WA8cLJ6r3FqBiP0XUzea8gsdbgXO4HB/hXtWx7oWzuPL39kx+aJHdt/rSI90shkk7dzhI3FQy+iSC5QnfVhNPyc3OprzWNlNcZjbI7ElS106LjrKcr8ilAMgpuFa06wuybxo= +env: + global: + - secure: x2agpUFUHBYcTgmuHeYuqxuNIMFz1V0PG9a09BpDmYrj0PT2BsecaMCtsWyWOUkzTb9S2/fE6oJnWjyNewp936px681K1aBa5GKKCkifnbxkRRY0dmUkFcrgWG8JZH9hXjzmgjUzwTefJ+xKkeVCySRCNgFm6MP0TmzKZWHjAxOHIHz1akJBfuAhCyVJU0grw7JSAPHSwMj/7erj7ub1pEvWjRjtJe9pMYTtrSYJRtt2qCkSRNK3/i+YZ7mKfigcaIcDgdaPYw0osUqB1DZHJK4RtC80pHkZNAosWIIMU7WohpYijGTIZRPjY4le3uiGqTJcaDhL84Jnmervczd62Iqhf9TE6YfKYksnfbk4YYy9GcTPggBSUV0COB9vApncVENZgeFUO8nSodL0ru5PO0KHcX6Er2zMdqieKseuJY1iLDidyqbTHqR9bZ9dck9CLA4KfLUycwaXYHb9c35iA9caOn+Xm7G1UvVSh3uE63FBsG1ubATe6pIfWZFgVo6zbH52hOTXQhLeximCD7CiSPef69iNzH9jTi9LU04hTuk69X8BWqvHTiYwPoLhdQueXwy2/LaOvSGex8qS5cGYBnWkeb0y99Qm6WpQjCkZhmcDvSXBlBnbbhm6Xc4NnLdX8cp/nVBYrYsgVsf2T7vUnmJBngv1Vsqa4+r9QRxBbxk= + - secure: Dwmug98uIhT3i03nai2Ufa0fBJS4i4zYclqywZJkqEVS84UrKDTZOLJFCDgJv3NlZ29BxvrnH8QqPHn9J6hJb++DGT6Ak7vonfuMYedxNAADn/RjBun+esQsPQYdko/GwGw1Z6kPucBT6Jp3Owd+GDjbnTXFmwSfag/sbTc38lp3mgvDAvcUiyTmQD0hsbHLw6GrpRte9BpSfnJ5ImCgz9nacuZPC084FQcMi0PyV4Ug9L32jnfVePwFD1pCjZpc41m3M2SP89XBjUrBRwyDzgTS8jxt82LN3mQKpxl3EguWLYuKCrK0vJQphrWlhUcRdAkCroSgTtWa9MAGb1DT44OqcbAGEPcMqJKNR7MtzsFnPq6QLO5IDXn05OmOZi1BChxXpENq/gmj2c8OmPkndZTHBwfG3sP0iuC7xrlSBr/++UWQYi07oIYgWfpxYMnXUS0ZEHOUg4oCvQBydB+mcVgRinN2cgXzwfOoVvAofjuT52mzyMHJz9OBqbcyqnCdEZzxS8cn7LTf6ZoWrFFSDQg5Fn8G/6U+yzPT87iz3di0uuCU7VsltXMs25HXdLLQxxbqBkPOhawtIUuzxmq/1eLBb0R+GzjgAsqMJl40iJdnDiYaOAgb6LE6Ix7II80I5nfLkE/60N0AgzZHJy6c/RhYy39eI7D9HQBDLcyW8AM= From 02aece97d9a82cd9a2087668f1899652d79c294b Mon Sep 17 00:00:00 2001 From: diskovod Date: Tue, 27 Oct 2020 11:07:30 +0200 Subject: [PATCH 24/87] Update .travis.yml Changed Python version from 3.6 -> 3.8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5cda5a1..85e5d21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python python: - '2.7' - '3.5' -- '3.6' +- '3.8' deploy: provider: pypi user: api.mailjet From 4e1aaaf5f58e98fc41886b541fb6326bdbf3092f Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Thu, 21 Apr 2022 19:55:37 +0300 Subject: [PATCH 25/87] Add filters usage documentation --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 681d1f9..fe78cb0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ Check out all the resources and Python code examples in the official [Mailjet Do - [Using actions](#using-actions) - [GET request](#get-request) - [Retrieve all objects](#retrieve-all-objects) - - [Use filtering](#use-filtering) + - [Using filtering](#using-filtering) + - [Using pagination](#using-pagination) - [Retrieve a single object](#retrieve-a-single-object) - [PUT request](#put-request) - [DELETE request](#delete-request) @@ -236,6 +237,30 @@ print result.status_code print result.json() ``` +#### Using pagination + +Pagination can be used when API returns Data as array type. +There is 2 options: `limit` and `offset`. +You can find such request example [here](https://dev.mailjet.com/email/reference/contacts/contact/#v3_get_contact). +Next example returns 40 contacts starting from 50 record: + +```python +import os +from mailjet_rest import Client + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] +mailjet = Client(auth=(api_key, api_secret)) + +filters = { + "limit": 40, + "offset": 50, +} +result = mailjet.contact.get(filters=filters) +print(result.status_code) +print(result.json()) +``` + #### Retrieve a single object ```python From e2e2107288f6be9e28adc358e67fd7c2e36468a3 Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Tue, 26 Apr 2022 19:34:48 +0300 Subject: [PATCH 26/87] Add some changes --- .gitignore | 3 ++- README.md | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 49f3ee8..2f5f953 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ nosetests.xml coverage.xml junit* build/ -dist/ \ No newline at end of file +dist/ +venv/ \ No newline at end of file diff --git a/README.md b/README.md index fe78cb0..4d7151e 100644 --- a/README.md +++ b/README.md @@ -239,10 +239,10 @@ print result.json() #### Using pagination -Pagination can be used when API returns Data as array type. -There is 2 options: `limit` and `offset`. -You can find such request example [here](https://dev.mailjet.com/email/reference/contacts/contact/#v3_get_contact). -Next example returns 40 contacts starting from 50 record: +Some requests (for example [GET /contact](https://dev.mailjet.com/email/reference/contacts/contact/#v3_get_contact)) has `limit` and `offset` query string parameters. These parameters could be used for pagination. +`limit` `int` Limit the response to a select number of returned objects. Default value: `10`. Maximum value: `1000` +`offset` `int` Retrieve a list of objects starting from a certain offset. Combine this query parameter with Limit to retrieve a specific section of the list of objects. Default value: `0` +Next example returns 40 contacts starting from 51th record: ```python import os From 7ee52ebb92af778634c8e98c1d40b12fb1f464d4 Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Wed, 27 Apr 2022 19:16:47 +0300 Subject: [PATCH 27/87] Add sort to doc --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4d7151e..21073f8 100644 --- a/README.md +++ b/README.md @@ -239,10 +239,11 @@ print result.json() #### Using pagination -Some requests (for example [GET /contact](https://dev.mailjet.com/email/reference/contacts/contact/#v3_get_contact)) has `limit` and `offset` query string parameters. These parameters could be used for pagination. +Some requests (for example [GET /contact](https://dev.mailjet.com/email/reference/contacts/contact/#v3_get_contact)) has `limit`, `offset` and `sort` query string parameters. These parameters could be used for pagination. `limit` `int` Limit the response to a select number of returned objects. Default value: `10`. Maximum value: `1000` -`offset` `int` Retrieve a list of objects starting from a certain offset. Combine this query parameter with Limit to retrieve a specific section of the list of objects. Default value: `0` -Next example returns 40 contacts starting from 51th record: +`offset` `int` Retrieve a list of objects starting from a certain offset. Combine this query parameter with `limit` to retrieve a specific section of the list of objects. Default value: `0` +`sort` `str` Sort the results by a property and select ascending (ASC) or descending (DESC) order. The default order is ascending. Keep in mind that this is not available for all properties. Default value: `ID asc` +Next example returns 40 contacts starting from 51th record sorted by `Email` field descendally: ```python import os @@ -255,6 +256,7 @@ mailjet = Client(auth=(api_key, api_secret)) filters = { "limit": 40, "offset": 50, + "sort": "Email desc", } result = mailjet.contact.get(filters=filters) print(result.status_code) From 10921bce411dd181aefb74115d5e44d3be44115a Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Wed, 27 Apr 2022 19:58:02 +0300 Subject: [PATCH 28/87] Add possibility to use dash contained URLs --- README.md | 17 +++++++++++++++++ mailjet_rest/client.py | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 681d1f9..5329830 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Check out all the resources and Python code examples in the official [Mailjet Do - [Client / Call configuration specifics](#client--call-configuration-specifics) - [API versioning](#api-versioning) - [Base URL](#base-url) + - [URL path](#url-path) - [Request examples](#request-examples) - [POST request](#post-request) - [Simple POST request](#simple-post-request) @@ -147,6 +148,22 @@ mailjet = Client(auth=(api_key, api_secret),api_url="https://api.us.mailjet.com/ If your account has been moved to Mailjet's **US architecture**, the URL value you need to set is `https://api.us.mailjet.com`. +### URL path + +According to python special characters limitations we can't use slashes `/` and dashes `-` which is acceptable for URL path building. Instead python client uses another way for path building. You should replase slashes `/` by underscore `_` and dashes `-` by capitalizing next letter in path. +For example, to reach `statistics/link-click` path you should call `statistics_linkClick` attribute of python client. + +```python +# GET `statistics/link-click` +mailjet = Client(auth=(api_key, api_secret)) +filters = { + 'CampaignId': 'xxxxxxx' +} +result = mailjet.statistics_linkClick.get(filters=filters) +print result.status_code +print result.json() +``` + ## Request examples ### POST request diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index ac25050..036dd56 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -3,6 +3,7 @@ import json import logging +import re import requests from requests.compat import urljoin @@ -11,6 +12,13 @@ requests.packages.urllib3.disable_warnings() +def prepare_url(key: str): + """Replaces capital letters to lower one with dash prefix.""" + char_elem = key.group(0) + if char_elem.isupper(): + return '-' + char_elem.lower() + + class Config(object): DEFAULT_API_URL = 'https://api.mailjet.com/' API_REF = 'http://dev.mailjet.com/email-api/v3/' @@ -79,6 +87,7 @@ def __init__(self, auth=None, **kwargs): self.config = Config(version=version, api_url=api_url) def __getattr__(self, name): + name = re.sub(r"[A-Z]", prepare_url, name) split = name.split('_') #identify the resource fname = split[0] From e4938374f4952db2ff43af126d71a075eedef4bd Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Fri, 29 Apr 2022 20:03:28 +0300 Subject: [PATCH 29/87] Add end of file to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2f5f953..a2f91f2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,4 @@ coverage.xml junit* build/ dist/ -venv/ \ No newline at end of file +venv/ From 004c48729fbd92c0f96cddfcf5ec1fad75b02b73 Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Wed, 29 Jun 2022 18:24:30 +0300 Subject: [PATCH 30/87] Add getting started sample --- mailjet_rest/client.py | 2 +- samples/contacts_sample.py | 55 +++++++++++++++++++++ samples/getting_started_sample.py | 81 +++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 samples/contacts_sample.py create mode 100644 samples/getting_started_sample.py diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 036dd56..d743d0c 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -12,7 +12,7 @@ requests.packages.urllib3.disable_warnings() -def prepare_url(key: str): +def prepare_url(key): """Replaces capital letters to lower one with dash prefix.""" char_elem = key.group(0) if char_elem.isupper(): diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py new file mode 100644 index 0000000..6189efd --- /dev/null +++ b/samples/contacts_sample.py @@ -0,0 +1,55 @@ +import json +import os + +from mailjet_rest import Client + + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def create_contact(): + """POST https://api.mailjet.com/v3/REST/contact""" + data = { + "IsExcludedFromCampaigns": "true", + "Name": "New Contact", + "Email": "passenger@mailjet.com", + } + return mailjet30.contact.create(data=data) + + +def create_contact_metadata(): + """POST https://api.mailjet.com/v3/REST/contactmetadata""" + data = { + "Datatype": "str", + "Name": "first_name", + "NameSpace": "static" + } + return mailjet30.contactmetadata.create(data=data) + + +def edit_contact_data(): + """PUT https://api.mailjet.com/v3/REST/contactdata/$contact_ID""" + id = "*********" # Put real ID to make it work. + data = { + "Data": [ + { + "Name": "first_name", + "Value": "John" + } + ] + } + return mailjet30.contactdata.update(id=id, data=data) + + +if __name__ == "__main__": + result = edit_contact_data() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) diff --git a/samples/getting_started_sample.py b/samples/getting_started_sample.py new file mode 100644 index 0000000..6caaed0 --- /dev/null +++ b/samples/getting_started_sample.py @@ -0,0 +1,81 @@ +import json +import os + +from mailjet_rest import Client + + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def send_messages(): + """POST https://api.mailjet.com/v3.1/send""" + data = { + "Messages": [ + { + "From": { + "Email": "pilot@mailjet.com", + "Name": "Mailjet Pilot" + }, + "To": [ + { + "Email": "passenger1@mailjet.com", + "Name": "passenger 1" + } + ], + "Subject": "Your email flight plan!", + "TextPart": "Dear passenger 1, welcome to Mailjet! May the " + "delivery force be with you!", + "HTMLPart": "

Dear passenger 1, welcome to Mailjet!
May the " + "delivery force be with you!" + } + ], + "SandboxMode": True, # Remove to send real message. + } + return mailjet31.send.create(data=data) + + +def retrieve_messages_from_campaign(): + """GET https://api.mailjet.com/v3/REST/message?CampaignID=$CAMPAIGNID""" + filters = { + "CampaignID": "*****", # Put real ID to make it work. + } + return mailjet30.message.get(filters=filters) + + +def retrieve_message(): + """GET https://api.mailjet.com/v3/REST/message/$MESSAGE_ID""" + _id = "*****************" # Put real ID to make it work. + return mailjet30.message.get(_id) + + +def view_message_history(): + """GET https://api.mailjet.com/v3/REST/messagehistory/$MESSAGE_ID""" + _id = "*****************" # Put real ID to make it work. + return mailjet30.messagehistory.get(_id) + + +def retrieve_statistic(): + """GET https://api.mailjet.com/v3/REST/statcounters?CounterSource=APIKey + \\&CounterTiming=Message\\&CounterResolution=Lifetime + """ + filters = { + "CounterSource": "APIKey", + "CounterTiming": "Message", + "CounterResolution": "Lifetime", + } + return mailjet30.statcounters.get(filters=filters) + + +if __name__ == "__main__": + result = retrieve_statistic() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) From dd814cd67eac42ea00a814e87b79a11cfaab3ffa Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Thu, 30 Jun 2022 10:45:28 +0300 Subject: [PATCH 31/87] Add Pycharm idea to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a2f91f2..88e6787 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ junit* build/ dist/ venv/ +.idea/ From eb2395cc39e5f0aab7f1bfe18f48863e8eebfc7f Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Thu, 30 Jun 2022 15:17:10 +0300 Subject: [PATCH 32/87] from 30.06.2022 --- samples/contacts_sample.py | 529 ++++++++++++++++++++++++++++++++++++- 1 file changed, 526 insertions(+), 3 deletions(-) diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 6189efd..18c0ed7 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -3,7 +3,6 @@ from mailjet_rest import Client - mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) @@ -34,7 +33,7 @@ def create_contact_metadata(): def edit_contact_data(): """PUT https://api.mailjet.com/v3/REST/contactdata/$contact_ID""" - id = "*********" # Put real ID to make it work. + _id = "*********" # Put real ID to make it work. data = { "Data": [ { @@ -43,7 +42,531 @@ def edit_contact_data(): } ] } - return mailjet30.contactdata.update(id=id, data=data) + return mailjet30.contactdata.update(id=_id, data=data) + + +def create_a_contact(): + """POST https://api.mailjet.com/v3/REST/contact""" + data = { + "IsExcludedFromCampaigns": "true", + "Name": "New Contact", + "Email": "passenger@mailjet.com" + } + return mailjet30.contact.create(data=data) + + +def manage_contact_properties(): + """POST https://api.mailjet.com/v3/REST/contactmetadata""" + _id = "$contact_ID" + data = { + "Data": [ + { + "Name": "first_name", + "Value": "John" + } + ] + } + return mailjet30.contactdata.update(id=_id, data=data) + + +def create_a_contact_list(): + """POST https://api.mailjet.com/v3/REST/contactslist""" + data = { + "Name": "my_contactslist" + } + return mailjet30.contactslist.create(data=data) + + +def add_a_contact_to_a_contact_list(): + """POST https://api.mailjet.com/v3/REST/listrecipient""" + data = { + "IsUnsubscribed": "true", + "ContactID": "987654321", + "ContactAlt": "passenger@mailjet.com", + "ListID": "123456", + "ListAlt": "abcdef123" + } + return mailjet30.listrecipient.create(data=data) + + +def manage_the_subscription_status_of_an_existing_contact(): + """POST https://api.mailjet.com/v3/REST/contact/$contact_ID/managecontactslists""" + _id = "$contact_ID" + data = { + "ContactsLists": [ + { + "Action": "addforce", + "ListID": "987654321" + }, + { + "Action": "addnoforce", + "ListID": "987654321" + }, + { + "Action": "remove", + "ListID": "987654321" + }, + { + "Action": "unsub", + "ListID": "987654321" + } + ] + } + return mailjet30.contact_managecontactslists.create(id=_id, data=data) + + +def manage_multiple_contacts_in_a_list(): + """POST https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" + _id = "$list_ID" + data = { + "Action": "addnoforce", + "Contacts": [ + { + "Email": "passenger@mailjet.com", + "IsExcludedFromCampaigns": "false", + "Name": "Passenger 1", + "Properties": "object" + } + ] + } + return mailjet30.contactslist_managemanycontacts.create(id=_id, data=data) + + +def monitor_the_upload_job(): + """GET https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" + _id = "$list_ID" + return mailjet30.contactslist_managemanycontacts.get(id=_id) + + +def manage_multiple_contacts_across_multiple_lists(): + """POST https://api.mailjet.com/v3/REST/contact/managemanycontacts""" + data = { + "Contacts": [ + { + "Email": "passenger@mailjet.com", + "IsExcludedFromCampaigns": "false", + "Name": "Passenger 1", + "Properties": "object" + } + ], + "ContactsLists": [ + { + "Action": "addforce", + "ListID": "987654321" + }, + { + "Action": "addnoforce", + "ListID": "987654321" + }, + { + "Action": "remove", + "ListID": "987654321" + }, + { + "Action": "unsub", + "ListID": "987654321" + } + ] + } + return mailjet30.contact_managemanycontacts.create(data=data) + + +def upload_the_csv(): + """POST https://api.mailjet.com/v3/DATA/contactslist/$ID_CONTACTLIST/CSVData/text:plain""" + f = open("./data.csv") + return mailjet30.contactslist_csvdata.create(id="$ID_CONTACTLIST", data=f.read()) + + +def import_csv_content_to_a_list(): + """POST https://api.mailjet.com/v3/REST/csvimport""" + data = { + "ErrTreshold": "1", + "ImportOptions": "", + "Method": "addnoforce", + "ContactsListID": "123456", + "DataID": "98765432123456789" + } + return mailjet30.csvimport.create(data=data) + + +def using_csv_with_atetime_contact_data(): + """POST https://api.mailjet.com/v3/REST/csvimport""" + data = { + "ContactsListID": "$ID_CONTACTLIST", + "DataID": "$ID_DATA", + "Method": "addnoforce", + "ImportOptions": "{\"DateTimeFormat\": \"yyyy/mm/dd\",\"TimezoneOffset\": 2,\"FieldNames\": [\"email\"," + "\"birthday\"]} " + } + return mailjet30.csvimport.create(data=data) + + +def monitor_the_import_progress(): + """GET https://api.mailjet.com/v3/REST/csvimport/$importjob_ID""" + _id = "$importjob_ID" + return mailjet30.csvimport.get(id=id) + + +def error_handling(): + """https://api.mailjet.com/v3/DATA/BatchJob/$job_id/CSVError/text:csv""" + """Not available in Python, please refer to Curl""" + + +def single_contact_exclusion(): + """PUT https://api.mailjet.com/v3/REST/contact/$ID_OR_EMAIL""" + _id = "$ID_OR_EMAIL" + data = { + "IsExcludedFromCampaigns": "true" + } + return mailjet30.contact.update(id=_id, data=data) + + +def using_contact_managemanycontacts(): + """POST https://api.mailjet.com/v3/REST/contact/managemanycontacts""" + data = { + "Contacts": [ + { + "Email": "jimsmith@example.com", + "Name": "Jim", + "IsExcludedFromCampaigns": "true", + "Properties": { + "Property1": "value", + "Property2": "value2" + } + }, + { + "Email": "janetdoe@example.com", + "Name": "Janet", + "IsExcludedFromCampaigns": "true", + "Properties": { + "Property1": "value", + "Property2": "value2" + } + } + ] + } + return mailjet30.contact_managemanycontacts.create(data=data) + + +def using_csvimport(): + """POST https://api.mailjet.com/v3/REST/csvimport""" + data = { + "DataID": "$ID_DATA", + "Method": "excludemarketing" + } + return mailjet30.csvimport.create(data=data) + + +def retrieve_a_contact(): + """GET https://api.mailjet.com/v3/REST/contact/$CONTACT_EMAIL""" + _id = "$CONTACT_EMAIL" + return mailjet30.contact.get(id=_id) + + +def delete_the_contact(): + """DELETE https://api.mailjet.com/v4/contacts/{contact_ID}""" + + +################################################################ +# ### Email template. Template API +################################################################ + + +def create_a_template(): + """POST https://api.mailjet.com/v3/REST/template""" + data = { + "Author": "John Doe", + "Categories": "array", + "Copyright": "Mailjet", + "Description": "Used to send out promo codes.", + "EditMode": "1", + "IsStarred": "false", + "IsTextPartGenerationEnabled": "true", + "Locale": "en_US", + "Name": "Promo Codes", + "OwnerType": "user", + "Presets": "string", + "Purposes": "array" + } + return mailjet30.template.create(data=data) + + +def create_a_template_detailcontent(): + """POST https://api.mailjet.com/v3/REST/template/$template_ID/detailcontent""" + _id = "$template_ID" + data = { + "Headers": "", + "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", + "MJMLContent": "", + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" + } + return mailjet30.template_detailcontent.create(id=_id, data=data) + + +def ise_templates_with_send_api(): + """POST https://api.mailjet.com/v3.1/send""" + data = { + "Messages": [ + { + "From": { + "Email": "pilot@mailjet.com", + "Name": "Mailjet Pilot" + }, + "To": [ + { + "Email": "passenger1@mailjet.com", + "Name": "passenger 1" + } + ], + "TemplateID": 1, + "TemplateLanguage": True, + "Subject": "Your email flight plan!" + } + ] + } + return mailjet31.send.create(data=data) + + +############################################################### +# ### Send campaigns using /campaigndraft +############################################################### + + +def create_a_campaign_draft(): + """POST https://api.mailjet.com/v3/REST/campaigndraft""" + data = { + "Locale": "en_US", + "Sender": "MisterMailjet", + "SenderEmail": "Mister@mailjet.com", + "Subject": "Greetings from Mailjet", + "ContactsListID": "$ID_CONTACTSLIST", + "Title": "Friday newsletter" + } + return mailjet30.campaigndraft.create(data=data) + + +def by_adding_custom_content(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/detailcontent""" + _id = "$draft_ID" + data = { + "Headers": "object", + "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", + "MJMLContent": "", + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" + } + return mailjet30.campaigndraft_detailcontent.create(id=_id, data=data) + + +def test_your_campaign(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/test""" + _id = "$draft_ID" + data = { + "Recipients": [ + { + "Email": "passenger@mailjet.com", + "Name": "Passenger 1" + } + ] + } + return mailjet30.campaigndraft_test.create(id=_id, data=data) + + +def schedule_the_sending(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/schedule""" + _id = "$draft_ID" + data = { + "Date": "2018-01-01T00:00:00" + } + return mailjet30.campaigndraft_schedule.create(id=_id, data=data) + + +def send_the_campaign_right_away(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/send""" + _id = "$draft_ID" + return mailjet30.campaigndraft_send.create(id=_id) + + +################################################################################ +# ## Send campaigns using the Send API +################################################################################ + +def api_call_requirements(): + """POST https://api.mailjet.com/v3.1/send""" + data = { + "Messages": [ + { + "From": { + "Email": "pilot@mailjet.com", + "Name": "Mailjet Pilot" + }, + "To": [ + { + "Email": "passenger1@mailjet.com", + "Name": "passenger 1" + } + ], + "Subject": "Your email flight plan!", + "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", + "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with " + "you!", + "CustomCampaign": "SendAPI_campaign", + "DeduplicateCampaign": True + } + ] + } + return mailjet31.send.create(data=data) + + +######################################################################################## +# ## Segmentation +######################################################################################## + + +def create_your_segment(): + """POST https://api.mailjet.com/v3/REST/contactfilter""" + data = { + "Description": "Will send only to contacts under 35 years of age.", + "Expression": "(age<35)", + "Name": "Customers under 35" + } + return mailjet30.contactfilter.create(data=data) + + +def create_a_campaign_with_a_segmentation_filter(): + """POST https://api.mailjet.com/v3/REST/newsletter""" + data = { + "Title": "Mailjet greets every contact over 40", + "Locale": "en_US", + "Sender": "MisterMailjet", + "SenderEmail": "Mister@mailjet.com", + "Subject": "Greetings from Mailjet", + "ContactsListID": "$ID_CONTACTLIST", + "SegmentationID": "$ID_CONTACT_FILTER" + } + return mailjet30.newsletter.create(data=data) + + +############################################################## +# ## Event tracking via webhooks +############################################################### + + +################################################################ +# ## Statistics +############################################################### + + +def event_based_vs_message_based_stats_timing(): + """GET https://api.mailjet.com/v3/REST/statcounters""" + filters = { + "SourceId": "$Campaign_ID", + "CounterSource": "Campaign", + "CounterTiming": "Message", + "CounterResolution": "Lifetime" + } + return mailjet30.statcounters.get(filters=filters) + + +def view_the_spread_of_events_over_time(): + """GET https://api.mailjet.com/v3/REST/statcounters""" + filters = { + "SourceId": "$Campaign_ID", + "CounterSource": "Campaign", + "CounterTiming": "Event", + "CounterResolution": "Day", + "FromTS": "123", + "ToTS": "456" + } + return mailjet30.statcounters.get(filters=filters) + + +def statistics_for_specific_recipient(): + """GET https://api.mailjet.com/v3/REST/contactstatistics""" + return mailjet30.contactstatistics.get() + + +def stats_for_clicked_links(): + """GET https://api.mailjet.com/v3/REST/statistics/link-click""" + filters = { + "CampaignId": "$Campaign_ID" + } + return mailjet30.statistics_linkClick.get(filters=filters) # TODO + + +def mailbox_provider_statistics(): + """GET https://api.mailjet.com/v3/REST/statistics/recipient-esp""" + filters = { + "CampaignId": "$Campaign_ID" + } + return mailjet30.statistics_recipientEsp.get(filters=filters) # TODO + + +def geographical_statistics(): + """GET https://api.mailjet.com/v3/REST/geostatistics""" + return mailjet30.geostatistics.get() + + +############################################################################ +# ## Parse API: inbound emails +############################################################################ + + +def basic_setup(): + """POST https://api.mailjet.com/v3/REST/parseroute""" + data = { + "Url": "https://www.mydomain.com/mj_parse.php" + } + return mailjet30.parseroute.create(data=data) + + +#################################################################################### +# ## Senders and Domains +################################################################################### + + +def validate_an_entire_domain(): + """GET https: // api.mailjet.com / v3 / REST / dns""" + _id = "$dns_ID" + return mailjet30.dns.get(id=_id) + + +def do_an_immediate_check_via_a_post(): + """POST https://api.mailjet.com/v3/REST/dns/$dns_ID/check""" + _id = "$dns_ID" + return mailjet30.dns_check.create(id=_id) + + +def host_a_text_file(): + """GET https://api.mailjet.com/v3/REST/sender""" + _id = "$sender_ID" + return mailjet30.sender.get(id=_id) + + +def validation_by_doing_a_post(): + """POST https://api.mailjet.com/v3/REST/sender/$sender_ID/validate""" + _id = "$sender_ID" + return mailjet30.sender_validate.create(id=_id) + + +def spf_and_dkim_validation(): + """ET https://api.mailjet.com/v3/REST/dns""" + _id = "$dns_ID" + return mailjet30.dns.get(id=_id) + + +def use_a_sender_on_all_api_keys(): + """POST https://api.mailjet.com/v3/REST/metasender""" + data = { + "Description": "Metasender 1 - used for Promo emails", + "Email": "pilot@mailjet.com" + } + return mailjet30.metasender.create(data=data) + + +################################################################### +# ## Language libraries +################################################################### if __name__ == "__main__": From e315f28429f35ac563c57cc982f501c11b2b37dc Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Wed, 6 Jul 2022 18:51:30 +0300 Subject: [PATCH 33/87] Separate samples on themes --- samples/campaign_sample.py | 102 ++++++++ samples/contacts_sample.py | 338 ++------------------------- samples/email_template_sample.py | 75 ++++++ samples/new_sample.py | 20 ++ samples/parse_api_sample.py | 28 +++ samples/segments_sample.py | 44 ++++ samples/sender_and_domain_samples.py | 59 +++++ samples/statistic_sample.py | 72 ++++++ 8 files changed, 417 insertions(+), 321 deletions(-) create mode 100644 samples/campaign_sample.py create mode 100644 samples/email_template_sample.py create mode 100644 samples/new_sample.py create mode 100644 samples/parse_api_sample.py create mode 100644 samples/segments_sample.py create mode 100644 samples/sender_and_domain_samples.py create mode 100644 samples/statistic_sample.py diff --git a/samples/campaign_sample.py b/samples/campaign_sample.py new file mode 100644 index 0000000..238c642 --- /dev/null +++ b/samples/campaign_sample.py @@ -0,0 +1,102 @@ +import json +import os + +from mailjet_rest import Client + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def create_a_campaign_draft(): + """POST https://api.mailjet.com/v3/REST/campaigndraft""" + data = { + "Locale": "en_US", + "Sender": "MisterMailjet", + "SenderEmail": "Mister@mailjet.com", + "Subject": "Greetings from Mailjet", + "ContactsListID": "$ID_CONTACTSLIST", + "Title": "Friday newsletter" + } + return mailjet30.campaigndraft.create(data=data) + + +def by_adding_custom_content(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/detailcontent""" + _id = "$draft_ID" + data = { + "Headers": "object", + "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", + "MJMLContent": "", + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" + } + return mailjet30.campaigndraft_detailcontent.create(id=_id, data=data) + + +def test_your_campaign(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/test""" + _id = "$draft_ID" + data = { + "Recipients": [ + { + "Email": "passenger@mailjet.com", + "Name": "Passenger 1" + } + ] + } + return mailjet30.campaigndraft_test.create(id=_id, data=data) + + +def schedule_the_sending(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/schedule""" + _id = "$draft_ID" + data = { + "Date": "2018-01-01T00:00:00" + } + return mailjet30.campaigndraft_schedule.create(id=_id, data=data) + + +def send_the_campaign_right_away(): + """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/send""" + _id = "$draft_ID" + return mailjet30.campaigndraft_send.create(id=_id) + + +def api_call_requirements(): + """POST https://api.mailjet.com/v3.1/send""" + data = { + "Messages": [ + { + "From": { + "Email": "pilot@mailjet.com", + "Name": "Mailjet Pilot" + }, + "To": [ + { + "Email": "passenger1@mailjet.com", + "Name": "passenger 1" + } + ], + "Subject": "Your email flight plan!", + "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", + "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with " + "you!", + "CustomCampaign": "SendAPI_campaign", + "DeduplicateCampaign": True + } + ] + } + return mailjet31.send.create(data=data) + + +if __name__ == "__main__": + result = create_a_campaign_draft() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 18c0ed7..646d50b 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -11,12 +11,12 @@ version="v3.1") -def create_contact(): +def create_a_contact(): """POST https://api.mailjet.com/v3/REST/contact""" data = { "IsExcludedFromCampaigns": "true", "Name": "New Contact", - "Email": "passenger@mailjet.com", + "Email": "passenger@mailjet.com" } return mailjet30.contact.create(data=data) @@ -45,16 +45,6 @@ def edit_contact_data(): return mailjet30.contactdata.update(id=_id, data=data) -def create_a_contact(): - """POST https://api.mailjet.com/v3/REST/contact""" - data = { - "IsExcludedFromCampaigns": "true", - "Name": "New Contact", - "Email": "passenger@mailjet.com" - } - return mailjet30.contact.create(data=data) - - def manage_contact_properties(): """POST https://api.mailjet.com/v3/REST/contactmetadata""" _id = "$contact_ID" @@ -90,7 +80,8 @@ def add_a_contact_to_a_contact_list(): def manage_the_subscription_status_of_an_existing_contact(): - """POST https://api.mailjet.com/v3/REST/contact/$contact_ID/managecontactslists""" + """POST https://api.mailjet.com/v3/REST/contact/$contact_ID + /managecontactslists""" _id = "$contact_ID" data = { "ContactsLists": [ @@ -116,7 +107,8 @@ def manage_the_subscription_status_of_an_existing_contact(): def manage_multiple_contacts_in_a_list(): - """POST https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" + """POST https://api.mailjet.com/v3/REST/contactslist/$list_ID + /managemanycontacts""" _id = "$list_ID" data = { "Action": "addnoforce", @@ -133,7 +125,8 @@ def manage_multiple_contacts_in_a_list(): def monitor_the_upload_job(): - """GET https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" + """GET https://api.mailjet.com/v3/REST/contactslist/$list_ID + /managemanycontacts""" _id = "$list_ID" return mailjet30.contactslist_managemanycontacts.get(id=_id) @@ -172,9 +165,13 @@ def manage_multiple_contacts_across_multiple_lists(): def upload_the_csv(): - """POST https://api.mailjet.com/v3/DATA/contactslist/$ID_CONTACTLIST/CSVData/text:plain""" + """POST https://api.mailjet.com/v3/DATA/contactslist + /$ID_CONTACTLIST/CSVData/text:plain""" f = open("./data.csv") - return mailjet30.contactslist_csvdata.create(id="$ID_CONTACTLIST", data=f.read()) + return mailjet30.contactslist_csvdata.create( + id="$ID_CONTACTLIST", + data=f.read(), + ) def import_csv_content_to_a_list(): @@ -195,8 +192,9 @@ def using_csv_with_atetime_contact_data(): "ContactsListID": "$ID_CONTACTLIST", "DataID": "$ID_DATA", "Method": "addnoforce", - "ImportOptions": "{\"DateTimeFormat\": \"yyyy/mm/dd\",\"TimezoneOffset\": 2,\"FieldNames\": [\"email\"," - "\"birthday\"]} " + "ImportOptions": "{\"DateTimeFormat\": \"yyyy/mm/dd\"," + "\"TimezoneOffset\": 2,\"FieldNames\": " + "[\"email\", \"birthday\"]} " } return mailjet30.csvimport.create(data=data) @@ -267,308 +265,6 @@ def delete_the_contact(): """DELETE https://api.mailjet.com/v4/contacts/{contact_ID}""" -################################################################ -# ### Email template. Template API -################################################################ - - -def create_a_template(): - """POST https://api.mailjet.com/v3/REST/template""" - data = { - "Author": "John Doe", - "Categories": "array", - "Copyright": "Mailjet", - "Description": "Used to send out promo codes.", - "EditMode": "1", - "IsStarred": "false", - "IsTextPartGenerationEnabled": "true", - "Locale": "en_US", - "Name": "Promo Codes", - "OwnerType": "user", - "Presets": "string", - "Purposes": "array" - } - return mailjet30.template.create(data=data) - - -def create_a_template_detailcontent(): - """POST https://api.mailjet.com/v3/REST/template/$template_ID/detailcontent""" - _id = "$template_ID" - data = { - "Headers": "", - "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", - "MJMLContent": "", - "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" - } - return mailjet30.template_detailcontent.create(id=_id, data=data) - - -def ise_templates_with_send_api(): - """POST https://api.mailjet.com/v3.1/send""" - data = { - "Messages": [ - { - "From": { - "Email": "pilot@mailjet.com", - "Name": "Mailjet Pilot" - }, - "To": [ - { - "Email": "passenger1@mailjet.com", - "Name": "passenger 1" - } - ], - "TemplateID": 1, - "TemplateLanguage": True, - "Subject": "Your email flight plan!" - } - ] - } - return mailjet31.send.create(data=data) - - -############################################################### -# ### Send campaigns using /campaigndraft -############################################################### - - -def create_a_campaign_draft(): - """POST https://api.mailjet.com/v3/REST/campaigndraft""" - data = { - "Locale": "en_US", - "Sender": "MisterMailjet", - "SenderEmail": "Mister@mailjet.com", - "Subject": "Greetings from Mailjet", - "ContactsListID": "$ID_CONTACTSLIST", - "Title": "Friday newsletter" - } - return mailjet30.campaigndraft.create(data=data) - - -def by_adding_custom_content(): - """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/detailcontent""" - _id = "$draft_ID" - data = { - "Headers": "object", - "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", - "MJMLContent": "", - "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" - } - return mailjet30.campaigndraft_detailcontent.create(id=_id, data=data) - - -def test_your_campaign(): - """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/test""" - _id = "$draft_ID" - data = { - "Recipients": [ - { - "Email": "passenger@mailjet.com", - "Name": "Passenger 1" - } - ] - } - return mailjet30.campaigndraft_test.create(id=_id, data=data) - - -def schedule_the_sending(): - """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/schedule""" - _id = "$draft_ID" - data = { - "Date": "2018-01-01T00:00:00" - } - return mailjet30.campaigndraft_schedule.create(id=_id, data=data) - - -def send_the_campaign_right_away(): - """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/send""" - _id = "$draft_ID" - return mailjet30.campaigndraft_send.create(id=_id) - - -################################################################################ -# ## Send campaigns using the Send API -################################################################################ - -def api_call_requirements(): - """POST https://api.mailjet.com/v3.1/send""" - data = { - "Messages": [ - { - "From": { - "Email": "pilot@mailjet.com", - "Name": "Mailjet Pilot" - }, - "To": [ - { - "Email": "passenger1@mailjet.com", - "Name": "passenger 1" - } - ], - "Subject": "Your email flight plan!", - "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", - "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with " - "you!", - "CustomCampaign": "SendAPI_campaign", - "DeduplicateCampaign": True - } - ] - } - return mailjet31.send.create(data=data) - - -######################################################################################## -# ## Segmentation -######################################################################################## - - -def create_your_segment(): - """POST https://api.mailjet.com/v3/REST/contactfilter""" - data = { - "Description": "Will send only to contacts under 35 years of age.", - "Expression": "(age<35)", - "Name": "Customers under 35" - } - return mailjet30.contactfilter.create(data=data) - - -def create_a_campaign_with_a_segmentation_filter(): - """POST https://api.mailjet.com/v3/REST/newsletter""" - data = { - "Title": "Mailjet greets every contact over 40", - "Locale": "en_US", - "Sender": "MisterMailjet", - "SenderEmail": "Mister@mailjet.com", - "Subject": "Greetings from Mailjet", - "ContactsListID": "$ID_CONTACTLIST", - "SegmentationID": "$ID_CONTACT_FILTER" - } - return mailjet30.newsletter.create(data=data) - - -############################################################## -# ## Event tracking via webhooks -############################################################### - - -################################################################ -# ## Statistics -############################################################### - - -def event_based_vs_message_based_stats_timing(): - """GET https://api.mailjet.com/v3/REST/statcounters""" - filters = { - "SourceId": "$Campaign_ID", - "CounterSource": "Campaign", - "CounterTiming": "Message", - "CounterResolution": "Lifetime" - } - return mailjet30.statcounters.get(filters=filters) - - -def view_the_spread_of_events_over_time(): - """GET https://api.mailjet.com/v3/REST/statcounters""" - filters = { - "SourceId": "$Campaign_ID", - "CounterSource": "Campaign", - "CounterTiming": "Event", - "CounterResolution": "Day", - "FromTS": "123", - "ToTS": "456" - } - return mailjet30.statcounters.get(filters=filters) - - -def statistics_for_specific_recipient(): - """GET https://api.mailjet.com/v3/REST/contactstatistics""" - return mailjet30.contactstatistics.get() - - -def stats_for_clicked_links(): - """GET https://api.mailjet.com/v3/REST/statistics/link-click""" - filters = { - "CampaignId": "$Campaign_ID" - } - return mailjet30.statistics_linkClick.get(filters=filters) # TODO - - -def mailbox_provider_statistics(): - """GET https://api.mailjet.com/v3/REST/statistics/recipient-esp""" - filters = { - "CampaignId": "$Campaign_ID" - } - return mailjet30.statistics_recipientEsp.get(filters=filters) # TODO - - -def geographical_statistics(): - """GET https://api.mailjet.com/v3/REST/geostatistics""" - return mailjet30.geostatistics.get() - - -############################################################################ -# ## Parse API: inbound emails -############################################################################ - - -def basic_setup(): - """POST https://api.mailjet.com/v3/REST/parseroute""" - data = { - "Url": "https://www.mydomain.com/mj_parse.php" - } - return mailjet30.parseroute.create(data=data) - - -#################################################################################### -# ## Senders and Domains -################################################################################### - - -def validate_an_entire_domain(): - """GET https: // api.mailjet.com / v3 / REST / dns""" - _id = "$dns_ID" - return mailjet30.dns.get(id=_id) - - -def do_an_immediate_check_via_a_post(): - """POST https://api.mailjet.com/v3/REST/dns/$dns_ID/check""" - _id = "$dns_ID" - return mailjet30.dns_check.create(id=_id) - - -def host_a_text_file(): - """GET https://api.mailjet.com/v3/REST/sender""" - _id = "$sender_ID" - return mailjet30.sender.get(id=_id) - - -def validation_by_doing_a_post(): - """POST https://api.mailjet.com/v3/REST/sender/$sender_ID/validate""" - _id = "$sender_ID" - return mailjet30.sender_validate.create(id=_id) - - -def spf_and_dkim_validation(): - """ET https://api.mailjet.com/v3/REST/dns""" - _id = "$dns_ID" - return mailjet30.dns.get(id=_id) - - -def use_a_sender_on_all_api_keys(): - """POST https://api.mailjet.com/v3/REST/metasender""" - data = { - "Description": "Metasender 1 - used for Promo emails", - "Email": "pilot@mailjet.com" - } - return mailjet30.metasender.create(data=data) - - -################################################################### -# ## Language libraries -################################################################### - - if __name__ == "__main__": result = edit_contact_data() print(result.status_code) diff --git a/samples/email_template_sample.py b/samples/email_template_sample.py new file mode 100644 index 0000000..a818549 --- /dev/null +++ b/samples/email_template_sample.py @@ -0,0 +1,75 @@ +import json +import os + +from mailjet_rest import Client + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def create_a_template(): + """POST https://api.mailjet.com/v3/REST/template""" + data = { + "Author": "John Doe", + "Categories": "array", + "Copyright": "Mailjet", + "Description": "Used to send out promo codes.", + "EditMode": "1", + "IsStarred": "false", + "IsTextPartGenerationEnabled": "true", + "Locale": "en_US", + "Name": "Promo Codes", + "OwnerType": "user", + "Presets": "string", + "Purposes": "array" + } + return mailjet30.template.create(data=data) + + +def create_a_template_detailcontent(): + """POST https://api.mailjet.com/v3/REST/template/$template_ID/detailcontent""" + _id = "$template_ID" + data = { + "Headers": "", + "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", + "MJMLContent": "", + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" + } + return mailjet30.template_detailcontent.create(id=_id, data=data) + + +def use_templates_with_send_api(): + """POST https://api.mailjet.com/v3.1/send""" + data = { + "Messages": [ + { + "From": { + "Email": "pilot@mailjet.com", + "Name": "Mailjet Pilot" + }, + "To": [ + { + "Email": "passenger1@mailjet.com", + "Name": "passenger 1" + } + ], + "TemplateID": 1, + "TemplateLanguage": True, + "Subject": "Your email flight plan!" + } + ] + } + return mailjet31.send.create(data=data) + + +if __name__ == "__main__": + result = create_a_template() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) diff --git a/samples/new_sample.py b/samples/new_sample.py new file mode 100644 index 0000000..ee05062 --- /dev/null +++ b/samples/new_sample.py @@ -0,0 +1,20 @@ +import json +import os + +from mailjet_rest import Client + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +if __name__ == "__main__": + result = edit_contact_data() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) diff --git a/samples/parse_api_sample.py b/samples/parse_api_sample.py new file mode 100644 index 0000000..0866c2b --- /dev/null +++ b/samples/parse_api_sample.py @@ -0,0 +1,28 @@ +import json +import os + +from mailjet_rest import Client + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def basic_setup(): + """POST https://api.mailjet.com/v3/REST/parseroute""" + data = { + "Url": "https://www.mydomain.com/mj_parse.php" + } + return mailjet30.parseroute.create(data=data) + + +if __name__ == "__main__": + result = basic_setup() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) diff --git a/samples/segments_sample.py b/samples/segments_sample.py new file mode 100644 index 0000000..66a5d98 --- /dev/null +++ b/samples/segments_sample.py @@ -0,0 +1,44 @@ +import json +import os + +from mailjet_rest import Client + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def create_your_segment(): + """POST https://api.mailjet.com/v3/REST/contactfilter""" + data = { + "Description": "Will send only to contacts under 35 years of age.", + "Expression": "(age<35)", + "Name": "Customers under 35" + } + return mailjet30.contactfilter.create(data=data) + + +def create_a_campaign_with_a_segmentation_filter(): + """POST https://api.mailjet.com/v3/REST/newsletter""" + data = { + "Title": "Mailjet greets every contact over 40", + "Locale": "en_US", + "Sender": "MisterMailjet", + "SenderEmail": "Mister@mailjet.com", + "Subject": "Greetings from Mailjet", + "ContactsListID": "$ID_CONTACTLIST", + "SegmentationID": "$ID_CONTACT_FILTER" + } + return mailjet30.newsletter.create(data=data) + + +if __name__ == "__main__": + result = create_your_segment() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) diff --git a/samples/sender_and_domain_samples.py b/samples/sender_and_domain_samples.py new file mode 100644 index 0000000..37c1cee --- /dev/null +++ b/samples/sender_and_domain_samples.py @@ -0,0 +1,59 @@ +import json +import os + +from mailjet_rest import Client + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def validate_an_entire_domain(): + """GET https: // api.mailjet.com / v3 / REST / dns""" + _id = "$dns_ID" + return mailjet30.dns.get(id=_id) + + +def do_an_immediate_check_via_a_post(): + """POST https://api.mailjet.com/v3/REST/dns/$dns_ID/check""" + _id = "$dns_ID" + return mailjet30.dns_check.create(id=_id) + + +def host_a_text_file(): + """GET https://api.mailjet.com/v3/REST/sender""" + _id = "$sender_ID" + return mailjet30.sender.get(id=_id) + + +def validation_by_doing_a_post(): + """POST https://api.mailjet.com/v3/REST/sender/$sender_ID/validate""" + _id = "$sender_ID" + return mailjet30.sender_validate.create(id=_id) + + +def spf_and_dkim_validation(): + """ET https://api.mailjet.com/v3/REST/dns""" + _id = "$dns_ID" + return mailjet30.dns.get(id=_id) + + +def use_a_sender_on_all_api_keys(): + """POST https://api.mailjet.com/v3/REST/metasender""" + data = { + "Description": "Metasender 1 - used for Promo emails", + "Email": "pilot@mailjet.com" + } + return mailjet30.metasender.create(data=data) + + +if __name__ == "__main__": + result = validate_an_entire_domain() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) diff --git a/samples/statistic_sample.py b/samples/statistic_sample.py new file mode 100644 index 0000000..7c3b094 --- /dev/null +++ b/samples/statistic_sample.py @@ -0,0 +1,72 @@ +import json +import os + +from mailjet_rest import Client + +mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"])) + +mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1") + + +def event_based_vs_message_based_stats_timing(): + """GET https://api.mailjet.com/v3/REST/statcounters""" + filters = { + "SourceId": "$Campaign_ID", + "CounterSource": "Campaign", + "CounterTiming": "Message", + "CounterResolution": "Lifetime" + } + return mailjet30.statcounters.get(filters=filters) + + +def view_the_spread_of_events_over_time(): + """GET https://api.mailjet.com/v3/REST/statcounters""" + filters = { + "SourceId": "$Campaign_ID", + "CounterSource": "Campaign", + "CounterTiming": "Event", + "CounterResolution": "Day", + "FromTS": "123", + "ToTS": "456" + } + return mailjet30.statcounters.get(filters=filters) + + +def statistics_for_specific_recipient(): + """GET https://api.mailjet.com/v3/REST/contactstatistics""" + return mailjet30.contactstatistics.get() + + +def stats_for_clicked_links(): + """GET https://api.mailjet.com/v3/REST/statistics/link-click""" + filters = { + "CampaignId": "$Campaign_ID" + } + # TODO Outdated in doc. + return mailjet30.statistics_linkClick.get(filters=filters) + + +def mailbox_provider_statistics(): + """GET https://api.mailjet.com/v3/REST/statistics/recipient-esp""" + filters = { + "CampaignId": "$Campaign_ID" + } + # TODO Outdated in doc. + return mailjet30.statistics_recipientEsp.get(filters=filters) + + +def geographical_statistics(): + """GET https://api.mailjet.com/v3/REST/geostatistics""" + return mailjet30.geostatistics.get() + + +if __name__ == "__main__": + result = geographical_statistics() + print(result.status_code) + try: + print(json.dumps(result.json(), indent=4)) + except json.decoder.JSONDecodeError: + print(result.text) From 053346a76ce534679de9858c97b5d7cb72b43663 Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Mon, 25 Jul 2022 19:17:10 +0300 Subject: [PATCH 34/87] Remove TODOs --- samples/statistic_sample.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/statistic_sample.py b/samples/statistic_sample.py index 7c3b094..70dcdfb 100644 --- a/samples/statistic_sample.py +++ b/samples/statistic_sample.py @@ -45,7 +45,6 @@ def stats_for_clicked_links(): filters = { "CampaignId": "$Campaign_ID" } - # TODO Outdated in doc. return mailjet30.statistics_linkClick.get(filters=filters) @@ -54,7 +53,6 @@ def mailbox_provider_statistics(): filters = { "CampaignId": "$Campaign_ID" } - # TODO Outdated in doc. return mailjet30.statistics_recipientEsp.get(filters=filters) From 4686b2822afe809dbd7ab666614d988c36cd0a6a Mon Sep 17 00:00:00 2001 From: TheFlighteur Date: Sun, 19 Feb 2023 20:57:06 +0100 Subject: [PATCH 35/87] Corrected url building bug Corrected a bug affecting url building when accessing api with a ressource_id and an action_id. Exemple of bug : URL built before correction : MAILJET_API_URL/endpoint/ressource_id/action_id URL built after correction : MAILJET_API_URL/contact/action_id/ressource_id --- mailjet_rest/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 036dd56..2dee288 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -139,13 +139,12 @@ def build_headers(resource, action=None, extra_headers=None): def build_url(url, method, action=None, resource_id=None, action_id=None): - if resource_id: - url += '/%s' % str(resource_id) if action: url += '/%s' % action if action_id: url += '/{}'.format(action_id) - + if resource_id: + url += '/%s' % str(resource_id) return url From de522ab1127e28d9a38cc3833b01aadb771761d1 Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Sun, 11 Jun 2023 19:18:17 +0300 Subject: [PATCH 36/87] Add check disabling --- mailjet_rest/client.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index d743d0c..3e8546d 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -64,14 +64,20 @@ def get_many(self, filters=None, action_id=None, **kwargs): def get(self, id=None, filters=None, action_id=None, **kwargs): return self._get(id=id, filters=filters, action_id=action_id, **kwargs) - def create(self, data=None, filters=None, id=None, action_id=None, **kwargs): + def create(self, data=None, filters=None, id=None, action_id=None, ensure_ascii=True, data_encoding="utf-8", **kwargs): if self.headers['Content-type'] == 'application/json': - data = json.dumps(data) + if ensure_ascii: + data = json.dumps(data) + else: + data = json.dumps(data, ensure_ascii=False).encode(data_encoding) return api_call(self._auth, 'post', self._url, headers=self.headers, resource_id=id, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) - def update(self, id, data, filters=None, action_id=None, **kwargs): + def update(self, id, data, filters=None, action_id=None, ensure_ascii=True, data_encoding="utf-8", **kwargs): if self.headers['Content-type'] == 'application/json': - data = json.dumps(data) + if ensure_ascii: + data = json.dumps(data) + else: + data = json.dumps(data, ensure_ascii=False).encode(data_encoding) return api_call(self._auth, 'put', self._url, resource_id=id, headers=self.headers, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) def delete(self, id, **kwargs): From 095179e5a2d1df78d4763f8f78ea7a99dca295ea Mon Sep 17 00:00:00 2001 From: Danyil Nefodov <48028650+DanyilNefodov@users.noreply.github.com> Date: Sun, 11 Jun 2023 19:35:43 +0300 Subject: [PATCH 37/87] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e7fbdc..5b956d8 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ This library officially supports the following Python versions: - v2.7 - v3.5 - - v3.6 + - v3.6+ ## Installation From 2307e27e07300fde364e6915c0cdf29a346dd2ca Mon Sep 17 00:00:00 2001 From: Danyil Nefodov Date: Mon, 28 Aug 2023 19:41:52 +0300 Subject: [PATCH 38/87] Add dependabot config --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..624cc35 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + time: "00:00" + groups: + python-packages: + patterns: + - "*" From 7396a6da8f5faea0dd57aa27cdb2d9d7179d6f82 Mon Sep 17 00:00:00 2001 From: Danyil Nefodov <48028650+DanyilNefodov@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:04:45 +0300 Subject: [PATCH 39/87] Update dependabot.yml --- .github/dependabot.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 624cc35..73961f1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,7 @@ updates: - package-ecosystem: pip directory: "/" schedule: - interval: daily - time: "00:00" + interval: "weekly" groups: python-packages: patterns: From 342454e522930bb52c741bc7e428089d9a6ceef9 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:34:13 +0300 Subject: [PATCH 40/87] Update README.md, fix the license name in setup.py --- README.md | 91 ++++++++++++++++++++++++------------------------------- setup.py | 2 +- 2 files changed, 41 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 5b956d8..f0d8a63 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [mailjet]:(http://www.mailjet.com/) -[api_credential]: https://app.mailjet.com/account/api_keys +[api_credential]: https://app.mailjet.com/account/apikeys [doc]: http://dev.mailjet.com/guides/?python# [api_doc]: https://github.com/mailjet/api-documentation @@ -73,10 +73,10 @@ from mailjet_rest import Client import os # Get your environment Mailjet keys -API_KEY = os.environ['MJ_APIKEY_PUBLIC'] -API_SECRET = os.environ['MJ_APIKEY_PRIVATE'] +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] -mailjet = Client(auth=(API_KEY, API_SECRET)) +mailjet = Client(auth=(api_key, api_secret)) ``` ## Make your first call @@ -88,29 +88,18 @@ from mailjet_rest import Client import os api_key = os.environ['MJ_APIKEY_PUBLIC'] api_secret = os.environ['MJ_APIKEY_PRIVATE'] -mailjet = Client(auth=(api_key, api_secret), version='v3.1') +mailjet = Client(auth=(api_key, api_secret)) data = { - 'Messages': [ - { - "From": { - "Email": "$SENDER_EMAIL", - "Name": "Me" - }, - "To": [ - { - "Email": "$RECIPIENT_EMAIL", - "Name": "You" - } - ], - "Subject": "My first Mailjet Email!", - "TextPart": "Greetings from Mailjet!", - "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with you!" - } - ] + 'FromEmail': '$SENDER_EMAIL', + 'FromName': '$SENDER_NAME', + 'Subject': 'Your email flight plan!', + 'Text-part': 'Dear passenger, welcome to Mailjet! May the delivery force be with you!', + 'Html-part': '

Dear passenger, welcome to Mailjet!
May the delivery force be with you!', + 'Recipients': [{'Email': '$RECIPIENT_EMAIL'}] } result = mailjet.send.create(data=data) -print result.status_code -print result.json() +print(result.status_code) +print(result.json()) ``` ## Client / Call Configuration Specifics @@ -131,13 +120,13 @@ from mailjet_rest import Client import os # Get your environment Mailjet keys -API_KEY = os.environ['MJ_APIKEY_PUBLIC'] -API_SECRET = os.environ['MJ_APIKEY_PRIVATE'] +api_key = os.environ['MJ_APIKEY_PUBLIC'] +api_secret = os.environ['MJ_APIKEY_PRIVATE'] -mailjet = Client(auth=(API_KEY, API_SECRET), version='v3.1') +mailjet = Client(auth=(api_key, api_secret), version='v3.1') ``` -For additional information refer to our [API Reference](https://dev.preprod.mailjet.com/reference/overview/versioning/). +For additional information refer to our [API Reference](https://dev.mailjet.com/reference/overview/versioning/). ### Base URL @@ -161,8 +150,8 @@ filters = { 'CampaignId': 'xxxxxxx' } result = mailjet.statistics_linkClick.get(filters=filters) -print result.status_code -print result.json() +print(result.status_code) +print(result.json()) ``` ## Request examples @@ -184,8 +173,8 @@ data = { 'Email': 'Mister@mailjet.com' } result = mailjet.contact.create(data=data) -print result.status_code -print result.json() +print(result.status_code) +print(result.json()) ``` #### Using actions @@ -213,8 +202,8 @@ data = { ] } result = mailjet.contact_managecontactslists.create(id=id, data=data) -print result.status_code -print result.json() +print(result.status_code) +print(result.json()) ``` ### GET Request @@ -231,8 +220,8 @@ api_key = os.environ['MJ_APIKEY_PUBLIC'] api_secret = os.environ['MJ_APIKEY_PRIVATE'] mailjet = Client(auth=(api_key, api_secret)) result = mailjet.contact.get() -print result.status_code -print result.json() +print(result.status_code) +print(result.json()) ``` #### Using filtering @@ -247,11 +236,11 @@ api_key = os.environ['MJ_APIKEY_PUBLIC'] api_secret = os.environ['MJ_APIKEY_PRIVATE'] mailjet = Client(auth=(api_key, api_secret)) filters = { - 'IsExcludedFromCampaigns': false, + 'IsExcludedFromCampaigns': 'false', } result = mailjet.contact.get(filters=filters) -print result.status_code -print result.json() +print(result.status_code) +print(result.json()) ``` #### Using pagination @@ -291,10 +280,10 @@ import os api_key = os.environ['MJ_APIKEY_PUBLIC'] api_secret = os.environ['MJ_APIKEY_PRIVATE'] mailjet = Client(auth=(api_key, api_secret)) -id = 'Contact_ID' -result = mailjet.contact.get(id=id) -print result.status_code -print result.json() +id_ = 'Contact_ID' +result = mailjet.contact.get(id=id_) +print(result.status_code) +print(result.json()) ``` ### PUT request @@ -312,7 +301,7 @@ import os api_key = os.environ['MJ_APIKEY_PUBLIC'] api_secret = os.environ['MJ_APIKEY_PRIVATE'] mailjet = Client(auth=(api_key, api_secret)) -id = '$CONTACT_ID' +id_ = '$CONTACT_ID' data = { 'Data': [ { @@ -325,9 +314,9 @@ data = { } ] } -result = mailjet.contactdata.update(id=id, data=data) -print result.status_code -print result.json() +result = mailjet.contactdata.update(id=id_, data=data) +print(result.status_code) +print(result.json()) ``` ### DELETE request @@ -345,10 +334,10 @@ import os api_key = os.environ['MJ_APIKEY_PUBLIC'] api_secret = os.environ['MJ_APIKEY_PRIVATE'] mailjet = Client(auth=(api_key, api_secret)) -id = 'Template_ID' -result = mailjet.template.delete(id=id) -print result.status_code -print result.json() +id_ = 'Template_ID' +result = mailjet.template.delete(id=id_) +print(result.status_code) +print(result.json()) ``` ## Contribute diff --git a/setup.py b/setup.py index 9762f34..711b4a3 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ classifiers=['Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License (GPL)', + 'License :: The MIT License (MIT)', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', From d664fe3e28069fb95f4137a28b0d90bdb10000eb Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:31:32 +0300 Subject: [PATCH 41/87] Fix license name --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 711b4a3..f3ff52a 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ classifiers=['Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', - 'License :: The MIT License (MIT)', + 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', From 07fe84294e4cfbc74ae714a000a9b792a33d8cc7 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:55:59 +0300 Subject: [PATCH 42/87] style: Use double quotes --- mailjet_rest/client.py | 86 +++++++++++++++++------------------ mailjet_rest/utils/version.py | 6 +-- setup.py | 44 +++++++++--------- test.py | 66 +++++++++++++-------------- 4 files changed, 101 insertions(+), 101 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 9da068a..a577b80 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -16,14 +16,14 @@ def prepare_url(key): """Replaces capital letters to lower one with dash prefix.""" char_elem = key.group(0) if char_elem.isupper(): - return '-' + char_elem.lower() + return "-" + char_elem.lower() class Config(object): - DEFAULT_API_URL = 'https://api.mailjet.com/' - API_REF = 'http://dev.mailjet.com/email-api/v3/' - version = 'v3' - user_agent = 'mailjet-apiv3-python/v' + get_version() + DEFAULT_API_URL = "https://api.mailjet.com/" + API_REF = "http://dev.mailjet.com/email-api/v3/" + version = "v3" + user_agent = "mailjet-apiv3-python/v" + get_version() def __init__(self, version=None, api_url=None): if version is not None: @@ -33,17 +33,17 @@ def __init__(self, version=None, api_url=None): def __getitem__(self, key): # Append version to URL. # Forward slash is ignored if present in self.version. - url = urljoin(self.api_url, self.version + '/') - headers = {'Content-type': 'application/json', 'User-agent': self.user_agent} - if key.lower() == 'contactslist_csvdata': - url = urljoin(url, 'DATA/') - headers['Content-type'] = 'text/plain' - elif key.lower() == 'batchjob_csverror': - url = urljoin(url, 'DATA/') - headers['Content-type'] = 'text/csv' - elif key.lower() != 'send' and self.version != 'v4': - url = urljoin(url, 'REST/') - url = url + key.split('_')[0].lower() + url = urljoin(self.api_url, self.version + "/") + headers = {"Content-type": "application/json", "User-agent": self.user_agent} + if key.lower() == "contactslist_csvdata": + url = urljoin(url, "DATA/") + headers["Content-type"] = "text/plain" + elif key.lower() == "batchjob_csverror": + url = urljoin(url, "DATA/") + headers["Content-type"] = "text/csv" + elif key.lower() != "send" and self.version != "v4": + url = urljoin(url, "REST/") + url = url + key.split("_")[0].lower() return url, headers @@ -56,7 +56,7 @@ def __doc__(self): return self._doc def _get(self, filters=None, action_id=None, id=None, **kwargs): - return api_call(self._auth, 'get', self._url, headers=self.headers, action=self.action, action_id=action_id, filters=filters, resource_id=id, **kwargs) + return api_call(self._auth, "get", self._url, headers=self.headers, action=self.action, action_id=action_id, filters=filters, resource_id=id, **kwargs) def get_many(self, filters=None, action_id=None, **kwargs): return self._get(filters=filters, action_id=action_id **kwargs) @@ -65,46 +65,46 @@ def get(self, id=None, filters=None, action_id=None, **kwargs): return self._get(id=id, filters=filters, action_id=action_id, **kwargs) def create(self, data=None, filters=None, id=None, action_id=None, ensure_ascii=True, data_encoding="utf-8", **kwargs): - if self.headers['Content-type'] == 'application/json': + if self.headers["Content-type"] == "application/json": if ensure_ascii: data = json.dumps(data) else: data = json.dumps(data, ensure_ascii=False).encode(data_encoding) - return api_call(self._auth, 'post', self._url, headers=self.headers, resource_id=id, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) + return api_call(self._auth, "post", self._url, headers=self.headers, resource_id=id, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) def update(self, id, data, filters=None, action_id=None, ensure_ascii=True, data_encoding="utf-8", **kwargs): - if self.headers['Content-type'] == 'application/json': + if self.headers["Content-type"] == "application/json": if ensure_ascii: data = json.dumps(data) else: data = json.dumps(data, ensure_ascii=False).encode(data_encoding) - return api_call(self._auth, 'put', self._url, resource_id=id, headers=self.headers, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) + return api_call(self._auth, "put", self._url, resource_id=id, headers=self.headers, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) def delete(self, id, **kwargs): - return api_call(self._auth, 'delete', self._url, action=self.action, headers=self.headers, resource_id=id, **kwargs) + return api_call(self._auth, "delete", self._url, action=self.action, headers=self.headers, resource_id=id, **kwargs) class Client(object): def __init__(self, auth=None, **kwargs): self.auth = auth - version = kwargs.get('version', None) - api_url = kwargs.get('api_url', None) + version = kwargs.get("version", None) + api_url = kwargs.get("api_url", None) self.config = Config(version=version, api_url=api_url) def __getattr__(self, name): name = re.sub(r"[A-Z]", prepare_url, name) - split = name.split('_') + split = name.split("_") #identify the resource fname = split[0] action = None if (len(split) > 1): #identify the sub resource (action) action = split[1] - if action == 'csvdata': - action = 'csvdata/text:plain' - if action == 'csverror': - action = 'csverror/text:csv' + if action == "csvdata": + action = "csvdata/text:plain" + if action == "csverror": + action = "csverror/text:csv" url, headers = self.config[name] return type(fname, (Endpoint,), {})(url=url, headers=headers, action=action, auth=self.auth) @@ -131,12 +131,12 @@ def api_call(auth, method, url, headers, data=None, filters=None, resource_id=No def build_headers(resource, action=None, extra_headers=None): - headers = {'Content-type': 'application/json'} + headers = {"Content-type": "application/json"} - if resource.lower() == 'contactslist' and action.lower() == 'csvdata': - headers = {'Content-type': 'text/plain'} - elif resource.lower() == 'batchjob' and action.lower() == 'csverror': - headers = {'Content-type': 'text/csv'} + if resource.lower() == "contactslist" and action.lower() == "csvdata": + headers = {"Content-type": "text/plain"} + elif resource.lower() == "batchjob" and action.lower() == "csverror": + headers = {"Content-type": "text/csv"} if extra_headers: headers.update(extra_headers) @@ -146,11 +146,11 @@ def build_headers(resource, action=None, extra_headers=None): def build_url(url, method, action=None, resource_id=None, action_id=None): if action: - url += '/%s' % action + url += "/%s" % action if action_id: - url += '/{}'.format(action_id) + url += "/{}".format(action_id) if resource_id: - url += '/%s' % str(resource_id) + url += "/%s" % str(resource_id) return url @@ -158,13 +158,13 @@ def parse_response(response, debug=False): data = response.json() if debug: - logging.debug('REQUEST: %s' % response.request.url) - logging.debug('REQUEST_HEADERS: %s' % response.request.headers) - logging.debug('REQUEST_CONTENT: %s' % response.request.body) + logging.debug("REQUEST: %s" % response.request.url) + logging.debug("REQUEST_HEADERS: %s" % response.request.headers) + logging.debug("REQUEST_CONTENT: %s" % response.request.body) - logging.debug('RESPONSE: %s' % response.content) - logging.debug('RESP_HEADERS: %s' % response.headers) - logging.debug('RESP_CODE: %s' % response.status_code) + logging.debug("RESPONSE: %s" % response.content) + logging.debug("RESP_HEADERS: %s" % response.headers) + logging.debug("RESP_CODE: %s" % response.status_code) return data diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 7833d6e..7b6a143 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -2,12 +2,12 @@ def get_version(version=None): - ''' + """ Calculate package version based on a 3 item tuple. In addition verify that the tuple contains 3 items - ''' + """ if version is None: version = VERSION else: assert len(version) == 3 - return '{0}.{1}.{2}'.format(*(x for x in version)) + return "{0}.{1}.{2}".format(*(x for x in version)) diff --git a/setup.py b/setup.py index 9762f34..4572d83 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages, setup HERE = os.path.abspath(os.path.dirname(__file__)) -PACKAGE_NAME = 'mailjet_rest' +PACKAGE_NAME = "mailjet_rest" with open("README.md", "r") as fh: long_description = fh.read() @@ -15,32 +15,32 @@ setup( name=PACKAGE_NAME, - author='starenka', - author_email='starenka0@gmail.com', - maintainer='Mailjet', - maintainer_email='api@mailjet.com', + author="starenka", + author_email="starenka0@gmail.com", + maintainer="Mailjet", + maintainer_email="api@mailjet.com", version="latest", - download_url='https://github.com/mailjet/mailjet-apiv3-python/releases/' + version, - url='https://github.com/mailjet/mailjet-apiv3-python', - description=('Mailjet V3 API wrapper'), + download_url="https://github.com/mailjet/mailjet-apiv3-python/releases/" + version, + url="https://github.com/mailjet/mailjet-apiv3-python", + description=("Mailjet V3 API wrapper"), long_description=long_description, long_description_content_type="text/markdown", - classifiers=['Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License (GPL)', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Utilities'], - license='MIT', - keywords='Mailjet API v3 / v3.1 Python Wrapper', + classifiers=["Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Topic :: Utilities"], + license="MIT", + keywords="Mailjet API v3 / v3.1 Python Wrapper", include_package_data=True, - install_requires=['requests>=2.4.3'], - tests_require=['unittest'], + install_requires=["requests>=2.4.3"], + tests_require=["unittest"], entry_points={}, packages=find_packages(), ) diff --git a/test.py b/test.py index 3bb3ecc..c46683f 100644 --- a/test.py +++ b/test.py @@ -9,50 +9,50 @@ class TestSuite(unittest.TestCase): def setUp(self): self.auth = ( - os.environ['MJ_APIKEY_PUBLIC'], - os.environ['MJ_APIKEY_PRIVATE'] + os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"] ) self.client = Client(auth=self.auth) def test_get_no_param(self): result = self.client.contact.get().json() - self.assertTrue(('Data' in result and 'Count' in result)) + self.assertTrue(("Data" in result and "Count" in result)) def test_get_valid_params(self): - result = self.client.contact.get(filters={'limit': 2}).json() - self.assertTrue(result['Count'] >= 0 or result['Count'] <= 2) + result = self.client.contact.get(filters={"limit": 2}).json() + self.assertTrue(result["Count"] >= 0 or result["Count"] <= 2) def test_get_invalid_parameters(self): # invalid parameters are ignored - result = self.client.contact.get(filters={'invalid': 'false'}).json() - self.assertTrue('Count' in result) + result = self.client.contact.get(filters={"invalid": "false"}).json() + self.assertTrue("Count" in result) def test_get_with_data(self): # it shouldn't use data - result = self.client.contact.get(data={'Email': 'api@mailjet.com'}) + result = self.client.contact.get(data={"Email": "api@mailjet.com"}) self.assertTrue(result.status_code == 200) def test_get_with_action(self): - get_contact = self.client.contact.get(filters={'limit': 1}).json() - if get_contact['Count'] != 0: - contact_id = get_contact['Data'][0]['ID'] + get_contact = self.client.contact.get(filters={"limit": 1}).json() + if get_contact["Count"] != 0: + contact_id = get_contact["Data"][0]["ID"] else: - contact_random_email = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + '@mailjet.com' - post_contact = self.client.contact.create(data={'Email': contact_random_email}) + contact_random_email = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + post_contact = self.client.contact.create(data={"Email": contact_random_email}) self.assertTrue(post_contact.status_code == 201) - contact_id = post_contact.json()['Data'][0]['ID'] + contact_id = post_contact.json()["Data"][0]["ID"] - get_contact_list = self.client.contactslist.get(filters={'limit': 1}).json() - if get_contact_list['Count'] != 0: - list_id = get_contact_list['Data'][0]['ID'] + get_contact_list = self.client.contactslist.get(filters={"limit": 1}).json() + if get_contact_list["Count"] != 0: + list_id = get_contact_list["Data"][0]["ID"] else: - contact_list_random_name = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + '@mailjet.com' - post_contact_list = self.client.contactslist.create(data={'Name': contact_list_random_name}) + contact_list_random_name = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + post_contact_list = self.client.contactslist.create(data={"Name": contact_list_random_name}) self.assertTrue(post_contact_list.status_code == 201) - list_id = post_contact_list.json()['Data'][0]['ID'] + list_id = post_contact_list.json()["Data"][0]["ID"] data = { - 'ContactsLists': [ + "ContactsLists": [ { "ListID": list_id, "Action": "addnoforce" @@ -63,35 +63,35 @@ def test_get_with_action(self): self.assertTrue(result_add_list.status_code == 201) result = self.client.contact_getcontactslists.get(contact_id).json() - self.assertTrue('Count' in result) + self.assertTrue("Count" in result) def test_get_with_id_filter(self): - result_contact = self.client.contact.get(filters={'limit': 1}).json() - result_contact_with_id = self.client.contact.get(filter={'Email': result_contact['Data'][0]['Email']}).json() - self.assertTrue(result_contact_with_id['Data'][0]['Email'] == result_contact['Data'][0]['Email']) + result_contact = self.client.contact.get(filters={"limit": 1}).json() + result_contact_with_id = self.client.contact.get(filter={"Email": result_contact["Data"][0]["Email"]}).json() + self.assertTrue(result_contact_with_id["Data"][0]["Email"] == result_contact["Data"][0]["Email"]) def test_post_with_no_param(self): result = self.client.sender.create(data={}).json() - self.assertTrue('StatusCode' in result and result['StatusCode'] == 400) + self.assertTrue("StatusCode" in result and result["StatusCode"] == 400) def test_client_custom_version(self): self.client = Client( auth=self.auth, - version='v3.1' + version="v3.1" ) - self.assertEqual(self.client.config.version, 'v3.1') + self.assertEqual(self.client.config.version, "v3.1") self.assertEqual( - self.client.config['send'][0], - 'https://api.mailjet.com/v3.1/send' + self.client.config["send"][0], + "https://api.mailjet.com/v3.1/send" ) def test_user_agent(self): self.client = Client( auth=self.auth, - version='v3.1' + version="v3.1" ) - self.assertEqual(self.client.config.user_agent, 'mailjet-apiv3-python/v1.3.3') + self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.3") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 3fe7c50c8120db3210328c096180f6565bc0fb29 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:12:07 +0300 Subject: [PATCH 43/87] Add pyproject.toml, sort imports, fix the exception's usage --- mailjet_rest/__init__.py | 1 + mailjet_rest/client.py | 4 +- pyproject.toml | 175 +++++++++++++++++++++++++++ samples/campaign_sample.py | 1 + samples/contacts_sample.py | 1 + samples/email_template_sample.py | 1 + samples/new_sample.py | 4 + samples/parse_api_sample.py | 1 + samples/segments_sample.py | 1 + samples/sender_and_domain_samples.py | 1 + samples/statistic_sample.py | 1 + setup.py | 5 +- test.py | 5 +- 13 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 pyproject.toml diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index 01baf9c..09da870 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -3,6 +3,7 @@ from mailjet_rest.client import Client from mailjet_rest.utils.version import get_version + __version__ = get_version() __all__ = (Client, get_version) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index a577b80..4a18b39 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -7,8 +7,10 @@ import requests from requests.compat import urljoin + from .utils.version import get_version + requests.packages.urllib3.disable_warnings() @@ -126,7 +128,7 @@ def api_call(auth, method, url, headers, data=None, filters=None, resource_id=No raise TimeoutError except requests.RequestException as e: raise ApiError(e) - except Exception as e: + except Exception: raise diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8d529fa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,175 @@ +[tool.autopep8] +max_line_length = 88 +ignore = "" # or ["E501", "W6"] +in-place = true +recursive = true +aggressive = 3 + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] +extend-exclude = ["tests", "test"] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.11. +target-version = "py311" + +[tool.ruff.lint] +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default, ('UP') is pyupgrade. +# "ERA" - Found commented-out code +select = ["E", "F", "W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] + +extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] +# Never enforce `E501` (line length violations). +ignore = [ + 'E501', + "F401", + 'E722', + 'E741', + 'F405', + 'F811', + 'F841', + 'A001' , + 'A002', + 'A003', + 'B008', + 'PD901', + 'PD015', + 'N802', + 'N806', + # TODO: PLE0604 Invalid object in `__all__`, must contain only strings + 'PLE0604', + 'RUF012', + # TODO: PT009 Use a regular `assert` instead of unittest-style `assertTrue` + 'PT009', + # Skip for logging: UP031 Use format specifiers instead of percent format + 'UP031' +] + + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = ["B"] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.flake8-annotations] +allow-star-arg-any = false +ignore-fully-untyped = false + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" + +# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402"] +#"path/to/file.py" = ["E402"] + +[tool.ruff.lint.isort] +force-single-line = true +force-sort-within-sections = false +lines-after-imports = 2 + +[tool.ruff.lint.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + +[tool.ruff.lint.pycodestyle] +ignore-overlong-task-comments = true + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + + +[tool.mypy] +strict = true +# Adapted from this StackOverflow post: +# https://stackoverflow.com/questions/55944201/python-type-hinting-how-do-i-enforce-that-project-wide +python_version = "3.11" +# This flag enhances the user feedback for error messages +pretty = true +# 3rd party import +ignore_missing_imports = true +# Disallow dynamic typing +disallow_any_unimported = false +disallow_any_expr = false +disallow_any_decorated = false +disallow_any_explicit = true +disallow_any_generics = false +disallow_subclassing_any = true +# Disallow untyped definitions and calls +disallow_untyped_calls = true +disallow_untyped_defs = true +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +# None and optional handling +no_implicit_optional = true +# Configuring warnings +warn_return_any = false +warn_no_return = true +warn_unreachable = true +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = false +# Misc +follow_imports = "silent" +strict_optional = false +strict_equality = true +exclude = '''(?x)( + (^|/)test[^/]*\.py$ # files named "test*.py" + )''' +# Configuring error messages +show_error_context = false +show_column_numbers = false +show_error_codes = true +disable_error_code = 'import-untyped' \ No newline at end of file diff --git a/samples/campaign_sample.py b/samples/campaign_sample.py index 238c642..bd55081 100644 --- a/samples/campaign_sample.py +++ b/samples/campaign_sample.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 646d50b..80118ca 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) diff --git a/samples/email_template_sample.py b/samples/email_template_sample.py index a818549..fbbfc84 100644 --- a/samples/email_template_sample.py +++ b/samples/email_template_sample.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) diff --git a/samples/new_sample.py b/samples/new_sample.py index ee05062..6c7e05f 100644 --- a/samples/new_sample.py +++ b/samples/new_sample.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) @@ -12,6 +13,9 @@ if __name__ == "__main__": + from samples.contacts_sample import edit_contact_data + + result = edit_contact_data() print(result.status_code) try: diff --git a/samples/parse_api_sample.py b/samples/parse_api_sample.py index 0866c2b..fb60171 100644 --- a/samples/parse_api_sample.py +++ b/samples/parse_api_sample.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) diff --git a/samples/segments_sample.py b/samples/segments_sample.py index 66a5d98..88ebda4 100644 --- a/samples/segments_sample.py +++ b/samples/segments_sample.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) diff --git a/samples/sender_and_domain_samples.py b/samples/sender_and_domain_samples.py index 37c1cee..40560d0 100644 --- a/samples/sender_and_domain_samples.py +++ b/samples/sender_and_domain_samples.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) diff --git a/samples/statistic_sample.py b/samples/statistic_sample.py index 70dcdfb..2871f7c 100644 --- a/samples/statistic_sample.py +++ b/samples/statistic_sample.py @@ -3,6 +3,7 @@ from mailjet_rest import Client + mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"])) diff --git a/setup.py b/setup.py index 4572d83..7c791a6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,10 @@ # coding=utf-8 import os -from setuptools import find_packages, setup + +from setuptools import find_packages +from setuptools import setup + HERE = os.path.abspath(os.path.dirname(__file__)) PACKAGE_NAME = "mailjet_rest" diff --git a/test.py b/test.py index c46683f..eb16763 100644 --- a/test.py +++ b/test.py @@ -1,8 +1,9 @@ -import unittest -from mailjet_rest import Client import os import random import string +import unittest + +from mailjet_rest import Client class TestSuite(unittest.TestCase): From eb76cdbc4f9a0c65a27912b13b4737b77cbc0907 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:51:59 +0300 Subject: [PATCH 44/87] Add more linting rules by default --- pyproject.toml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d529fa..384afbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,12 +49,15 @@ target-version = "py311" # McCabe complexity (`C901`) by default. # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default, ('UP') is pyupgrade. # "ERA" - Found commented-out code -select = ["E", "F", "W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] +# see https://docs.astral.sh/ruff/rules/#rules +select = ["A", "B", "C4", "E", "EM", "ERA", "EXE", "F", "FA", "FURB", "G", "ICN", "LOG", "N", "PD", "PERF", "PIE", "PLE", "PLW", "PT", "PTH", "PYI", "Q", "RET", "RUF", "S", "SIM", "UP", "W"] -extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] +#extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] # Never enforce `E501` (line length violations). ignore = [ + # pycodestyle (E, W) 'E501', + # Pyflakes (`F`) "F401", 'E722', 'E741', @@ -67,6 +70,7 @@ ignore = [ 'B008', 'PD901', 'PD015', + # pep8-naming (N) 'N802', 'N806', # TODO: PLE0604 Invalid object in `__all__`, must contain only strings @@ -74,7 +78,7 @@ ignore = [ 'RUF012', # TODO: PT009 Use a regular `assert` instead of unittest-style `assertTrue` 'PT009', - # Skip for logging: UP031 Use format specifiers instead of percent format + # pyupgrade (UP): Skip for logging: UP031 Use format specifiers instead of percent format 'UP031' ] From ac35367ab4356bf53c03f3374fcad71ac0001007 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:52:40 +0300 Subject: [PATCH 45/87] Fix EXE001 Shebang is present but file is not executable --- mailjet_rest/__init__.py | 1 - mailjet_rest/client.py | 1 - 2 files changed, 2 deletions(-) diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index 09da870..73edbde 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding=utf-8 from mailjet_rest.client import Client from mailjet_rest.utils.version import get_version diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 4a18b39..c0f3235 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding=utf-8 import json From 6d15471c261db971f02401ff19836e5e15cf4bc1 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:53:42 +0300 Subject: [PATCH 46/87] Fix UP009 [*] UTF-8 encoding declaration is unnecessary --- mailjet_rest/__init__.py | 1 - mailjet_rest/client.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index 73edbde..3398d46 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -1,4 +1,3 @@ -# coding=utf-8 from mailjet_rest.client import Client from mailjet_rest.utils.version import get_version diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index c0f3235..7f8caf7 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -1,5 +1,3 @@ -# coding=utf-8 - import json import logging import re From 17f78641f9d507b6c06f3c49f6be1c3dd1fd292b Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:55:16 +0300 Subject: [PATCH 47/87] Fix RET503 Missing explicit at the end of function able to return non- value --- mailjet_rest/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 7f8caf7..7ba9cae 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -16,6 +16,7 @@ def prepare_url(key): char_elem = key.group(0) if char_elem.isupper(): return "-" + char_elem.lower() + return '' class Config(object): From 85fc8a3add6156f8be6bfd1cbaaa15baacdff4d3 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:57:08 +0300 Subject: [PATCH 48/87] Use double quotes, Fix UP004 [*] Class inherits from --- mailjet_rest/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 7ba9cae..90654f7 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -16,10 +16,10 @@ def prepare_url(key): char_elem = key.group(0) if char_elem.isupper(): return "-" + char_elem.lower() - return '' + return "" -class Config(object): +class Config: DEFAULT_API_URL = "https://api.mailjet.com/" API_REF = "http://dev.mailjet.com/email-api/v3/" version = "v3" @@ -47,7 +47,7 @@ def __getitem__(self, key): return url, headers -class Endpoint(object): +class Endpoint: def __init__(self, url, headers, auth, action=None): self._url, self.headers, self._auth, self.action = url, headers, auth, action @@ -84,7 +84,7 @@ def delete(self, id, **kwargs): return api_call(self._auth, "delete", self._url, action=self.action, headers=self.headers, resource_id=id, **kwargs) -class Client(object): +class Client: def __init__(self, auth=None, **kwargs): self.auth = auth From 2608c3923e5a8a0c01db8a6c7ca7df3a171803f2 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:58:15 +0300 Subject: [PATCH 49/87] Fix SIM910 [*] Use kwargs.get('version') instead of kwargs.get('version', None) --- mailjet_rest/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 90654f7..f8594ea 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -88,8 +88,8 @@ class Client: def __init__(self, auth=None, **kwargs): self.auth = auth - version = kwargs.get("version", None) - api_url = kwargs.get("api_url", None) + version = kwargs.get("version") + api_url = kwargs.get("api_url") self.config = Config(version=version, api_url=api_url) def __getattr__(self, name): From 1a0233b752f6c0f5fb5cb8f97983fd4d0cfca121 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:07:18 +0300 Subject: [PATCH 50/87] Fix B904 Within an 'except' clause, raise exceptions with 'raise ... from err' --- mailjet_rest/client.py | 8 ++++---- pyproject.toml | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index f8594ea..ac58623 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -122,10 +122,10 @@ def api_call(auth, method, url, headers, data=None, filters=None, resource_id=No timeout=timeout, verify=True, stream=False) return response - except requests.exceptions.Timeout: - raise TimeoutError - except requests.RequestException as e: - raise ApiError(e) + except requests.exceptions.Timeout as err: + raise TimeoutError from err + except requests.RequestException as err: + raise ApiError from err except Exception: raise diff --git a/pyproject.toml b/pyproject.toml index 384afbf..55bd188 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,8 @@ ignore = [ 'N806', # TODO: PLE0604 Invalid object in `__all__`, must contain only strings 'PLE0604', + # RET504 Unnecessary assignment to `response` before `return` statement + 'RET504', 'RUF012', # TODO: PT009 Use a regular `assert` instead of unittest-style `assertTrue` 'PT009', From 1fbfb98c959d9423b20fcde21d3929e9a43c1367 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:10:03 +0300 Subject: [PATCH 51/87] Fix UP032 [*] Use f-string instead of 'format' call; fix %s calls --- mailjet_rest/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index ac58623..8243465 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -146,11 +146,11 @@ def build_headers(resource, action=None, extra_headers=None): def build_url(url, method, action=None, resource_id=None, action_id=None): if action: - url += "/%s" % action + url += f"/{action}" if action_id: - url += "/{}".format(action_id) + url += f"/{action_id}" if resource_id: - url += "/%s" % str(resource_id) + url += f"/{str(resource_id)}" return url From 536505445cb1314dbc11f7e4fb476d88e572f45f Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:13:36 +0300 Subject: [PATCH 52/87] RUF010 [*] Use explicit conversion flag --- mailjet_rest/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 8243465..adacb4b 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -150,7 +150,7 @@ def build_url(url, method, action=None, resource_id=None, action_id=None): if action_id: url += f"/{action_id}" if resource_id: - url += f"/{str(resource_id)}" + url += f"/{resource_id}" return url From 23935f6733b481d08ee134377d745bd7a1a2b7ce Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:18:06 +0300 Subject: [PATCH 53/87] Fix G002 Logging statement uses '%' by replacing it with ',' --- mailjet_rest/client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index adacb4b..13d49de 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -158,13 +158,13 @@ def parse_response(response, debug=False): data = response.json() if debug: - logging.debug("REQUEST: %s" % response.request.url) - logging.debug("REQUEST_HEADERS: %s" % response.request.headers) - logging.debug("REQUEST_CONTENT: %s" % response.request.body) + logging.debug("REQUEST: %s", response.request.url) + logging.debug("REQUEST_HEADERS: %s", response.request.headers) + logging.debug("REQUEST_CONTENT: %s", response.request.body) - logging.debug("RESPONSE: %s" % response.content) - logging.debug("RESP_HEADERS: %s" % response.headers) - logging.debug("RESP_CODE: %s" % response.status_code) + logging.debug("RESPONSE: %s", response.content) + logging.debug("RESP_HEADERS: %s", response.headers) + logging.debug("RESP_CODE: %s", response.status_code) return data From 54619205db71b36a8b7808e8d3a8030330ab1234 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:23:46 +0300 Subject: [PATCH 54/87] Fix S101 Use of 'assert' detected by replacing it with raise ValueError --- mailjet_rest/utils/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 7b6a143..5a833b4 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -8,6 +8,6 @@ def get_version(version=None): """ if version is None: version = VERSION - else: - assert len(version) == 3 + if len(version) != 3: + raise ValueError("The tuple 'version' must contain 3 items") return "{0}.{1}.{2}".format(*(x for x in version)) From a3d620a4cccad24ad27596b0109a9729a3c45643 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:25:03 +0300 Subject: [PATCH 55/87] Fix UP030 Use implicit references for positional format fields --- mailjet_rest/utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 5a833b4..cfc8705 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -10,4 +10,4 @@ def get_version(version=None): version = VERSION if len(version) != 3: raise ValueError("The tuple 'version' must contain 3 items") - return "{0}.{1}.{2}".format(*(x for x in version)) + return "{}.{}.{}".format(*(x for x in version)) From be67f6f010d36014ecd6c0b59e8f6917cb9583f0 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:26:57 +0300 Subject: [PATCH 56/87] Fix EM101 Exception must not use a string literal, assign to variable first --- mailjet_rest/utils/version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index cfc8705..032dcea 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -9,5 +9,6 @@ def get_version(version=None): if version is None: version = VERSION if len(version) != 3: - raise ValueError("The tuple 'version' must contain 3 items") + msg = "The tuple 'version' must contain 3 items" + raise ValueError(msg) return "{}.{}.{}".format(*(x for x in version)) From 7b7970b6f10a0b827952d20076d417098dc37689 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:37:29 +0300 Subject: [PATCH 57/87] Use Path(filename).read_text() if you read the text file to a varible --- pyproject.toml | 1 + samples/contacts_sample.py | 4 ++-- setup.py | 6 ++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 55bd188..c9cfdf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ ignore = [ 'N806', # TODO: PLE0604 Invalid object in `__all__`, must contain only strings 'PLE0604', + "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') # RET504 Unnecessary assignment to `response` before `return` statement 'RET504', 'RUF012', diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 80118ca..d0b8c76 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -1,5 +1,6 @@ import json import os +from pathlib import Path from mailjet_rest import Client @@ -168,10 +169,9 @@ def manage_multiple_contacts_across_multiple_lists(): def upload_the_csv(): """POST https://api.mailjet.com/v3/DATA/contactslist /$ID_CONTACTLIST/CSVData/text:plain""" - f = open("./data.csv") return mailjet30.contactslist_csvdata.create( id="$ID_CONTACTLIST", - data=f.read(), + data=Path("./data.csv").read_text(), ) diff --git a/setup.py b/setup.py index 7c791a6..f12c4c4 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ # coding=utf-8 import os +from pathlib import Path from setuptools import find_packages from setuptools import setup @@ -10,9 +11,6 @@ HERE = os.path.abspath(os.path.dirname(__file__)) PACKAGE_NAME = "mailjet_rest" -with open("README.md", "r") as fh: - long_description = fh.read() - # Dynamically calculate the version based on mailjet_rest.VERSION. version = "latest" @@ -26,7 +24,7 @@ download_url="https://github.com/mailjet/mailjet-apiv3-python/releases/" + version, url="https://github.com/mailjet/mailjet-apiv3-python", description=("Mailjet V3 API wrapper"), - long_description=long_description, + long_description=Path("README.md").read_text(), long_description_content_type="text/markdown", classifiers=["Development Status :: 4 - Beta", "Environment :: Console", From cdb8fac520dae8f91ef8a810ea36f7fa6a22231b Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:38:23 +0300 Subject: [PATCH 58/87] Fix EXE001 and UP009 in setup.py too --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index f12c4c4..20bc30f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# coding=utf-8 - import os from pathlib import Path From b765a2ee7b5fa3ed8e46670da6c3cdb03e71436c Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:39:44 +0300 Subject: [PATCH 59/87] Fix UP034 [*] Avoid extraneous parentheses --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index eb16763..623609e 100644 --- a/test.py +++ b/test.py @@ -17,7 +17,7 @@ def setUp(self): def test_get_no_param(self): result = self.client.contact.get().json() - self.assertTrue(("Data" in result and "Count" in result)) + self.assertTrue("Data" in result and "Count" in result) def test_get_valid_params(self): result = self.client.contact.get(filters={"limit": 2}).json() From ab59f94ecd25aa707e7bb5174e1705bb020f20b1 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:42:11 +0300 Subject: [PATCH 60/87] Fix PTH100 'os.path.abspath()' should be replaced by 'Path.resolve()' --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 20bc30f..02292b0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup -HERE = os.path.abspath(os.path.dirname(__file__)) +HERE = Path(os.path.dirname(__file__)).resolve() PACKAGE_NAME = "mailjet_rest" # Dynamically calculate the version based on mailjet_rest.VERSION. From 37b36f2a62d70446b7e2bda73f4bb20649b2ce81 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:43:45 +0300 Subject: [PATCH 61/87] Fix PTH120 'os.path.dirname()' should be replaced by 'Path.parent' --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02292b0..46ab4d2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup -HERE = Path(os.path.dirname(__file__)).resolve() +HERE = Path(Path(__file__).parent).resolve() PACKAGE_NAME = "mailjet_rest" # Dynamically calculate the version based on mailjet_rest.VERSION. From 331ad44c4cf3acca6128e95d42fa3380605af600 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:01:41 +0300 Subject: [PATCH 62/87] TRY300 Consider moving response to an 'else' block; use secrets instead of random; add new linting and skipping rules --- mailjet_rest/client.py | 3 ++- pyproject.toml | 3 ++- test.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 13d49de..38b8274 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -120,7 +120,6 @@ def api_call(auth, method, url, headers, data=None, filters=None, resource_id=No filters_str = "&".join("%s=%s" % (k, v) for k, v in filters.items()) response = req_method(url, data=data, params=filters_str, headers=headers, auth=auth, timeout=timeout, verify=True, stream=False) - return response except requests.exceptions.Timeout as err: raise TimeoutError from err @@ -128,6 +127,8 @@ def api_call(auth, method, url, headers, data=None, filters=None, resource_id=No raise ApiError from err except Exception: raise + else: + return response def build_headers(resource, action=None, extra_headers=None): diff --git a/pyproject.toml b/pyproject.toml index c9cfdf3..24fe06e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ target-version = "py311" # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default, ('UP') is pyupgrade. # "ERA" - Found commented-out code # see https://docs.astral.sh/ruff/rules/#rules -select = ["A", "B", "C4", "E", "EM", "ERA", "EXE", "F", "FA", "FURB", "G", "ICN", "LOG", "N", "PD", "PERF", "PIE", "PLE", "PLW", "PT", "PTH", "PYI", "Q", "RET", "RUF", "S", "SIM", "UP", "W"] +select = ["A", "ARG", "B", "C4", "DOC", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA", "FLY", "FURB", "G", "ICN", "INP", "INT", "ISC", "LOG", "N", "PD", "PERF", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "T10", "TID", "TRY", "UP", "W"] #extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] # Never enforce `E501` (line length violations). @@ -75,6 +75,7 @@ ignore = [ 'N806', # TODO: PLE0604 Invalid object in `__all__`, must contain only strings 'PLE0604', + 'PLR0913', # PLR0913 Too many arguments in function definition (6 > 5) "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') # RET504 Unnecessary assignment to `response` before `return` statement 'RET504', diff --git a/test.py b/test.py index 623609e..bb66637 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,6 @@ import os import random +import secrets import string import unittest @@ -38,7 +39,7 @@ def test_get_with_action(self): if get_contact["Count"] != 0: contact_id = get_contact["Data"][0]["ID"] else: - contact_random_email = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + contact_random_email = "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" post_contact = self.client.contact.create(data={"Email": contact_random_email}) self.assertTrue(post_contact.status_code == 201) contact_id = post_contact.json()["Data"][0]["ID"] @@ -47,7 +48,7 @@ def test_get_with_action(self): if get_contact_list["Count"] != 0: list_id = get_contact_list["Data"][0]["ID"] else: - contact_list_random_name = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + contact_list_random_name = "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" post_contact_list = self.client.contactslist.create(data={"Name": contact_list_random_name}) self.assertTrue(post_contact_list.status_code == 201) list_id = post_contact_list.json()["Data"][0]["ID"] From c32a024a6c139fb56b2934a65a87630351105395 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:05:53 +0300 Subject: [PATCH 63/87] Skip temporarily ARG001 --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 24fe06e..3a8693e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ select = ["A", "ARG", "B", "C4", "DOC", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA #extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] # Never enforce `E501` (line length violations). ignore = [ + # TODO: Fix unused function argument: `debug`, `kwargs`, and `method` in class Client + 'ARG001', # ARG001 Unused function argument: `debug`, `kwargs`, and `method` in class Client + # pycodestyle (E, W) 'E501', # Pyflakes (`F`) @@ -68,6 +71,7 @@ ignore = [ 'A002', 'A003', 'B008', + 'INP001', # INP001 File `samples/campaign_sample.py` is part of an implicit namespace package. Add an `__init__.py`. 'PD901', 'PD015', # pep8-naming (N) @@ -75,6 +79,7 @@ ignore = [ 'N806', # TODO: PLE0604 Invalid object in `__all__`, must contain only strings 'PLE0604', + 'PLR2004', # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable 'PLR0913', # PLR0913 Too many arguments in function definition (6 > 5) "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') # RET504 Unnecessary assignment to `response` before `return` statement From 0df7e2d9f09c919e154948f388ac00cc938e3caf Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:09:54 +0300 Subject: [PATCH 64/87] Fix minor issues --- mailjet_rest/client.py | 6 +++--- samples/contacts_sample.py | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 38b8274..cfcc95b 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -43,7 +43,7 @@ def __getitem__(self, key): headers["Content-type"] = "text/csv" elif key.lower() != "send" and self.version != "v4": url = urljoin(url, "REST/") - url = url + key.split("_")[0].lower() + url += key.split("_")[0].lower() return url, headers @@ -95,11 +95,11 @@ def __init__(self, auth=None, **kwargs): def __getattr__(self, name): name = re.sub(r"[A-Z]", prepare_url, name) split = name.split("_") - #identify the resource + # identify the resource fname = split[0] action = None if (len(split) > 1): - #identify the sub resource (action) + # identify the sub resource (action) action = split[1] if action == "csvdata": action = "csvdata/text:plain" diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index d0b8c76..26aa160 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -171,7 +171,7 @@ def upload_the_csv(): /$ID_CONTACTLIST/CSVData/text:plain""" return mailjet30.contactslist_csvdata.create( id="$ID_CONTACTLIST", - data=Path("./data.csv").read_text(), + data=Path("./data.csv", encoding="utf-8").read_text(), ) diff --git a/setup.py b/setup.py index 46ab4d2..d6728bb 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ download_url="https://github.com/mailjet/mailjet-apiv3-python/releases/" + version, url="https://github.com/mailjet/mailjet-apiv3-python", description=("Mailjet V3 API wrapper"), - long_description=Path("README.md").read_text(), + long_description=Path("README.md", encoding="utf-8").read_text(), long_description_content_type="text/markdown", classifiers=["Development Status :: 4 - Beta", "Environment :: Console", From a4666287df42e01ac4067940019776afa4be8de4 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:11:42 +0300 Subject: [PATCH 65/87] Fix minor issues --- mailjet_rest/client.py | 2 +- samples/new_sample.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index cfcc95b..eb55fca 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -59,7 +59,7 @@ def _get(self, filters=None, action_id=None, id=None, **kwargs): return api_call(self._auth, "get", self._url, headers=self.headers, action=self.action, action_id=action_id, filters=filters, resource_id=id, **kwargs) def get_many(self, filters=None, action_id=None, **kwargs): - return self._get(filters=filters, action_id=action_id **kwargs) + return self._get(filters=filters, action_id=action_id, **kwargs) def get(self, id=None, filters=None, action_id=None, **kwargs): return self._get(id=id, filters=filters, action_id=action_id, **kwargs) diff --git a/samples/new_sample.py b/samples/new_sample.py index 6c7e05f..d742454 100644 --- a/samples/new_sample.py +++ b/samples/new_sample.py @@ -15,7 +15,6 @@ if __name__ == "__main__": from samples.contacts_sample import edit_contact_data - result = edit_contact_data() print(result.status_code) try: From ca160a4363e801e9889b7802f377c83aa41408fe Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:13:08 +0300 Subject: [PATCH 66/87] Fix encoding --- samples/contacts_sample.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 26aa160..64183ed 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -171,7 +171,7 @@ def upload_the_csv(): /$ID_CONTACTLIST/CSVData/text:plain""" return mailjet30.contactslist_csvdata.create( id="$ID_CONTACTLIST", - data=Path("./data.csv", encoding="utf-8").read_text(), + data=Path("./data.csv").read_text(encoding="utf-8"), ) diff --git a/setup.py b/setup.py index d6728bb..8e40344 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ download_url="https://github.com/mailjet/mailjet-apiv3-python/releases/" + version, url="https://github.com/mailjet/mailjet-apiv3-python", description=("Mailjet V3 API wrapper"), - long_description=Path("README.md", encoding="utf-8").read_text(), + long_description=Path("README.md").read_text(encoding="utf-8"), long_description_content_type="text/markdown", classifiers=["Development Status :: 4 - Beta", "Environment :: Console", From 4bec737682869af5ab14360c1223b11bb74a655f Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:14:25 +0300 Subject: [PATCH 67/87] F401 imported but unused --- setup.py | 1 - test.py | 1 - 2 files changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 8e40344..f7071bb 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -import os from pathlib import Path from setuptools import find_packages diff --git a/test.py b/test.py index bb66637..a64c456 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,4 @@ import os -import random import secrets import string import unittest From 175dcf227646a8297530e32c1377b9e2d714f08f Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:17:03 +0300 Subject: [PATCH 68/87] Fix F841 local variable '_id' is assigned to but never used --- samples/contacts_sample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 64183ed..84d1e41 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -203,7 +203,7 @@ def using_csv_with_atetime_contact_data(): def monitor_the_import_progress(): """GET https://api.mailjet.com/v3/REST/csvimport/$importjob_ID""" _id = "$importjob_ID" - return mailjet30.csvimport.get(id=id) + return mailjet30.csvimport.get(id=_id) def error_handling(): From 489b7cd055096f50203e3c3ae1f98d0ad1e0de48 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:29:57 +0300 Subject: [PATCH 69/87] Fix E122, E125, E126, E131, E202, E501 (88 symbols) --- mailjet_rest/client.py | 88 ++++++++++++++++++++++++++++---- samples/campaign_sample.py | 19 +++---- samples/email_template_sample.py | 3 +- test.py | 21 +++++--- 4 files changed, 98 insertions(+), 33 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index eb55fca..a1d6f7a 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -56,7 +56,16 @@ def __doc__(self): return self._doc def _get(self, filters=None, action_id=None, id=None, **kwargs): - return api_call(self._auth, "get", self._url, headers=self.headers, action=self.action, action_id=action_id, filters=filters, resource_id=id, **kwargs) + return api_call( + self._auth, + "get", + self._url, + headers=self.headers, + action=self.action, + action_id=action_id, + filters=filters, + resource_id=id, + **kwargs) def get_many(self, filters=None, action_id=None, **kwargs): return self._get(filters=filters, action_id=action_id, **kwargs) @@ -64,24 +73,67 @@ def get_many(self, filters=None, action_id=None, **kwargs): def get(self, id=None, filters=None, action_id=None, **kwargs): return self._get(id=id, filters=filters, action_id=action_id, **kwargs) - def create(self, data=None, filters=None, id=None, action_id=None, ensure_ascii=True, data_encoding="utf-8", **kwargs): + def create( + self, + data=None, + filters=None, + id=None, + action_id=None, + ensure_ascii=True, + data_encoding="utf-8", + **kwargs): if self.headers["Content-type"] == "application/json": if ensure_ascii: data = json.dumps(data) else: data = json.dumps(data, ensure_ascii=False).encode(data_encoding) - return api_call(self._auth, "post", self._url, headers=self.headers, resource_id=id, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) - - def update(self, id, data, filters=None, action_id=None, ensure_ascii=True, data_encoding="utf-8", **kwargs): + return api_call( + self._auth, + "post", + self._url, + headers=self.headers, + resource_id=id, + data=data, + action=self.action, + action_id=action_id, + filters=filters, + **kwargs) + + def update( + self, + id, + data, + filters=None, + action_id=None, + ensure_ascii=True, + data_encoding="utf-8", + **kwargs): if self.headers["Content-type"] == "application/json": if ensure_ascii: data = json.dumps(data) else: data = json.dumps(data, ensure_ascii=False).encode(data_encoding) - return api_call(self._auth, "put", self._url, resource_id=id, headers=self.headers, data=data, action=self.action, action_id=action_id, filters=filters, **kwargs) + return api_call( + self._auth, + "put", + self._url, + resource_id=id, + headers=self.headers, + data=data, + action=self.action, + action_id=action_id, + filters=filters, + **kwargs) def delete(self, id, **kwargs): - return api_call(self._auth, "delete", self._url, action=self.action, headers=self.headers, resource_id=id, **kwargs) + return api_call( + self._auth, + "delete", + self._url, + action=self.action, + headers=self.headers, + resource_id=id, + **kwargs) class Client: @@ -106,20 +158,34 @@ def __getattr__(self, name): if action == "csverror": action = "csverror/text:csv" url, headers = self.config[name] - return type(fname, (Endpoint,), {})(url=url, headers=headers, action=action, auth=self.auth) + return type( + fname, (Endpoint,), {})( + url=url, headers=headers, action=action, auth=self.auth) def api_call(auth, method, url, headers, data=None, filters=None, resource_id=None, timeout=60, debug=False, action=None, action_id=None, **kwargs): - url = build_url(url, method=method, action=action, resource_id=resource_id, action_id=action_id) + url = build_url( + url, + method=method, + action=action, + resource_id=resource_id, + action_id=action_id) req_method = getattr(requests, method) try: filters_str = None if filters: filters_str = "&".join("%s=%s" % (k, v) for k, v in filters.items()) - response = req_method(url, data=data, params=filters_str, headers=headers, auth=auth, - timeout=timeout, verify=True, stream=False) + response = req_method( + url, + data=data, + params=filters_str, + headers=headers, + auth=auth, + timeout=timeout, + verify=True, + stream=False) except requests.exceptions.Timeout as err: raise TimeoutError from err diff --git a/samples/campaign_sample.py b/samples/campaign_sample.py index bd55081..54b95b9 100644 --- a/samples/campaign_sample.py +++ b/samples/campaign_sample.py @@ -32,8 +32,7 @@ def by_adding_custom_content(): "Headers": "object", "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", "MJMLContent": "", - "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" - } + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!"} return mailjet30.campaigndraft_detailcontent.create(id=_id, data=data) @@ -73,24 +72,18 @@ def api_call_requirements(): { "From": { "Email": "pilot@mailjet.com", - "Name": "Mailjet Pilot" - }, + "Name": "Mailjet Pilot"}, "To": [ { "Email": "passenger1@mailjet.com", - "Name": "passenger 1" - } - ], + "Name": "passenger 1"}], "Subject": "Your email flight plan!", "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with " - "you!", + "href=\"https://www.mailjet.com/\">Mailjet!


May the delivery force be with " + "you!", "CustomCampaign": "SendAPI_campaign", - "DeduplicateCampaign": True - } - ] - } + "DeduplicateCampaign": True}]} return mailjet31.send.create(data=data) diff --git a/samples/email_template_sample.py b/samples/email_template_sample.py index fbbfc84..3ecfda1 100644 --- a/samples/email_template_sample.py +++ b/samples/email_template_sample.py @@ -38,8 +38,7 @@ def create_a_template_detailcontent(): "Headers": "", "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", "MJMLContent": "", - "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!" - } + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!"} return mailjet30.template_detailcontent.create(id=_id, data=data) diff --git a/test.py b/test.py index a64c456..fd982bc 100644 --- a/test.py +++ b/test.py @@ -38,8 +38,10 @@ def test_get_with_action(self): if get_contact["Count"] != 0: contact_id = get_contact["Data"][0]["ID"] else: - contact_random_email = "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" - post_contact = self.client.contact.create(data={"Email": contact_random_email}) + contact_random_email = "".join(secrets.choice( + string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + post_contact = self.client.contact.create( + data={"Email": contact_random_email}) self.assertTrue(post_contact.status_code == 201) contact_id = post_contact.json()["Data"][0]["ID"] @@ -47,8 +49,10 @@ def test_get_with_action(self): if get_contact_list["Count"] != 0: list_id = get_contact_list["Data"][0]["ID"] else: - contact_list_random_name = "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" - post_contact_list = self.client.contactslist.create(data={"Name": contact_list_random_name}) + contact_list_random_name = "".join(secrets.choice( + string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + post_contact_list = self.client.contactslist.create( + data={"Name": contact_list_random_name}) self.assertTrue(post_contact_list.status_code == 201) list_id = post_contact_list.json()["Data"][0]["ID"] @@ -60,7 +64,8 @@ def test_get_with_action(self): } ] } - result_add_list = self.client.contact_managecontactslists.create(id=contact_id, data=data) + result_add_list = self.client.contact_managecontactslists.create( + id=contact_id, data=data) self.assertTrue(result_add_list.status_code == 201) result = self.client.contact_getcontactslists.get(contact_id).json() @@ -68,8 +73,10 @@ def test_get_with_action(self): def test_get_with_id_filter(self): result_contact = self.client.contact.get(filters={"limit": 1}).json() - result_contact_with_id = self.client.contact.get(filter={"Email": result_contact["Data"][0]["Email"]}).json() - self.assertTrue(result_contact_with_id["Data"][0]["Email"] == result_contact["Data"][0]["Email"]) + result_contact_with_id = self.client.contact.get( + filter={"Email": result_contact["Data"][0]["Email"]}).json() + self.assertTrue( + result_contact_with_id["Data"][0]["Email"] == result_contact["Data"][0]["Email"]) def test_post_with_no_param(self): result = self.client.sender.create(data={}).json() From 39e18aaa7ee547ab7b4e699656aaa93cb96b5b92 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:33:18 +0300 Subject: [PATCH 70/87] Skip the linting rule PLR0917 Too many positional arguments --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 3a8693e..44c1a78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ ignore = [ 'PLE0604', 'PLR2004', # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable 'PLR0913', # PLR0913 Too many arguments in function definition (6 > 5) + 'PLR0917', # PLR0917 Too many positional arguments "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') # RET504 Unnecessary assignment to `response` before `return` statement 'RET504', From f8590176dbbeaa5d16a8f9c5b1f087deddb82e72 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:39:32 +0300 Subject: [PATCH 71/87] Improve skipping linter rules --- pyproject.toml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 44c1a78..e4ab3c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,20 +57,12 @@ select = ["A", "ARG", "B", "C4", "DOC", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA ignore = [ # TODO: Fix unused function argument: `debug`, `kwargs`, and `method` in class Client 'ARG001', # ARG001 Unused function argument: `debug`, `kwargs`, and `method` in class Client - - # pycodestyle (E, W) - 'E501', - # Pyflakes (`F`) - "F401", - 'E722', - 'E741', - 'F405', - 'F811', - 'F841', + # TODO: Fix A001 Variable `TimeoutError` is shadowing a Python builtin 'A001' , + # TODO: Fix A002 Argument `id` is shadowing a Python builtin 'A002', - 'A003', - 'B008', + # pycodestyle (E, W) + 'E501', 'INP001', # INP001 File `samples/campaign_sample.py` is part of an implicit namespace package. Add an `__init__.py`. 'PD901', 'PD015', From 2542fb364d28d4b0ea3c6df505a89d9f46940be3 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:43:45 +0300 Subject: [PATCH 72/87] Add bandit configs --- pyproject.toml | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e4ab3c7..248421a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,4 +178,52 @@ exclude = '''(?x)( show_error_context = false show_column_numbers = false show_error_codes = true -disable_error_code = 'import-untyped' \ No newline at end of file +disable_error_code = 'import-untyped' + + +[tool.bandit] +# usage: bandit -c pyproject.toml -r . +exclude_dirs = ["tests", "test.py"] +tests = ["B201", "B301"] +skips = ["B101", "B601"] + +[tool.bandit.any_other_function_with_shell_equals_true] +no_shell = [ + "os.execl", + "os.execle", + "os.execlp", + "os.execlpe", + "os.execv", + "os.execve", + "os.execvp", + "os.execvpe", + "os.spawnl", + "os.spawnle", + "os.spawnlp", + "os.spawnlpe", + "os.spawnv", + "os.spawnve", + "os.spawnvp", + "os.spawnvpe", + "os.startfile" +] +shell = [ + "os.system", + "os.popen", + "os.popen2", + "os.popen3", + "os.popen4", + "popen2.popen2", + "popen2.popen3", + "popen2.popen4", + "popen2.Popen3", + "popen2.Popen4", + "commands.getoutput", + "commands.getstatusoutput" +] +subprocess = [ + "subprocess.Popen", + "subprocess.call", + "subprocess.check_call", + "subprocess.check_output" +] From 2a0d70c35c82b14fdab5f1039ceeca95fd60886a Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:01:37 +0300 Subject: [PATCH 73/87] Set linting configs --- pyproject.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 248421a..0635aec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,11 +96,23 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" allow-star-arg-any = false ignore-fully-untyped = false +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" + [tool.ruff.format] +exclude = ["*.pyi"] +# Like Black, use double quotes for strings. quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. indent-style = "space" + +# Like Black, respect magic trailing commas. skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. line-ending = "auto" + # Enable auto-formatting of code examples in docstrings. Markdown, # reStructuredText code/literal blocks and doctests are all supported. # From 4ef394cc1aec6b8f82b65104423587cd2ab31a80 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:20:01 +0300 Subject: [PATCH 74/87] Fix E1101: Instance of 'Endpoint' has no '_doc' member (no-member) --- mailjet_rest/client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index a1d6f7a..0e4f56d 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -52,9 +52,6 @@ class Endpoint: def __init__(self, url, headers, auth, action=None): self._url, self.headers, self._auth, self.action = url, headers, auth, action - def __doc__(self): - return self._doc - def _get(self, filters=None, action_id=None, id=None, **kwargs): return api_call( self._auth, From d4d0d2045695e3c48d86e02b7e646d9f55c5a403 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:21:35 +0300 Subject: [PATCH 75/87] Fix C0325: Unnecessary parens after 'if' keyword --- mailjet_rest/__init__.py | 1 + mailjet_rest/client.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index 3398d46..8daae9d 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -4,4 +4,5 @@ __version__ = get_version() +# TODO: E0604: Invalid object 'Client' and 'get_version' in __all__, must contain only strings (invalid-all-object) __all__ = (Client, get_version) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 0e4f56d..c0950ee 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -147,7 +147,7 @@ def __getattr__(self, name): # identify the resource fname = split[0] action = None - if (len(split) > 1): + if len(split) > 1: # identify the sub resource (action) action = split[1] if action == "csvdata": From dd2bde0e90fbf9f59e2ce0b2ef2f68c10a97c962 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:37:58 +0300 Subject: [PATCH 76/87] Add black linting configs; try to skip some formating rules --- pyproject.toml | 15 ++++++++++++++- samples/campaign_sample.py | 2 ++ samples/contacts_sample.py | 2 ++ samples/getting_started_sample.py | 2 ++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0635aec..fbdbff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,16 @@ +[tool.black] +line-length = 88 +target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +skip-string-normalization = false +skip-magic-trailing-comma = false +extend-exclude = ''' +/( + | docs + | setup.py + | venv +)/ +''' + [tool.autopep8] max_line_length = 88 ignore = "" # or ["E501", "W6"] @@ -50,7 +63,7 @@ target-version = "py311" # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default, ('UP') is pyupgrade. # "ERA" - Found commented-out code # see https://docs.astral.sh/ruff/rules/#rules -select = ["A", "ARG", "B", "C4", "DOC", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA", "FLY", "FURB", "G", "ICN", "INP", "INT", "ISC", "LOG", "N", "PD", "PERF", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "T10", "TID", "TRY", "UP", "W"] +select = ["A", "ARG", "B", "C4", "DOC", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA", "FLY", "FURB", "G", "ICN", "INP", "INT", "LOG", "N", "PD", "PERF", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "T10", "TID", "TRY", "UP", "W"] #extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] # Never enforce `E501` (line length violations). diff --git a/samples/campaign_sample.py b/samples/campaign_sample.py index 54b95b9..03bb287 100644 --- a/samples/campaign_sample.py +++ b/samples/campaign_sample.py @@ -67,6 +67,7 @@ def send_the_campaign_right_away(): def api_call_requirements(): """POST https://api.mailjet.com/v3.1/send""" + # fmt: off data = { "Messages": [ { @@ -84,6 +85,7 @@ def api_call_requirements(): "you!", "CustomCampaign": "SendAPI_campaign", "DeduplicateCampaign": True}]} + # fmt: on return mailjet31.send.create(data=data) diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 84d1e41..07449a9 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -189,6 +189,7 @@ def import_csv_content_to_a_list(): def using_csv_with_atetime_contact_data(): """POST https://api.mailjet.com/v3/REST/csvimport""" + # fmt: off data = { "ContactsListID": "$ID_CONTACTLIST", "DataID": "$ID_DATA", @@ -197,6 +198,7 @@ def using_csv_with_atetime_contact_data(): "\"TimezoneOffset\": 2,\"FieldNames\": " "[\"email\", \"birthday\"]} " } + # fmt: on return mailjet30.csvimport.create(data=data) diff --git a/samples/getting_started_sample.py b/samples/getting_started_sample.py index 6caaed0..37bf3e2 100644 --- a/samples/getting_started_sample.py +++ b/samples/getting_started_sample.py @@ -14,6 +14,7 @@ def send_messages(): """POST https://api.mailjet.com/v3.1/send""" + # fmt: off; pylint; noqa data = { "Messages": [ { @@ -37,6 +38,7 @@ def send_messages(): ], "SandboxMode": True, # Remove to send real message. } + # fmt: on; pylint; noqa return mailjet31.send.create(data=data) From c4ea666545827627d47f9604842d13dd9678588a Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:40:57 +0300 Subject: [PATCH 77/87] Apply ruff formatter --- mailjet_rest/client.py | 84 ++++++++++-------- samples/campaign_sample.py | 30 +++---- samples/contacts_sample.py | 123 ++++++++------------------- samples/email_template_sample.py | 31 +++---- samples/getting_started_sample.py | 34 +++----- samples/new_sample.py | 12 +-- samples/parse_api_sample.py | 16 ++-- samples/segments_sample.py | 16 ++-- samples/sender_and_domain_samples.py | 14 +-- samples/statistic_sample.py | 24 +++--- setup.py | 23 ++--- test.py | 62 +++++++------- 12 files changed, 208 insertions(+), 261 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index c0950ee..50fcba8 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -48,7 +48,6 @@ def __getitem__(self, key): class Endpoint: - def __init__(self, url, headers, auth, action=None): self._url, self.headers, self._auth, self.action = url, headers, auth, action @@ -62,7 +61,8 @@ def _get(self, filters=None, action_id=None, id=None, **kwargs): action_id=action_id, filters=filters, resource_id=id, - **kwargs) + **kwargs, + ) def get_many(self, filters=None, action_id=None, **kwargs): return self._get(filters=filters, action_id=action_id, **kwargs) @@ -71,14 +71,15 @@ def get(self, id=None, filters=None, action_id=None, **kwargs): return self._get(id=id, filters=filters, action_id=action_id, **kwargs) def create( - self, - data=None, - filters=None, - id=None, - action_id=None, - ensure_ascii=True, - data_encoding="utf-8", - **kwargs): + self, + data=None, + filters=None, + id=None, + action_id=None, + ensure_ascii=True, + data_encoding="utf-8", + **kwargs, + ): if self.headers["Content-type"] == "application/json": if ensure_ascii: data = json.dumps(data) @@ -94,17 +95,19 @@ def create( action=self.action, action_id=action_id, filters=filters, - **kwargs) + **kwargs, + ) def update( - self, - id, - data, - filters=None, - action_id=None, - ensure_ascii=True, - data_encoding="utf-8", - **kwargs): + self, + id, + data, + filters=None, + action_id=None, + ensure_ascii=True, + data_encoding="utf-8", + **kwargs, + ): if self.headers["Content-type"] == "application/json": if ensure_ascii: data = json.dumps(data) @@ -120,7 +123,8 @@ def update( action=self.action, action_id=action_id, filters=filters, - **kwargs) + **kwargs, + ) def delete(self, id, **kwargs): return api_call( @@ -130,11 +134,11 @@ def delete(self, id, **kwargs): action=self.action, headers=self.headers, resource_id=id, - **kwargs) + **kwargs, + ) class Client: - def __init__(self, auth=None, **kwargs): self.auth = auth version = kwargs.get("version") @@ -155,19 +159,28 @@ def __getattr__(self, name): if action == "csverror": action = "csverror/text:csv" url, headers = self.config[name] - return type( - fname, (Endpoint,), {})( - url=url, headers=headers, action=action, auth=self.auth) - - -def api_call(auth, method, url, headers, data=None, filters=None, resource_id=None, - timeout=60, debug=False, action=None, action_id=None, **kwargs): + return type(fname, (Endpoint,), {})( + url=url, headers=headers, action=action, auth=self.auth + ) + + +def api_call( + auth, + method, + url, + headers, + data=None, + filters=None, + resource_id=None, + timeout=60, + debug=False, + action=None, + action_id=None, + **kwargs, +): url = build_url( - url, - method=method, - action=action, - resource_id=resource_id, - action_id=action_id) + url, method=method, action=action, resource_id=resource_id, action_id=action_id + ) req_method = getattr(requests, method) try: @@ -182,7 +195,8 @@ def api_call(auth, method, url, headers, data=None, filters=None, resource_id=No auth=auth, timeout=timeout, verify=True, - stream=False) + stream=False, + ) except requests.exceptions.Timeout as err: raise TimeoutError from err diff --git a/samples/campaign_sample.py b/samples/campaign_sample.py index 03bb287..d3b8117 100644 --- a/samples/campaign_sample.py +++ b/samples/campaign_sample.py @@ -4,12 +4,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def create_a_campaign_draft(): @@ -20,7 +22,7 @@ def create_a_campaign_draft(): "SenderEmail": "Mister@mailjet.com", "Subject": "Greetings from Mailjet", "ContactsListID": "$ID_CONTACTSLIST", - "Title": "Friday newsletter" + "Title": "Friday newsletter", } return mailjet30.campaigndraft.create(data=data) @@ -32,30 +34,22 @@ def by_adding_custom_content(): "Headers": "object", "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", "MJMLContent": "", - "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!"} + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!", + } return mailjet30.campaigndraft_detailcontent.create(id=_id, data=data) def test_your_campaign(): """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/test""" _id = "$draft_ID" - data = { - "Recipients": [ - { - "Email": "passenger@mailjet.com", - "Name": "Passenger 1" - } - ] - } + data = {"Recipients": [{"Email": "passenger@mailjet.com", "Name": "Passenger 1"}]} return mailjet30.campaigndraft_test.create(id=_id, data=data) def schedule_the_sending(): """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/schedule""" _id = "$draft_ID" - data = { - "Date": "2018-01-01T00:00:00" - } + data = {"Date": "2018-01-01T00:00:00"} return mailjet30.campaigndraft_schedule.create(id=_id, data=data) diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 07449a9..00990b3 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -5,12 +5,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def create_a_contact(): @@ -18,54 +20,34 @@ def create_a_contact(): data = { "IsExcludedFromCampaigns": "true", "Name": "New Contact", - "Email": "passenger@mailjet.com" + "Email": "passenger@mailjet.com", } return mailjet30.contact.create(data=data) def create_contact_metadata(): """POST https://api.mailjet.com/v3/REST/contactmetadata""" - data = { - "Datatype": "str", - "Name": "first_name", - "NameSpace": "static" - } + data = {"Datatype": "str", "Name": "first_name", "NameSpace": "static"} return mailjet30.contactmetadata.create(data=data) def edit_contact_data(): """PUT https://api.mailjet.com/v3/REST/contactdata/$contact_ID""" _id = "*********" # Put real ID to make it work. - data = { - "Data": [ - { - "Name": "first_name", - "Value": "John" - } - ] - } + data = {"Data": [{"Name": "first_name", "Value": "John"}]} return mailjet30.contactdata.update(id=_id, data=data) def manage_contact_properties(): """POST https://api.mailjet.com/v3/REST/contactmetadata""" _id = "$contact_ID" - data = { - "Data": [ - { - "Name": "first_name", - "Value": "John" - } - ] - } + data = {"Data": [{"Name": "first_name", "Value": "John"}]} return mailjet30.contactdata.update(id=_id, data=data) def create_a_contact_list(): """POST https://api.mailjet.com/v3/REST/contactslist""" - data = { - "Name": "my_contactslist" - } + data = {"Name": "my_contactslist"} return mailjet30.contactslist.create(data=data) @@ -76,33 +58,21 @@ def add_a_contact_to_a_contact_list(): "ContactID": "987654321", "ContactAlt": "passenger@mailjet.com", "ListID": "123456", - "ListAlt": "abcdef123" + "ListAlt": "abcdef123", } return mailjet30.listrecipient.create(data=data) def manage_the_subscription_status_of_an_existing_contact(): """POST https://api.mailjet.com/v3/REST/contact/$contact_ID - /managecontactslists""" + /managecontactslists""" _id = "$contact_ID" data = { "ContactsLists": [ - { - "Action": "addforce", - "ListID": "987654321" - }, - { - "Action": "addnoforce", - "ListID": "987654321" - }, - { - "Action": "remove", - "ListID": "987654321" - }, - { - "Action": "unsub", - "ListID": "987654321" - } + {"Action": "addforce", "ListID": "987654321"}, + {"Action": "addnoforce", "ListID": "987654321"}, + {"Action": "remove", "ListID": "987654321"}, + {"Action": "unsub", "ListID": "987654321"}, ] } return mailjet30.contact_managecontactslists.create(id=_id, data=data) @@ -110,7 +80,7 @@ def manage_the_subscription_status_of_an_existing_contact(): def manage_multiple_contacts_in_a_list(): """POST https://api.mailjet.com/v3/REST/contactslist/$list_ID - /managemanycontacts""" + /managemanycontacts""" _id = "$list_ID" data = { "Action": "addnoforce", @@ -119,16 +89,16 @@ def manage_multiple_contacts_in_a_list(): "Email": "passenger@mailjet.com", "IsExcludedFromCampaigns": "false", "Name": "Passenger 1", - "Properties": "object" + "Properties": "object", } - ] + ], } return mailjet30.contactslist_managemanycontacts.create(id=_id, data=data) def monitor_the_upload_job(): """GET https://api.mailjet.com/v3/REST/contactslist/$list_ID - /managemanycontacts""" + /managemanycontacts""" _id = "$list_ID" return mailjet30.contactslist_managemanycontacts.get(id=_id) @@ -141,34 +111,22 @@ def manage_multiple_contacts_across_multiple_lists(): "Email": "passenger@mailjet.com", "IsExcludedFromCampaigns": "false", "Name": "Passenger 1", - "Properties": "object" + "Properties": "object", } ], "ContactsLists": [ - { - "Action": "addforce", - "ListID": "987654321" - }, - { - "Action": "addnoforce", - "ListID": "987654321" - }, - { - "Action": "remove", - "ListID": "987654321" - }, - { - "Action": "unsub", - "ListID": "987654321" - } - ] + {"Action": "addforce", "ListID": "987654321"}, + {"Action": "addnoforce", "ListID": "987654321"}, + {"Action": "remove", "ListID": "987654321"}, + {"Action": "unsub", "ListID": "987654321"}, + ], } return mailjet30.contact_managemanycontacts.create(data=data) def upload_the_csv(): """POST https://api.mailjet.com/v3/DATA/contactslist - /$ID_CONTACTLIST/CSVData/text:plain""" + /$ID_CONTACTLIST/CSVData/text:plain""" return mailjet30.contactslist_csvdata.create( id="$ID_CONTACTLIST", data=Path("./data.csv").read_text(encoding="utf-8"), @@ -182,7 +140,7 @@ def import_csv_content_to_a_list(): "ImportOptions": "", "Method": "addnoforce", "ContactsListID": "123456", - "DataID": "98765432123456789" + "DataID": "98765432123456789", } return mailjet30.csvimport.create(data=data) @@ -216,9 +174,7 @@ def error_handling(): def single_contact_exclusion(): """PUT https://api.mailjet.com/v3/REST/contact/$ID_OR_EMAIL""" _id = "$ID_OR_EMAIL" - data = { - "IsExcludedFromCampaigns": "true" - } + data = {"IsExcludedFromCampaigns": "true"} return mailjet30.contact.update(id=_id, data=data) @@ -230,20 +186,14 @@ def using_contact_managemanycontacts(): "Email": "jimsmith@example.com", "Name": "Jim", "IsExcludedFromCampaigns": "true", - "Properties": { - "Property1": "value", - "Property2": "value2" - } + "Properties": {"Property1": "value", "Property2": "value2"}, }, { "Email": "janetdoe@example.com", "Name": "Janet", "IsExcludedFromCampaigns": "true", - "Properties": { - "Property1": "value", - "Property2": "value2" - } - } + "Properties": {"Property1": "value", "Property2": "value2"}, + }, ] } return mailjet30.contact_managemanycontacts.create(data=data) @@ -251,10 +201,7 @@ def using_contact_managemanycontacts(): def using_csvimport(): """POST https://api.mailjet.com/v3/REST/csvimport""" - data = { - "DataID": "$ID_DATA", - "Method": "excludemarketing" - } + data = {"DataID": "$ID_DATA", "Method": "excludemarketing"} return mailjet30.csvimport.create(data=data) diff --git a/samples/email_template_sample.py b/samples/email_template_sample.py index 3ecfda1..b9df933 100644 --- a/samples/email_template_sample.py +++ b/samples/email_template_sample.py @@ -4,12 +4,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def create_a_template(): @@ -26,7 +28,7 @@ def create_a_template(): "Name": "Promo Codes", "OwnerType": "user", "Presets": "string", - "Purposes": "array" + "Purposes": "array", } return mailjet30.template.create(data=data) @@ -38,7 +40,8 @@ def create_a_template_detailcontent(): "Headers": "", "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", "MJMLContent": "", - "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!"} + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!", + } return mailjet30.template_detailcontent.create(id=_id, data=data) @@ -47,19 +50,11 @@ def use_templates_with_send_api(): data = { "Messages": [ { - "From": { - "Email": "pilot@mailjet.com", - "Name": "Mailjet Pilot" - }, - "To": [ - { - "Email": "passenger1@mailjet.com", - "Name": "passenger 1" - } - ], + "From": {"Email": "pilot@mailjet.com", "Name": "Mailjet Pilot"}, + "To": [{"Email": "passenger1@mailjet.com", "Name": "passenger 1"}], "TemplateID": 1, "TemplateLanguage": True, - "Subject": "Your email flight plan!" + "Subject": "Your email flight plan!", } ] } diff --git a/samples/getting_started_sample.py b/samples/getting_started_sample.py index 37bf3e2..ea29f1a 100644 --- a/samples/getting_started_sample.py +++ b/samples/getting_started_sample.py @@ -4,12 +4,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def send_messages(): @@ -18,22 +20,14 @@ def send_messages(): data = { "Messages": [ { - "From": { - "Email": "pilot@mailjet.com", - "Name": "Mailjet Pilot" - }, - "To": [ - { - "Email": "passenger1@mailjet.com", - "Name": "passenger 1" - } - ], + "From": {"Email": "pilot@mailjet.com", "Name": "Mailjet Pilot"}, + "To": [{"Email": "passenger1@mailjet.com", "Name": "passenger 1"}], "Subject": "Your email flight plan!", "TextPart": "Dear passenger 1, welcome to Mailjet! May the " - "delivery force be with you!", - "HTMLPart": "

Dear passenger 1, welcome to Mailjet!
May the " - "delivery force be with you!" + "delivery force be with you!", + "HTMLPart": '

Dear passenger 1, welcome to Mailjet!
May the ' + "delivery force be with you!", } ], "SandboxMode": True, # Remove to send real message. @@ -64,7 +58,7 @@ def view_message_history(): def retrieve_statistic(): """GET https://api.mailjet.com/v3/REST/statcounters?CounterSource=APIKey - \\&CounterTiming=Message\\&CounterResolution=Lifetime + \\&CounterTiming=Message\\&CounterResolution=Lifetime """ filters = { "CounterSource": "APIKey", diff --git a/samples/new_sample.py b/samples/new_sample.py index d742454..7db707e 100644 --- a/samples/new_sample.py +++ b/samples/new_sample.py @@ -4,12 +4,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) if __name__ == "__main__": diff --git a/samples/parse_api_sample.py b/samples/parse_api_sample.py index fb60171..cd1d032 100644 --- a/samples/parse_api_sample.py +++ b/samples/parse_api_sample.py @@ -4,19 +4,19 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def basic_setup(): """POST https://api.mailjet.com/v3/REST/parseroute""" - data = { - "Url": "https://www.mydomain.com/mj_parse.php" - } + data = {"Url": "https://www.mydomain.com/mj_parse.php"} return mailjet30.parseroute.create(data=data) diff --git a/samples/segments_sample.py b/samples/segments_sample.py index 88ebda4..16e1a2b 100644 --- a/samples/segments_sample.py +++ b/samples/segments_sample.py @@ -4,12 +4,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def create_your_segment(): @@ -17,7 +19,7 @@ def create_your_segment(): data = { "Description": "Will send only to contacts under 35 years of age.", "Expression": "(age<35)", - "Name": "Customers under 35" + "Name": "Customers under 35", } return mailjet30.contactfilter.create(data=data) @@ -31,7 +33,7 @@ def create_a_campaign_with_a_segmentation_filter(): "SenderEmail": "Mister@mailjet.com", "Subject": "Greetings from Mailjet", "ContactsListID": "$ID_CONTACTLIST", - "SegmentationID": "$ID_CONTACT_FILTER" + "SegmentationID": "$ID_CONTACT_FILTER", } return mailjet30.newsletter.create(data=data) diff --git a/samples/sender_and_domain_samples.py b/samples/sender_and_domain_samples.py index 40560d0..0192dfd 100644 --- a/samples/sender_and_domain_samples.py +++ b/samples/sender_and_domain_samples.py @@ -4,12 +4,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def validate_an_entire_domain(): @@ -46,7 +48,7 @@ def use_a_sender_on_all_api_keys(): """POST https://api.mailjet.com/v3/REST/metasender""" data = { "Description": "Metasender 1 - used for Promo emails", - "Email": "pilot@mailjet.com" + "Email": "pilot@mailjet.com", } return mailjet30.metasender.create(data=data) diff --git a/samples/statistic_sample.py b/samples/statistic_sample.py index 2871f7c..03e73c8 100644 --- a/samples/statistic_sample.py +++ b/samples/statistic_sample.py @@ -4,12 +4,14 @@ from mailjet_rest import Client -mailjet30 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"])) +mailjet30 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) +) -mailjet31 = Client(auth=(os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"]), - version="v3.1") +mailjet31 = Client( + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), + version="v3.1", +) def event_based_vs_message_based_stats_timing(): @@ -18,7 +20,7 @@ def event_based_vs_message_based_stats_timing(): "SourceId": "$Campaign_ID", "CounterSource": "Campaign", "CounterTiming": "Message", - "CounterResolution": "Lifetime" + "CounterResolution": "Lifetime", } return mailjet30.statcounters.get(filters=filters) @@ -31,7 +33,7 @@ def view_the_spread_of_events_over_time(): "CounterTiming": "Event", "CounterResolution": "Day", "FromTS": "123", - "ToTS": "456" + "ToTS": "456", } return mailjet30.statcounters.get(filters=filters) @@ -43,17 +45,13 @@ def statistics_for_specific_recipient(): def stats_for_clicked_links(): """GET https://api.mailjet.com/v3/REST/statistics/link-click""" - filters = { - "CampaignId": "$Campaign_ID" - } + filters = {"CampaignId": "$Campaign_ID"} return mailjet30.statistics_linkClick.get(filters=filters) def mailbox_provider_statistics(): """GET https://api.mailjet.com/v3/REST/statistics/recipient-esp""" - filters = { - "CampaignId": "$Campaign_ID" - } + filters = {"CampaignId": "$Campaign_ID"} return mailjet30.statistics_recipientEsp.get(filters=filters) diff --git a/setup.py b/setup.py index f7071bb..528aadb 100644 --- a/setup.py +++ b/setup.py @@ -22,19 +22,20 @@ description=("Mailjet V3 API wrapper"), long_description=Path("README.md").read_text(encoding="utf-8"), long_description_content_type="text/markdown", - classifiers=["Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU General Public License (GPL)", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Topic :: Utilities"], + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Topic :: Utilities", + ], license="MIT", keywords="Mailjet API v3 / v3.1 Python Wrapper", - include_package_data=True, install_requires=["requests>=2.4.3"], tests_require=["unittest"], diff --git a/test.py b/test.py index fd982bc..09cceee 100644 --- a/test.py +++ b/test.py @@ -7,12 +7,8 @@ class TestSuite(unittest.TestCase): - def setUp(self): - self.auth = ( - os.environ["MJ_APIKEY_PUBLIC"], - os.environ["MJ_APIKEY_PRIVATE"] - ) + self.auth = (os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) self.client = Client(auth=self.auth) def test_get_no_param(self): @@ -38,10 +34,16 @@ def test_get_with_action(self): if get_contact["Count"] != 0: contact_id = get_contact["Data"][0]["ID"] else: - contact_random_email = "".join(secrets.choice( - string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + contact_random_email = ( + "".join( + secrets.choice(string.ascii_uppercase + string.digits) + for _ in range(10) + ) + + "@mailjet.com" + ) post_contact = self.client.contact.create( - data={"Email": contact_random_email}) + data={"Email": contact_random_email} + ) self.assertTrue(post_contact.status_code == 201) contact_id = post_contact.json()["Data"][0]["ID"] @@ -49,23 +51,23 @@ def test_get_with_action(self): if get_contact_list["Count"] != 0: list_id = get_contact_list["Data"][0]["ID"] else: - contact_list_random_name = "".join(secrets.choice( - string.ascii_uppercase + string.digits) for _ in range(10)) + "@mailjet.com" + contact_list_random_name = ( + "".join( + secrets.choice(string.ascii_uppercase + string.digits) + for _ in range(10) + ) + + "@mailjet.com" + ) post_contact_list = self.client.contactslist.create( - data={"Name": contact_list_random_name}) + data={"Name": contact_list_random_name} + ) self.assertTrue(post_contact_list.status_code == 201) list_id = post_contact_list.json()["Data"][0]["ID"] - data = { - "ContactsLists": [ - { - "ListID": list_id, - "Action": "addnoforce" - } - ] - } + data = {"ContactsLists": [{"ListID": list_id, "Action": "addnoforce"}]} result_add_list = self.client.contact_managecontactslists.create( - id=contact_id, data=data) + id=contact_id, data=data + ) self.assertTrue(result_add_list.status_code == 201) result = self.client.contact_getcontactslists.get(contact_id).json() @@ -74,30 +76,26 @@ def test_get_with_action(self): def test_get_with_id_filter(self): result_contact = self.client.contact.get(filters={"limit": 1}).json() result_contact_with_id = self.client.contact.get( - filter={"Email": result_contact["Data"][0]["Email"]}).json() + filter={"Email": result_contact["Data"][0]["Email"]} + ).json() self.assertTrue( - result_contact_with_id["Data"][0]["Email"] == result_contact["Data"][0]["Email"]) + result_contact_with_id["Data"][0]["Email"] + == result_contact["Data"][0]["Email"] + ) def test_post_with_no_param(self): result = self.client.sender.create(data={}).json() self.assertTrue("StatusCode" in result and result["StatusCode"] == 400) def test_client_custom_version(self): - self.client = Client( - auth=self.auth, - version="v3.1" - ) + self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.version, "v3.1") self.assertEqual( - self.client.config["send"][0], - "https://api.mailjet.com/v3.1/send" + self.client.config["send"][0], "https://api.mailjet.com/v3.1/send" ) def test_user_agent(self): - self.client = Client( - auth=self.auth, - version="v3.1" - ) + self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.3") From 624d63234083df21dd8cc3bb76d3b3762fb84586 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:18:30 +0300 Subject: [PATCH 78/87] Revert back Client error handling to see whole traceback --- mailjet_rest/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 50fcba8..6713f52 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -198,10 +198,10 @@ def api_call( stream=False, ) - except requests.exceptions.Timeout as err: - raise TimeoutError from err - except requests.RequestException as err: - raise ApiError from err + except requests.exceptions.Timeout: + raise TimeoutError + except requests.RequestException as e: + raise ApiError(e) except Exception: raise else: From 8093b4d0dc3a82db722cdc1c30cb0aa959b81cdc Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:51:34 +0300 Subject: [PATCH 79/87] Revert back random import --- test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test.py b/test.py index 09cceee..e5cff19 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,5 @@ import os -import secrets +import random import string import unittest @@ -36,7 +36,7 @@ def test_get_with_action(self): else: contact_random_email = ( "".join( - secrets.choice(string.ascii_uppercase + string.digits) + random.choice(string.ascii_uppercase + string.digits) for _ in range(10) ) + "@mailjet.com" @@ -53,7 +53,7 @@ def test_get_with_action(self): else: contact_list_random_name = ( "".join( - secrets.choice(string.ascii_uppercase + string.digits) + random.choice(string.ascii_uppercase + string.digits) for _ in range(10) ) + "@mailjet.com" From 6364c3f219fbb03efbc32544308a1e77469b4618 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:06:40 +0300 Subject: [PATCH 80/87] Skip more linting rules, revert a few changes with double qoutes --- pyproject.toml | 42 ++++++++++++++++++++------------------ samples/campaign_sample.py | 4 ++-- samples/contacts_sample.py | 9 +++----- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fbdbff0..b7a09d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,32 +69,34 @@ select = ["A", "ARG", "B", "C4", "DOC", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA # Never enforce `E501` (line length violations). ignore = [ # TODO: Fix unused function argument: `debug`, `kwargs`, and `method` in class Client - 'ARG001', # ARG001 Unused function argument: `debug`, `kwargs`, and `method` in class Client + "ARG001", # ARG001 Unused function argument: `debug`, `kwargs`, and `method` in class Client # TODO: Fix A001 Variable `TimeoutError` is shadowing a Python builtin - 'A001' , + "A001" , # TODO: Fix A002 Argument `id` is shadowing a Python builtin - 'A002', + "A002", + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` # pycodestyle (E, W) - 'E501', - 'INP001', # INP001 File `samples/campaign_sample.py` is part of an implicit namespace package. Add an `__init__.py`. - 'PD901', - 'PD015', + "E501", + "INP001", # INP001 File `samples/campaign_sample.py` is part of an implicit namespace package. Add an `__init__.py`. + "PD901", + "PD015", # pep8-naming (N) - 'N802', - 'N806', + "N802", + "N806", # TODO: PLE0604 Invalid object in `__all__`, must contain only strings - 'PLE0604', - 'PLR2004', # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable - 'PLR0913', # PLR0913 Too many arguments in function definition (6 > 5) - 'PLR0917', # PLR0917 Too many positional arguments + "PLE0604", + "PLR2004", # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable + "PLR0913", # PLR0913 Too many arguments in function definition (6 > 5) + "PLR0917", # PLR0917 Too many positional arguments + # TODO: "Remove Q000 it before the next release + "Q000", "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') - # RET504 Unnecessary assignment to `response` before `return` statement - 'RET504', - 'RUF012', - # TODO: PT009 Use a regular `assert` instead of unittest-style `assertTrue` - 'PT009', - # pyupgrade (UP): Skip for logging: UP031 Use format specifiers instead of percent format - 'UP031' + "RET504", # RET504 Unnecessary assignment to `response` before `return` statement + "RUF012", + # TODO:" PT009 Use a regular `assert` instead of unittest-style `assertTrue` + "PT009", + "S311", # S311 Standard pseudo-random generators are not suitable for cryptographic purposes + "UP031", # pyupgrade (UP): Skip for logging: UP031 Use format specifiers instead of percent format ] diff --git a/samples/campaign_sample.py b/samples/campaign_sample.py index d3b8117..1d8d4a3 100644 --- a/samples/campaign_sample.py +++ b/samples/campaign_sample.py @@ -75,8 +75,8 @@ def api_call_requirements(): "Subject": "Your email flight plan!", "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with " - "you!", + "href=\"https://www.mailjet.com/\">Mailjet!
May the delivery force be with " + "you!", "CustomCampaign": "SendAPI_campaign", "DeduplicateCampaign": True}]} # fmt: on diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 00990b3..63766f8 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -64,8 +64,7 @@ def add_a_contact_to_a_contact_list(): def manage_the_subscription_status_of_an_existing_contact(): - """POST https://api.mailjet.com/v3/REST/contact/$contact_ID - /managecontactslists""" + """POST https://api.mailjet.com/v3/REST/contact/$contact_ID/managecontactslists""" _id = "$contact_ID" data = { "ContactsLists": [ @@ -79,8 +78,7 @@ def manage_the_subscription_status_of_an_existing_contact(): def manage_multiple_contacts_in_a_list(): - """POST https://api.mailjet.com/v3/REST/contactslist/$list_ID - /managemanycontacts""" + """POST https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" _id = "$list_ID" data = { "Action": "addnoforce", @@ -97,8 +95,7 @@ def manage_multiple_contacts_in_a_list(): def monitor_the_upload_job(): - """GET https://api.mailjet.com/v3/REST/contactslist/$list_ID - /managemanycontacts""" + """GET https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" _id = "$list_ID" return mailjet30.contactslist_managemanycontacts.get(id=_id) From 155c9862b53c013e0231e40a218930dc71892a21 Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Thu, 31 Oct 2024 08:23:13 +0200 Subject: [PATCH 81/87] Support py>=39,; git add to track large files with + # git-lfs rather than commiting them directly to the git history + - id: check-added-large-files + args: [ "--maxkb=500" ] + - id: fix-byte-order-marker + - id: check-case-conflict + # Fails if there are any ">>>>>" lines in files due to merge conflicts. + - id: check-merge-conflict + # ensure syntaxes are valid + - id: check-toml + - id: debug-statements + - id: detect-private-key + # Makes sure files end in a newline and only a newline; + - id: end-of-file-fixer + - id: mixed-line-ending + # Trims trailing whitespace. Allow a single space on the end of .md lines for hard line breaks. + - id: trailing-whitespace + args: [ --markdown-linebreak-ext=md ] + # Sort requirements in requirements.txt files. + - id: requirements-txt-fixer + # Prevent committing directly to trunk + - id: no-commit-to-branch + args: [ "--branch=master" ] + # Detects the presence of private keys + - id: detect-private-key + + - repo: https://github.com/jorisroovers/gitlint + rev: v0.19.1 + hooks: + - id: gitlint + + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: [--write] + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.29.2 + hooks: + - id: check-github-workflows + + - repo: https://github.com/akaihola/darker + rev: v2.1.1 + hooks: + - id: darker + + - repo: https://github.com/PyCQA/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + args: + - --in-place + - --remove-all-unused-imports + - --remove-unused-variable + - --ignore-init-module-imports + + - repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.4 + hooks: + - id: autopep8 + exclude: ^docs/ + +# - repo: https://github.com/pycqa/flake8 +# rev: 7.1.1 +# hooks: +# - id: flake8 +# additional_dependencies: +# - radon +# - flake8-docstrings +# - Flake8-pyproject +# exclude: ^docs/ + + + - repo: https://github.com/PyCQA/pylint + rev: v3.3.1 + hooks: + - id: pylint + args: + - --exit-zero + +# - repo: https://github.com/google/yapf +# rev: v0.31.0 +# hooks: +# - id: yapf +# name: "yapf" +# additional_dependencies: [toml] + +# - repo: https://github.com/RobertCraigie/pyright-python +# rev: v1.1.383 +# hooks: +# - id: pyright + +# - repo: https://github.com/adrienverge/yamllint.git +# rev: v1.29.0 +# hooks: +# - id: yamllint +# args: [--strict] + +# - repo: https://github.com/mattseymour/pre-commit-pytype +# rev: '2020.10.8' +# hooks: +# - id: pytype + + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + args: [--py38-plus, --keep-runtime-typing] + + - repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: v0.6.8 + hooks: + # Run the linter. + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Run the formatter. + - id: ruff-format + + - repo: https://github.com/dosisod/refurb + rev: v2.0.0 + hooks: + - id: refurb + +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.11.2 +# hooks: +# - id: mypy +# args: +# [ +# --config-file=./pyproject.toml, +# ] + +# - repo: https://github.com/executablebooks/mdformat +# rev: 0.7.17 +# hooks: +# - id: mdformat +# additional_dependencies: +# # gfm = GitHub Flavored Markdown +# - mdformat-gfm +# - mdformat-black + +# - repo: https://github.com/PyCQA/bandit +# rev: 1.7.10 +# hooks: +# - id: bandit +# args: ["-r", "."] +# # ignore all tests, not just tests data +# exclude: ^tests/ diff --git a/CHANGELOG.md b/CHANGELOG.md index cfe32ac..0471a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ **Merged pull requests:** - Update README.md [\#44](https://github.com/mailjet/mailjet-apiv3-python/pull/44) ([Hyask](https://github.com/Hyask)) -- new readme version with standartized content [\#42](https://github.com/mailjet/mailjet-apiv3-python/pull/42) ([adamyanliev](https://github.com/adamyanliev)) +- new readme version with standardized content [\#42](https://github.com/mailjet/mailjet-apiv3-python/pull/42) ([adamyanliev](https://github.com/adamyanliev)) - fix page [\#41](https://github.com/mailjet/mailjet-apiv3-python/pull/41) ([adamyanliev](https://github.com/adamyanliev)) - Fix unit tests for new API address [\#37](https://github.com/mailjet/mailjet-apiv3-python/pull/37) ([todorDim](https://github.com/todorDim)) - Fix URL slicing, update version in unit test [\#36](https://github.com/mailjet/mailjet-apiv3-python/pull/36) ([todorDim](https://github.com/todorDim)) diff --git a/LICENSE b/LICENSE index f44502d..3885664 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ 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. \ No newline at end of file +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bb111c1 --- /dev/null +++ b/Makefile @@ -0,0 +1,161 @@ +.PHONY: clean clean-env clean-test clean-pyc clean-build clean-other help dev test test-debug test-cov pre-commit lint format format-docs analyze docs +.DEFAULT_GOAL := help + +# The `.ONESHELL` and setting `SHELL` allows us to run commands that require +# `conda activate` +.ONESHELL: +SHELL := /bin/bash +# For GNU Make v4 and above, you must include the `-c` in order for `make` to find symbols from `PATH` +.SHELLFLAGS := -c -o pipefail -o errexit +CONDA_ACTIVATE = source $$(conda info --base)/etc/profile.d/conda.sh ; conda activate ; conda activate +# Ensure that we are using the python interpreter provided by the conda environment. +PYTHON3 := "$(CONDA_PREFIX)/bin/python3" + +CONDA_ENV_NAME ?= mailjet +SRC_DIR = mailjet_rest +TEST_DIR = tests +SCRIPTS_DIR = scripts/ + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +clean: clean-cov clean-build clean-env clean-pyc clean-test clean-temp clean-other ## remove all build, test, coverage and Python artifacts + +clean-cov: + rm -rf .coverage + rm -rf htmlcov + rm -rf reports/{*.html,*.png,*.js,*.css,*.json} + rm -rf pytest.xml + rm -rf pytest-coverage.txt + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-env: ## remove conda environment + conda remove -y -n $(CONDA_ENV_NAME) --all ; conda info + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + +clean-temp: ## remove temp artifacts + rm -fr temp/tmp.txt + rm -fr tmp.txt + +clean-other: + rm -fr *.prof + rm -fr profile.html profile.json + rm -fr wget-log + rm -fr logs/*.log + rm -fr *.txt + +help: + $(PYTHON3) -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +environment: ## handles environment creation + conda env create -f environment.yaml --name $(CONDA_ENV_NAME) --yes + conda run --name $(CONDA_ENV_NAME) pip install . + +install: clean ## install the package to the active Python's site-packages + pip install . + +release: dist ## package and upload a release + twine upload dist/* + +dist: clean ## builds source and wheel package + python -m build + ls -l dist + +dev: clean ## install the package's development version to a fresh environment + conda env create -f environment.yaml --name $(CONDA_ENV_NAME) --yes + conda run --name $(CONDA_ENV_NAME) pip install -e . + $(CONDA_ACTIVATE) $(CONDA_ENV_NAME) && pre-commit install + +dev-full: clean ## install the package's development version to a fresh environment + conda env create -f environment-dev.yaml --name $(CONDA_ENV_NAME) --yes + conda run --name $(CONDA_ENV_NAME) pip install -e . + $(CONDA_ACTIVATE) $(CONDA_ENV_NAME) && pre-commit install + + +pre-commit: ## runs pre-commit against files. NOTE: older files are disabled in the pre-commit config. + pre-commit run --all-files + +test: ## runs test cases + $(PYTHON3) -m pytest -n auto --capture=no $(TEST_DIR) test.py + +test-debug: ## runs test cases with debugging info enabled + $(PYTHON3) -m pytest -n auto -vv --capture=no $(TEST_DIR) test.py + +test-cov: ## checks test coverage requirements + $(PYTHON3) -m pytest -n auto --cov-config=.coveragerc --cov=$(SRC_DIR) \ + $(TEST_DIR) --cov-fail-under=80 --cov-report term-missing + +tests-cov-fail: + @pytest --cov=$(SRC_DIR) --cov-report term-missing --cov-report=html --cov-fail-under=80 + +coverage: ## check code coverage quickly with the default Python + coverage run --source $(SRC_DIR) -m pytest + coverage report -m + coverage html + $(BROWSER) htmlcov/index.html + +lint-black: + black --line-length=80 $(SRC_DIR) $(TEST_DIR) +lint-isort: + isort --profile black --line-length=80 $(SRC_DIR) $(TEST_DIR) +lint-flake8: + @flake8 $(SRC_DIR) $(TEST_DIR) +lint-pylint: + pylint --rcfile=.pylintrc $(SRC_DIR) $(TEST_DIR) +lint-refurb: + @refurb $(SRC_DIR) + +format-black: + @black --line-length=88 $(SRC_DIR) $(TEST_DIR) $(SCRIPTS_DIR) +format-isort: + @isort --profile black --line-length=88 $(SRC_DIR) $(TEST_DIR) $(SCRIPTS_DIR) +format: format-black format-isort + +format: ## runs the code auto-formatter + isort + black + +format-docs: ## runs the docstring auto-formatter. Note this requires manually installing `docconvert` with `pip` + docconvert --in-place --config .docconvert.json $(SRC_DIR) + +analyze: ## runs static analyzer on the project + mypy --config-file=.mypy.ini --cache-dir=/dev/null $(SRC_DIR) $(TEST_DIR) $(SCRIPTS_DIR) + +lint-mypy-report: + @mypy $(SRC_DIR) --html-report ./mypy_html diff --git a/README.md b/README.md index f0d8a63..8670dcc 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ If your account has been moved to Mailjet's **US architecture**, the URL value y ### URL path -According to python special characters limitations we can't use slashes `/` and dashes `-` which is acceptable for URL path building. Instead python client uses another way for path building. You should replase slashes `/` by underscore `_` and dashes `-` by capitalizing next letter in path. +According to python special characters limitations we can't use slashes `/` and dashes `-` which is acceptable for URL path building. Instead python client uses another way for path building. You should replace slashes `/` by underscore `_` and dashes `-` by capitalizing next letter in path. For example, to reach `statistics/link-click` path you should call `statistics_linkClick` attribute of python client. ```python @@ -152,7 +152,7 @@ filters = { result = mailjet.statistics_linkClick.get(filters=filters) print(result.status_code) print(result.json()) -``` +``` ## Request examples diff --git a/environment-dev.yaml b/environment-dev.yaml new file mode 100644 index 0000000..e233981 --- /dev/null +++ b/environment-dev.yaml @@ -0,0 +1,53 @@ +name: mailjet_dev +channels: + - defaults +dependencies: + - python >=3.9 + # build & host deps + - pip + # runtime deps + - requests >=2.32.3 + # tests + - pytest + - pytest-cov + - pytest-xdist + - conda-forge::pyfakefs + - pytest-benchmark + - coverage >=4.5.4 + # linters & formatters + # Match version with .pre-commit-config.yaml to ensure consistent rules with `make lint`. + - conda-forge::pylint ==3.2.2 + - autopep8 + - black + - isort + - make + - pycodestyle + - pydocstyle + - flake8 + - pep8-naming + - yapf + - ruff + - mypy + - toml + - types-requests + - pandas-stubs + - pyright + - radon + # other + - pre-commit + - conda + - conda-build + - jsonschema + - types-jsonschema + - python-dotenv >=0.19.2 + - pip: + - pyupgrade + - bandit + - autoflake8 + - refurb + - monkeytype + - pyment >=0.3.3 + - pytype + - vulture + - scalene >=1.3.16 + - snakeviz diff --git a/environment.yaml b/environment.yaml new file mode 100644 index 0000000..166edd7 --- /dev/null +++ b/environment.yaml @@ -0,0 +1,13 @@ +name: mailjet +channels: + - defaults +dependencies: + - python >=3.9 + # build & host deps + - pip + # runtime deps + - requests >=2.32.3 + # tests + - pytest >=7.0.0 + # other + - pre-commit diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index 8daae9d..6e19017 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -4,5 +4,6 @@ __version__ = get_version() -# TODO: E0604: Invalid object 'Client' and 'get_version' in __all__, must contain only strings (invalid-all-object) +# TODO: E0604: Invalid object 'Client' and 'get_version' in __all__, must +# contain only strings (invalid-all-object) __all__ = (Client, get_version) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 6713f52..1971ca1 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -186,7 +186,7 @@ def api_call( try: filters_str = None if filters: - filters_str = "&".join("%s=%s" % (k, v) for k, v in filters.items()) + filters_str = "&".join(f"{k}={v}" for k, v in filters.items()) response = req_method( url, data=data, @@ -223,12 +223,12 @@ def build_headers(resource, action=None, extra_headers=None): def build_url(url, method, action=None, resource_id=None, action_id=None): + if resource_id: + url += f"/{resource_id}" if action: url += f"/{action}" if action_id: url += f"/{action_id}" - if resource_id: - url += f"/{resource_id}" return url diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 032dcea..43553bc 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -1,4 +1,4 @@ -VERSION = (1, 3, 3) +VERSION = (1, 3, 5) def get_version(version=None): diff --git a/pyproject.toml b/pyproject.toml index b7a09d1..a2fe7a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,113 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["mailjet_rest", "mailjet_rest.*", "tests", "test.py"] + +[project] +name = "mailjet_rest" +version = "1.3.5" +description = "Mailjet V3 API wrapper" +authors = [ + { name = "starenka", email = "starenka0@gmail.com" }, + { name = "Mailjet", email = "api@mailjet.com" }, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.9" + +dependencies = ["requests>=2.32.3"] + +keywords = [ + "Mailjet API v3 / v3.1 Python Wrapper", + "wrapper", + "email python-wrapper", + "transactional-emails", + "mailjet", + "mailjet-api", + ] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Communications :: Email", + "Topic :: Utilities", +] + +[project.urls] +"Issue Tracker" = "https://github.com/mailjet/mailjet-apiv3-python" +"Repository" = "https://github.com/mailjet/mailjet-apiv3-python" +"Homepage" = "https://dev.mailjet.com" +"Documentation" = "https://dev.mailjet.com" + +[project.optional-dependencies] +linting = [ + # dev tools + "make", + "toml", + "autopep8", + "bandit", + "black>=21.7", + "autoflake", + "flake8>=3.7.8", + "pep8-naming", + "isort", + "yapf", + "pycodestyle", + "pydocstyle", + "pyupgrade", + "refurb", + "pre-commit", + "ruff", + "mypy", + "types-requests", # mypy requests stub + "pandas-stubs", # mypy pandas stub + "types-PyYAML", + "monkeytype", # It can generate type hints based on the observed behavior of your code. + "pyright", + "pylint", + "pyment>=0.3.3", # for generating docstrings + "pytype", # a static type checker for any type hints you have put in your code + "radon", + "vulture", + # env variables + "python-dotenv>=0.19.2", +] + +metrics = [ + "pystra", # provides functionalities to enable structural reliability analysis + "wily>=1.2.0", # a tool for reporting code complexity metrics +] + +profilers = ["scalene>=1.3.16", "snakeviz"] + +tests = [ + # tests + "pytest>=7.0.0", + "pytest-benchmark", + "pytest-cov", + "coverage>=4.5.4", + "codecov", +] + +conda_build = ["conda-build"] + + [tool.black] line-length = 88 -target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +target-version = ["py39", "py310", "py311", "py312"] skip-string-normalization = false skip-magic-trailing-comma = false extend-exclude = ''' @@ -54,8 +161,8 @@ extend-exclude = ["tests", "test"] line-length = 88 indent-width = 4 -# Assume Python 3.11. -target-version = "py311" +# Assume Python 3.9. +target-version = "py39" [tool.ruff.lint] # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or @@ -167,7 +274,7 @@ convention = "numpy" strict = true # Adapted from this StackOverflow post: # https://stackoverflow.com/questions/55944201/python-type-hinting-how-do-i-enforce-that-project-wide -python_version = "3.11" +python_version = "3.9" # This flag enhances the user feedback for error messages pretty = true # 3rd party import diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index da3fa4c..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.21.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 857147b..0000000 --- a/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -from setuptools import find_packages -from setuptools import setup - - -HERE = Path(Path(__file__).parent).resolve() -PACKAGE_NAME = "mailjet_rest" - -# Dynamically calculate the version based on mailjet_rest.VERSION. -version = "latest" - -setup( - name=PACKAGE_NAME, - author="starenka", - author_email="starenka0@gmail.com", - maintainer="Mailjet", - maintainer_email="api@mailjet.com", - version="latest", - download_url="https://github.com/mailjet/mailjet-apiv3-python/releases/" + version, - url="https://github.com/mailjet/mailjet-apiv3-python", - description=("Mailjet V3 API wrapper"), - long_description=Path("README.md").read_text(encoding="utf-8"), - long_description_content_type="text/markdown", - classifiers=['Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Utilities'], - license='MIT', - keywords='Mailjet API v3 / v3.1 Python Wrapper', - include_package_data=True, - install_requires=["requests>=2.4.3"], - tests_require=["unittest"], - entry_points={}, - packages=find_packages(), -) From e9e0733e7bf49925c8de828a015936451b2c4537 Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:56:13 +0200 Subject: [PATCH 82/87] DE-1038: PEP 484 enabled (#109) * Add .editorconfig for teamwork * Add type hints * Fix version, add tests * Fix __all__ in the mailjet_rest module * Add, fix, and improve type hints; add py.typed; add packaging stuff to pyproject.toml, enhance & improve .gitignore * Fix import * Fix package-data * Add type hints to tests * Remove unused type ignore, add mypy configs, improve type hints with checking by mypy --strict * Fix the sample module's import * Ignore type errors * Set allow_redefinition to false * Exclude samples * Add bandit to pre-commit hooks * Formatting * Add toml for bandit * Add pyright type checker with initial settings, and with the pre-comit hook * Add typos spelling checker with the pre-commit hook * Update pre-commit hooks --- .editorconfig | 28 ++++ .github/dependabot.yml | 17 +- .github/workflows/commit_checks.yaml | 1 + .gitignore | 238 +++++++++++++++++++++++++-- .pre-commit-config.yaml | 97 ++++++----- .travis.yml | 1 + environment-dev.yaml | 5 +- environment.yaml | 2 + mailjet_rest/__init__.py | 6 +- mailjet_rest/client.py | 199 ++++++++++++++-------- mailjet_rest/utils/version.py | 7 +- py.typed | 0 pyproject.toml | 57 +++++-- samples/__init__.py | 0 samples/campaign_sample.py | 2 +- samples/contacts_sample.py | 14 +- samples/email_template_sample.py | 6 +- samples/getting_started_sample.py | 4 +- samples/new_sample.py | 3 +- samples/parse_api_sample.py | 2 +- samples/segments_sample.py | 2 +- samples/sender_and_domain_samples.py | 2 +- samples/statistic_sample.py | 2 +- test.py | 74 +++++---- tests/__init__.py | 0 tests/test_client.py | 124 ++++++++++++++ tests/test_version.py | 43 +++++ 27 files changed, 725 insertions(+), 211 deletions(-) create mode 100644 .editorconfig create mode 100644 py.typed create mode 100644 samples/__init__.py create mode 100644 tests/__init__.py create mode 100644 tests/test_client.py create mode 100644 tests/test_version.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..07dc7ba --- /dev/null +++ b/.editorconfig @@ -0,0 +1,28 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.md] +trim_trailing_whitespace = false + +[*.bat] +indent_style = tab +end_of_line = crlf + +[LICENSE] +insert_final_newline = false + +[Makefile] +indent_style = tab +trim_trailing_whitespace = false + +[*.{yaml,yml}] +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 73961f1..b101ec0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,11 @@ +--- version: 2 updates: -- package-ecosystem: pip - directory: "/" - schedule: - interval: "weekly" - groups: - python-packages: - patterns: - - "*" + - package-ecosystem: pip + directory: "/" + schedule: + interval: "weekly" + groups: + python-packages: + patterns: + - "*" diff --git a/.github/workflows/commit_checks.yaml b/.github/workflows/commit_checks.yaml index 152d2aa..3993ccd 100644 --- a/.github/workflows/commit_checks.yaml +++ b/.github/workflows/commit_checks.yaml @@ -1,3 +1,4 @@ +--- name: CI on: diff --git a/.gitignore b/.gitignore index 88e6787..c2134aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,158 @@ +# Generic +__pycache__ +__pycache__/ +__pypackages__/ +!.elasticbeanstalk/*.cfg.yml +!.elasticbeanstalk/*.global.yml +../../../mentor/kupriienko/airtable-notify/.env +.anvil/* +.cache +.eggs/ +.elasticbeanstalk/* +.env +.env-mysql +.history +.hypothesis/ +.installed.cfg +.ipynb_checkpoints +.mr.developer.cfg +.nox/ +.pdm.toml +.prof +.project +.pybuilder/ +.pydevproject +.pyre/ +.Python +.python-version +.pytype/ +.ropeproject +.scrapy +.spyderproject +.spyproject +.tox +.tox/ +.vagrant/ +.venv +.webassets-cache +*.code-workspace +*.cover +*.egg +*.egg-info/ +*.gz +*.iml +*.iws +*.lock +*.log +*.manifest +*.mo +*.pot +*.py,cover +*.py[cod] +*.pyc +*.rar +*.sage.py +*.so +*.spec +*.sqlite +*.zip +**/__pycache__/*.pyc +**/.idea/dataSources.ids +**/.idea/dataSources.local.xml +**/.idea/dataSources.xml +**/.idea/dictionaries +**/.idea/dynamic.xml +**/.idea/jsLibraryMappings.xml +**/.idea/sqlDataSources.xml +**/.idea/tasks.xml +**/.idea/uiDesigner.xml +**/.idea/vcs.xml +**/.idea/workspace.xml +**/staticfiles/ +*/__pycache__/ +*/__pycache__/*.pyc +*/*/__pycache__/ +*/*/*/__pycache__/ +*/staticfiles/ +*$py.class +/bin +/include +/lib +/out/ +/site +/src + +atlassian-ide-plugin.xml +bin +build +build/ +celerybeat-schedule +celerybeat.pid +cmake-build-*/ +com_crashlytics_export_strings.xml +crashlytics-build.properties +crashlytics.properties +cython_debug/ +db.sqlite3 +db.sqlite3-journal +develop-eggs +develop-eggs/ +dist +dist/ +docs/_build/ +downloads/ +eggs +eggs/ +env.bak/ +env/ +ENV/ +fabric.properties +htmlcov/ +instance/ +ipython_config.py +lib +lib/ +lib64 +lib64/ +local_settings.py +MANIFEST +media +myvenv +node_modules +node_modules/ +nosetests.xml +parts +parts/ +pip-delete-this-directory.txt +pip-log.txt +pip-wheel-metadata/ +poetry.toml +profile_default/ +projects/static/ +pyrightconfig.json +pythonenv* +sdist +sdist/ +secret_key.txt +share/python-wheels/ +static/build/ +static/local/ +static/media +static/rev-manifest.json +staticfiles/ +target/ +tdd +temp/ +Thumbs.db +tmp/ +uploads/ +var +var/ +venv +venv.bak/ +venv/ +wheels/ + *~ \#*\# /.emacs.desktop @@ -8,27 +163,74 @@ tramp .\#* .projectile -.ropeproject -.env .overcommit.yml -__pycache__/ -*.py[cod] -*.so -*.egg-info/ -.installed.cfg -*.egg - -pip-log.txt -pip-delete-this-directory.txt +junit* -.tox/ +# Coverage Files +htmlcov +cover/ +.coverage.* .coverage -.cache -nosetests.xml +.coverage* coverage.xml -junit* -build/ -dist/ -venv/ + +# IDEs .idea/ +.idea_modules/ +.idea/* +.idea/*.iml +.idea/**/contentModel.xml +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/dataSources/ +.idea/**/dbnavigator.xml +.idea/**/dictionaries +.idea/**/dynamic.xml +.idea/**/gradle.xml +.idea/**/libraries +.idea/**/mongoSettings.xml +.idea/**/shelf +.idea/**/sqlDataSources.xml +.idea/**/tasks.xml +.idea/**/uiDesigner.xml +.idea/**/usage.statistics.xml +.idea/**/workspace.xml +.idea/caches/build_file_checksums.ser +.idea/dataSources.ids +.idea/dataSources.local.xml +.idea/dataSources.xml +.idea/dictionaries +.idea/dynamic.xml +.idea/gradle.xml +.idea/httpRequests +.idea/jsLibraryMappings.xml +.idea/libraries +.idea/modules +.idea/modules.xml +.idea/mongoSettings.xml +.idea/replstate.xml +.idea/sqlDataSources.xml +.idea/tasks.xml +.idea/uiDesigner.xml +.idea/vcs.xml +.idea/workspace.xml +# VS Code +.vscode/ +# pycharm +queue.json +dev/ + +# Operating Systems +.DS_Store + +# ruff cache +.ruff_cache/ + +# mypy cache +.dmypy.json +.mypy_cache/ + +# pytest cache +.pytest_cache/ +pytestdebug.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09641d4..86a1da6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,5 @@ -# Apply to all files without commiting: +--- +# Apply to all files without committing: # pre-commit run --all-files # Update this file: # pre-commit autoupdate @@ -18,7 +19,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-ast - id: check-builtin-literals @@ -28,7 +29,7 @@ repos: - id: check-vcs-permalinks # Fail if staged files are above a certain size. # To add a large file, use 'git lfs track ; git add to track large files with - # git-lfs rather than commiting them directly to the git history + # git-lfs rather than committing them directly to the git history - id: check-added-large-files args: [ "--maxkb=500" ] - id: fix-byte-order-marker @@ -65,7 +66,7 @@ repos: args: [--write] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.2 + rev: 0.29.4 hooks: - id: check-github-workflows @@ -108,38 +109,15 @@ repos: args: - --exit-zero -# - repo: https://github.com/google/yapf -# rev: v0.31.0 -# hooks: -# - id: yapf -# name: "yapf" -# additional_dependencies: [toml] - -# - repo: https://github.com/RobertCraigie/pyright-python -# rev: v1.1.383 -# hooks: -# - id: pyright - -# - repo: https://github.com/adrienverge/yamllint.git -# rev: v1.29.0 -# hooks: -# - id: yamllint -# args: [--strict] - -# - repo: https://github.com/mattseymour/pre-commit-pytype -# rev: '2020.10.8' -# hooks: -# - id: pytype - - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.19.0 hooks: - id: pyupgrade args: [--py38-plus, --keep-runtime-typing] - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: v0.6.8 + rev: v0.7.1 hooks: # Run the linter. - id: ruff @@ -152,14 +130,46 @@ repos: hooks: - id: refurb -# - repo: https://github.com/pre-commit/mirrors-mypy -# rev: v1.11.2 + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.13.0 + hooks: + - id: mypy + args: + [ + --config-file=./pyproject.toml, + ] + exclude: ^samples/ + + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.387 + hooks: + - id: pyright + + - repo: https://github.com/PyCQA/bandit + rev: 1.7.10 + hooks: + - id: bandit + args: ["-c", "pyproject.toml", "-r", "."] + # ignore all tests, not just tests data + exclude: ^tests/ + additional_dependencies: [".[toml]"] + + - repo: https://github.com/crate-ci/typos + rev: v1.26.8 + hooks: + - id: typos + +# - repo: https://github.com/google/yapf +# rev: v0.40.2 # hooks: -# - id: mypy -# args: -# [ -# --config-file=./pyproject.toml, -# ] +# - id: yapf +# name: "yapf" +# additional_dependencies: [toml] + +# - repo: https://github.com/mattseymour/pre-commit-pytype +# rev: '2023.5.8' +# hooks: +# - id: pytype # - repo: https://github.com/executablebooks/mdformat # rev: 0.7.17 @@ -170,10 +180,11 @@ repos: # - mdformat-gfm # - mdformat-black -# - repo: https://github.com/PyCQA/bandit -# rev: 1.7.10 -# hooks: -# - id: bandit -# args: ["-r", "."] -# # ignore all tests, not just tests data -# exclude: ^tests/ +# - repo: https://github.com/adrienverge/yamllint.git +# rev: v1.35.1 +# hooks: +# - id: yamllint +# #args: [--strict] +# entry: yamllint +# language: python +# types: [ file, yaml ] diff --git a/.travis.yml b/.travis.yml index 85e5d21..315ac8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +--- language: python python: - '2.7' diff --git a/environment-dev.yaml b/environment-dev.yaml index e233981..04e3f94 100644 --- a/environment-dev.yaml +++ b/environment-dev.yaml @@ -1,3 +1,4 @@ +--- name: mailjet_dev channels: - defaults @@ -15,8 +16,7 @@ dependencies: - pytest-benchmark - coverage >=4.5.4 # linters & formatters - # Match version with .pre-commit-config.yaml to ensure consistent rules with `make lint`. - - conda-forge::pylint ==3.2.2 + - pylint - autopep8 - black - isort @@ -51,3 +51,4 @@ dependencies: - vulture - scalene >=1.3.16 - snakeviz + - typos diff --git a/environment.yaml b/environment.yaml index 166edd7..4303c29 100644 --- a/environment.yaml +++ b/environment.yaml @@ -1,3 +1,4 @@ +--- name: mailjet channels: - defaults @@ -11,3 +12,4 @@ dependencies: - pytest >=7.0.0 # other - pre-commit + - toml diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index 6e19017..cc550a8 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -2,8 +2,6 @@ from mailjet_rest.utils.version import get_version -__version__ = get_version() +__version__: str = get_version() -# TODO: E0604: Invalid object 'Client' and 'get_version' in __all__, must -# contain only strings (invalid-all-object) -__all__ = (Client, get_version) +__all__ = ["Client", "get_version"] diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 1971ca1..26186bb 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -1,17 +1,28 @@ +from __future__ import annotations + import json import logging import re +from re import Match +from typing import TYPE_CHECKING +from typing import Any -import requests -from requests.compat import urljoin +import requests # type: ignore[import-untyped] +from requests.compat import urljoin # type: ignore[import-untyped] from .utils.version import get_version +if TYPE_CHECKING: + from collections.abc import Mapping + + from requests.models import Response # type: ignore[import-untyped] + + requests.packages.urllib3.disable_warnings() -def prepare_url(key): +def prepare_url(key: Match[str]) -> str: """Replaces capital letters to lower one with dash prefix.""" char_elem = key.group(0) if char_elem.isupper(): @@ -20,21 +31,24 @@ def prepare_url(key): class Config: - DEFAULT_API_URL = "https://api.mailjet.com/" - API_REF = "http://dev.mailjet.com/email-api/v3/" - version = "v3" - user_agent = "mailjet-apiv3-python/v" + get_version() + DEFAULT_API_URL: str = "https://api.mailjet.com/" + API_REF: str = "http://dev.mailjet.com/email-api/v3/" + version: str = "v3" + user_agent: str = "mailjet-apiv3-python/v" + get_version() - def __init__(self, version=None, api_url=None): + def __init__(self, version: str | None = None, api_url: str | None = None) -> None: if version is not None: self.version = version self.api_url = api_url or self.DEFAULT_API_URL - def __getitem__(self, key): + def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: # Append version to URL. # Forward slash is ignored if present in self.version. url = urljoin(self.api_url, self.version + "/") - headers = {"Content-type": "application/json", "User-agent": self.user_agent} + headers: dict[str, str] = { + "Content-type": "application/json", + "User-agent": self.user_agent, + } if key.lower() == "contactslist_csvdata": url = urljoin(url, "DATA/") headers["Content-type"] = "text/plain" @@ -48,10 +62,22 @@ def __getitem__(self, key): class Endpoint: - def __init__(self, url, headers, auth, action=None): + def __init__( + self, + url: str, + headers: dict[str, str], + auth: tuple[str, str] | None, + action: str | None = None, + ) -> None: self._url, self.headers, self._auth, self.action = url, headers, auth, action - def _get(self, filters=None, action_id=None, id=None, **kwargs): + def _get( + self, + filters: Mapping[str, str | Any] | None = None, + action_id: str | None = None, + id: str | None = None, + **kwargs: Any, + ) -> Response: return api_call( self._auth, "get", @@ -64,34 +90,45 @@ def _get(self, filters=None, action_id=None, id=None, **kwargs): **kwargs, ) - def get_many(self, filters=None, action_id=None, **kwargs): + def get_many( + self, + filters: Mapping[str, str | Any] | None = None, + action_id: str | None = None, + **kwargs: Any, + ) -> Response: return self._get(filters=filters, action_id=action_id, **kwargs) - def get(self, id=None, filters=None, action_id=None, **kwargs): + def get( + self, + id: str | None = None, + filters: Mapping[str, str | Any] | None = None, + action_id: str | None = None, + **kwargs: Any, + ) -> Response: return self._get(id=id, filters=filters, action_id=action_id, **kwargs) def create( self, - data=None, - filters=None, - id=None, - action_id=None, - ensure_ascii=True, - data_encoding="utf-8", - **kwargs, - ): - if self.headers["Content-type"] == "application/json": - if ensure_ascii: - data = json.dumps(data) - else: - data = json.dumps(data, ensure_ascii=False).encode(data_encoding) + data: dict | None = None, + filters: Mapping[str, str | Any] | None = None, + id: str | None = None, + action_id: str | None = None, + ensure_ascii: bool = True, + data_encoding: str = "utf-8", + **kwargs: Any, + ) -> Response: + json_data: str | bytes | None = None + if self.headers.get("Content-type") == "application/json" and data is not None: + json_data = json.dumps(data, ensure_ascii=ensure_ascii) + if not ensure_ascii: + json_data = json_data.encode(data_encoding) return api_call( self._auth, "post", self._url, headers=self.headers, resource_id=id, - data=data, + data=json_data, action=self.action, action_id=action_id, filters=filters, @@ -100,33 +137,33 @@ def create( def update( self, - id, - data, - filters=None, - action_id=None, - ensure_ascii=True, - data_encoding="utf-8", - **kwargs, - ): - if self.headers["Content-type"] == "application/json": - if ensure_ascii: - data = json.dumps(data) - else: - data = json.dumps(data, ensure_ascii=False).encode(data_encoding) + id: str | None, + data: dict | None = None, + filters: Mapping[str, str | Any] | None = None, + action_id: str | None = None, + ensure_ascii: bool = True, + data_encoding: str = "utf-8", + **kwargs: Any, + ) -> Response: + json_data: str | bytes | None = None + if self.headers.get("Content-type") == "application/json" and data is not None: + json_data = json.dumps(data, ensure_ascii=ensure_ascii) + if not ensure_ascii: + json_data = json_data.encode(data_encoding) return api_call( self._auth, "put", self._url, resource_id=id, headers=self.headers, - data=data, + data=json_data, action=self.action, action_id=action_id, filters=filters, **kwargs, ) - def delete(self, id, **kwargs): + def delete(self, id: str | None, **kwargs: Any) -> Response: return api_call( self._auth, "delete", @@ -139,18 +176,18 @@ def delete(self, id, **kwargs): class Client: - def __init__(self, auth=None, **kwargs): + def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None: self.auth = auth - version = kwargs.get("version") - api_url = kwargs.get("api_url") + version: str | None = kwargs.get("version") + api_url: str | None = kwargs.get("api_url") self.config = Config(version=version, api_url=api_url) - def __getattr__(self, name): - name = re.sub(r"[A-Z]", prepare_url, name) - split = name.split("_") + def __getattr__(self, name: str) -> Any: + name_regex: str = re.sub(r"[A-Z]", prepare_url, name) + split: list[str] = name_regex.split("_") # noqa: RUF100, FURB184 # identify the resource - fname = split[0] - action = None + fname: str = split[0] + action: str | None = None if len(split) > 1: # identify the sub resource (action) action = split[1] @@ -160,31 +197,38 @@ def __getattr__(self, name): action = "csverror/text:csv" url, headers = self.config[name] return type(fname, (Endpoint,), {})( - url=url, headers=headers, action=action, auth=self.auth + url=url, + headers=headers, + action=action, + auth=self.auth, ) def api_call( - auth, - method, - url, - headers, - data=None, - filters=None, - resource_id=None, - timeout=60, - debug=False, - action=None, - action_id=None, - **kwargs, -): + auth: tuple[str, str] | None, + method: str, + url: str, + headers: dict[str, str], + data: str | bytes | None = None, + filters: Mapping[str, str | Any] | None = None, + resource_id: str | None = None, + timeout: int = 60, + debug: bool = False, + action: str | None = None, + action_id: str | None = None, + **kwargs: Any, +) -> Response | Any: url = build_url( - url, method=method, action=action, resource_id=resource_id, action_id=action_id + url, + method=method, + action=action, + resource_id=resource_id, + action_id=action_id, ) req_method = getattr(requests, method) try: - filters_str = None + filters_str: str | None = None if filters: filters_str = "&".join(f"{k}={v}" for k, v in filters.items()) response = req_method( @@ -201,15 +245,20 @@ def api_call( except requests.exceptions.Timeout: raise TimeoutError except requests.RequestException as e: - raise ApiError(e) + raise ApiError(e) # noqa: RUF100, B904 except Exception: raise else: return response -def build_headers(resource, action=None, extra_headers=None): - headers = {"Content-type": "application/json"} +def build_headers( + resource: str, + action: str, + extra_headers: dict[str, str] | None = None, +) -> dict[str, str]: + """Build headers based on resource and action.""" + headers: dict[str, str] = {"Content-type": "application/json"} if resource.lower() == "contactslist" and action.lower() == "csvdata": headers = {"Content-type": "text/plain"} @@ -222,7 +271,13 @@ def build_headers(resource, action=None, extra_headers=None): return headers -def build_url(url, method, action=None, resource_id=None, action_id=None): +def build_url( + url: str, + method: str | None, + action: str | None = None, + resource_id: str | None = None, + action_id: str | None = None, +) -> str: if resource_id: url += f"/{resource_id}" if action: @@ -232,7 +287,7 @@ def build_url(url, method, action=None, resource_id=None, action_id=None): return url -def parse_response(response, debug=False): +def parse_response(response: Response, debug: bool = False) -> Any: data = response.json() if debug: diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 43553bc..2955c71 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -1,7 +1,10 @@ -VERSION = (1, 3, 5) +from __future__ import annotations -def get_version(version=None): +VERSION: tuple[int, int, int] = (1, 3, 5) + + +def get_version(version: tuple[int, ...] | None = None) -> str: """ Calculate package version based on a 3 item tuple. In addition verify that the tuple contains 3 items diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index a2fe7a7..83e5a6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,9 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] include = ["mailjet_rest", "mailjet_rest.*", "tests", "test.py"] +[tool.setuptools.package-data] +mailjet_rest = ["py.typed", "*.pyi"] + [project] name = "mailjet_rest" version = "1.3.5" @@ -104,6 +107,10 @@ tests = [ conda_build = ["conda-build"] +spelling = ["typos"] + +other = ["toml"] + [tool.black] line-length = 88 @@ -159,7 +166,7 @@ extend-exclude = ["tests", "test"] # Same as Black. line-length = 88 -indent-width = 4 +#indent-width = 4 # Assume Python 3.9. target-version = "py39" @@ -170,7 +177,12 @@ target-version = "py39" # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default, ('UP') is pyupgrade. # "ERA" - Found commented-out code # see https://docs.astral.sh/ruff/rules/#rules -select = ["A", "ARG", "B", "C4", "DOC", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA", "FLY", "FURB", "G", "ICN", "INP", "INT", "LOG", "N", "PD", "PERF", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "T10", "TID", "TRY", "UP", "W"] +select = ["ALL"] +#select = ["A", "ARG", "B", "C4", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA", "FLY", "FURB", "G", "ICN", "INP", "INT", "LOG", "N", "PD", "PERF", "PIE", "PLC", "PLE", "PLW", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "T10", "TID", "TRY", "UP", "W"] + +external = ["DOC", "PLR"] + +exclude = ["samples/*"] #extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] # Never enforce `E501` (line length violations). @@ -181,9 +193,16 @@ ignore = [ "A001" , # TODO: Fix A002 Argument `id` is shadowing a Python builtin "A002", + "ANN401", # ANN401 Dynamically typed expressions (typing.Any) are disallowed in `**kwargs` "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` # pycodestyle (E, W) + "CPY001", # Missing copyright notice at top of file + # TODO: Enable docstring lints when they will be available. + "D", + "DOC", "E501", + "FBT001", # Boolean-typed positional argument in function definition + "FBT002", # Boolean default positional argument in function definition "INP001", # INP001 File `samples/campaign_sample.py` is part of an implicit namespace package. Add an `__init__.py`. "PD901", "PD015", @@ -194,7 +213,7 @@ ignore = [ "PLE0604", "PLR2004", # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable "PLR0913", # PLR0913 Too many arguments in function definition (6 > 5) - "PLR0917", # PLR0917 Too many positional arguments + #"PLR0917", # PLR0917 Too many positional arguments # TODO: "Remove Q000 it before the next release "Q000", "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') @@ -203,6 +222,7 @@ ignore = [ # TODO:" PT009 Use a regular `assert` instead of unittest-style `assertTrue` "PT009", "S311", # S311 Standard pseudo-random generators are not suitable for cryptographic purposes + "T201", # T201 `print` found "UP031", # pyupgrade (UP): Skip for logging: UP031 Use format specifiers instead of percent format ] @@ -240,14 +260,14 @@ line-ending = "auto" # # This is currently disabled by default, but it is planned for this # to be opt-out in the future. -docstring-code-format = false +#docstring-code-format = false # Set the line length limit used when formatting code snippets in # docstrings. # # This only has an effect when the `docstring-code-format` setting is # enabled. -docstring-code-line-length = "dynamic" +#docstring-code-line-length = "dynamic" # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. [tool.ruff.lint.per-file-ignores] @@ -267,7 +287,7 @@ max-complexity = 10 ignore-overlong-task-comments = true [tool.ruff.lint.pydocstyle] -convention = "numpy" +convention = "google" [tool.mypy] @@ -275,15 +295,19 @@ strict = true # Adapted from this StackOverflow post: # https://stackoverflow.com/questions/55944201/python-type-hinting-how-do-i-enforce-that-project-wide python_version = "3.9" +mypy_path = "type_stubs" +namespace_packages = true # This flag enhances the user feedback for error messages pretty = true # 3rd party import ignore_missing_imports = true +# flag to suppress Name already defined on line +allow_redefinition = false # Disallow dynamic typing disallow_any_unimported = false disallow_any_expr = false disallow_any_decorated = false -disallow_any_explicit = true +disallow_any_explicit = false disallow_any_generics = false disallow_subclassing_any = true # Disallow untyped definitions and calls @@ -305,14 +329,25 @@ warn_unused_ignores = false follow_imports = "silent" strict_optional = false strict_equality = true -exclude = '''(?x)( - (^|/)test[^/]*\.py$ # files named "test*.py" - )''' +#exclude = '''(?x)( +# (^|/)test[^/]*\.py$ # files named "test*.py" +# )''' +exclude = [ + "samples", +] + # Configuring error messages +show-fixes = true show_error_context = false show_column_numbers = false show_error_codes = true -disable_error_code = 'import-untyped' +disable_error_code = 'misc' + +[tool.pyright] +include = ["mailjet_rest"] +exclude = ["samples/*", "**/__pycache__"] +reportAttributeAccessIssue = false +reportMissingImports = false [tool.bandit] diff --git a/samples/__init__.py b/samples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/campaign_sample.py b/samples/campaign_sample.py index 1d8d4a3..6d11dd8 100644 --- a/samples/campaign_sample.py +++ b/samples/campaign_sample.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( diff --git a/samples/contacts_sample.py b/samples/contacts_sample.py index 63766f8..7840175 100644 --- a/samples/contacts_sample.py +++ b/samples/contacts_sample.py @@ -6,7 +6,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( @@ -72,7 +72,7 @@ def manage_the_subscription_status_of_an_existing_contact(): {"Action": "addnoforce", "ListID": "987654321"}, {"Action": "remove", "ListID": "987654321"}, {"Action": "unsub", "ListID": "987654321"}, - ] + ], } return mailjet30.contact_managecontactslists.create(id=_id, data=data) @@ -88,7 +88,7 @@ def manage_multiple_contacts_in_a_list(): "IsExcludedFromCampaigns": "false", "Name": "Passenger 1", "Properties": "object", - } + }, ], } return mailjet30.contactslist_managemanycontacts.create(id=_id, data=data) @@ -109,7 +109,7 @@ def manage_multiple_contacts_across_multiple_lists(): "IsExcludedFromCampaigns": "false", "Name": "Passenger 1", "Properties": "object", - } + }, ], "ContactsLists": [ {"Action": "addforce", "ListID": "987654321"}, @@ -133,7 +133,7 @@ def upload_the_csv(): def import_csv_content_to_a_list(): """POST https://api.mailjet.com/v3/REST/csvimport""" data = { - "ErrTreshold": "1", + "ErrThreshold": "1", "ImportOptions": "", "Method": "addnoforce", "ContactsListID": "123456", @@ -151,7 +151,7 @@ def using_csv_with_atetime_contact_data(): "Method": "addnoforce", "ImportOptions": "{\"DateTimeFormat\": \"yyyy/mm/dd\"," "\"TimezoneOffset\": 2,\"FieldNames\": " - "[\"email\", \"birthday\"]} " + "[\"email\", \"birthday\"]} ", } # fmt: on return mailjet30.csvimport.create(data=data) @@ -191,7 +191,7 @@ def using_contact_managemanycontacts(): "IsExcludedFromCampaigns": "true", "Properties": {"Property1": "value", "Property2": "value2"}, }, - ] + ], } return mailjet30.contact_managemanycontacts.create(data=data) diff --git a/samples/email_template_sample.py b/samples/email_template_sample.py index b9df933..5899aea 100644 --- a/samples/email_template_sample.py +++ b/samples/email_template_sample.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( @@ -55,8 +55,8 @@ def use_templates_with_send_api(): "TemplateID": 1, "TemplateLanguage": True, "Subject": "Your email flight plan!", - } - ] + }, + ], } return mailjet31.send.create(data=data) diff --git a/samples/getting_started_sample.py b/samples/getting_started_sample.py index ea29f1a..67f9f05 100644 --- a/samples/getting_started_sample.py +++ b/samples/getting_started_sample.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( @@ -28,7 +28,7 @@ def send_messages(): "HTMLPart": '

Dear passenger 1, welcome to Mailjet!
May the ' "delivery force be with you!", - } + }, ], "SandboxMode": True, # Remove to send real message. } diff --git a/samples/new_sample.py b/samples/new_sample.py index 7db707e..9ca63f3 100644 --- a/samples/new_sample.py +++ b/samples/new_sample.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( @@ -13,7 +13,6 @@ version="v3.1", ) - if __name__ == "__main__": from samples.contacts_sample import edit_contact_data diff --git a/samples/parse_api_sample.py b/samples/parse_api_sample.py index cd1d032..3476b03 100644 --- a/samples/parse_api_sample.py +++ b/samples/parse_api_sample.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( diff --git a/samples/segments_sample.py b/samples/segments_sample.py index 16e1a2b..1148b35 100644 --- a/samples/segments_sample.py +++ b/samples/segments_sample.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( diff --git a/samples/sender_and_domain_samples.py b/samples/sender_and_domain_samples.py index 0192dfd..a594121 100644 --- a/samples/sender_and_domain_samples.py +++ b/samples/sender_and_domain_samples.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( diff --git a/samples/statistic_sample.py b/samples/statistic_sample.py index 03e73c8..40959cb 100644 --- a/samples/statistic_sample.py +++ b/samples/statistic_sample.py @@ -5,7 +5,7 @@ mailjet30 = Client( - auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) + auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), ) mailjet31 = Client( diff --git a/test.py b/test.py index e5cff19..b327115 100644 --- a/test.py +++ b/test.py @@ -2,39 +2,43 @@ import random import string import unittest +from typing import Any from mailjet_rest import Client class TestSuite(unittest.TestCase): - def setUp(self): - self.auth = (os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]) - self.client = Client(auth=self.auth) + def setUp(self) -> None: + self.auth: tuple[str, str] = ( + os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"], + ) + self.client: Client = Client(auth=self.auth) - def test_get_no_param(self): - result = self.client.contact.get().json() + def test_get_no_param(self) -> None: + result: Any = self.client.contact.get().json() self.assertTrue("Data" in result and "Count" in result) - def test_get_valid_params(self): - result = self.client.contact.get(filters={"limit": 2}).json() + def test_get_valid_params(self) -> None: + result: Any = self.client.contact.get(filters={"limit": 2}).json() self.assertTrue(result["Count"] >= 0 or result["Count"] <= 2) - def test_get_invalid_parameters(self): + def test_get_invalid_parameters(self) -> None: # invalid parameters are ignored - result = self.client.contact.get(filters={"invalid": "false"}).json() + result: Any = self.client.contact.get(filters={"invalid": "false"}).json() self.assertTrue("Count" in result) - def test_get_with_data(self): + def test_get_with_data(self) -> None: # it shouldn't use data result = self.client.contact.get(data={"Email": "api@mailjet.com"}) self.assertTrue(result.status_code == 200) - def test_get_with_action(self): - get_contact = self.client.contact.get(filters={"limit": 1}).json() + def test_get_with_action(self) -> None: + get_contact: Any = self.client.contact.get(filters={"limit": 1}).json() if get_contact["Count"] != 0: - contact_id = get_contact["Data"][0]["ID"] + contact_id: str = get_contact["Data"][0]["ID"] else: - contact_random_email = ( + contact_random_email: str = ( "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(10) @@ -42,16 +46,18 @@ def test_get_with_action(self): + "@mailjet.com" ) post_contact = self.client.contact.create( - data={"Email": contact_random_email} + data={"Email": contact_random_email}, ) self.assertTrue(post_contact.status_code == 201) contact_id = post_contact.json()["Data"][0]["ID"] - get_contact_list = self.client.contactslist.get(filters={"limit": 1}).json() + get_contact_list: Any = self.client.contactslist.get( + filters={"limit": 1}, + ).json() if get_contact_list["Count"] != 0: - list_id = get_contact_list["Data"][0]["ID"] + list_id: str = get_contact_list["Data"][0]["ID"] else: - contact_list_random_name = ( + contact_list_random_name: str = ( "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(10) @@ -59,44 +65,48 @@ def test_get_with_action(self): + "@mailjet.com" ) post_contact_list = self.client.contactslist.create( - data={"Name": contact_list_random_name} + data={"Name": contact_list_random_name}, ) self.assertTrue(post_contact_list.status_code == 201) list_id = post_contact_list.json()["Data"][0]["ID"] - data = {"ContactsLists": [{"ListID": list_id, "Action": "addnoforce"}]} + data: dict[str, list[dict[str, str]]] = { + "ContactsLists": [{"ListID": list_id, "Action": "addnoforce"}], + } result_add_list = self.client.contact_managecontactslists.create( - id=contact_id, data=data + id=contact_id, + data=data, ) self.assertTrue(result_add_list.status_code == 201) result = self.client.contact_getcontactslists.get(contact_id).json() self.assertTrue("Count" in result) - def test_get_with_id_filter(self): - result_contact = self.client.contact.get(filters={"limit": 1}).json() - result_contact_with_id = self.client.contact.get( - filter={"Email": result_contact["Data"][0]["Email"]} + def test_get_with_id_filter(self) -> None: + result_contact: Any = self.client.contact.get(filters={"limit": 1}).json() + result_contact_with_id: Any = self.client.contact.get( + filter={"Email": result_contact["Data"][0]["Email"]}, ).json() self.assertTrue( result_contact_with_id["Data"][0]["Email"] - == result_contact["Data"][0]["Email"] + == result_contact["Data"][0]["Email"], ) - def test_post_with_no_param(self): - result = self.client.sender.create(data={}).json() + def test_post_with_no_param(self) -> None: + result: Any = self.client.sender.create(data={}).json() self.assertTrue("StatusCode" in result and result["StatusCode"] == 400) - def test_client_custom_version(self): + def test_client_custom_version(self) -> None: self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.version, "v3.1") self.assertEqual( - self.client.config["send"][0], "https://api.mailjet.com/v3.1/send" + self.client.config["send"][0], + "https://api.mailjet.com/v3.1/send", ) - def test_user_agent(self): + def test_user_agent(self) -> None: self.client = Client(auth=self.auth, version="v3.1") - self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.3") + self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.5") if __name__ == "__main__": diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..2efd2df --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import json +import os +import re + +import pytest + +from mailjet_rest.utils.version import get_version +from mailjet_rest import Client +from mailjet_rest.client import prepare_url, Config + + +@pytest.fixture +def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: + data: dict[str, list[dict[str, str]]] = { + "Data": [{"Name": "first_name", "Value": "John"}] + } + data_encoding: str = "utf-8" + return data, data_encoding + + +@pytest.fixture +def client_mj30() -> Client: + auth: tuple[str, str] = ( + os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"], + ) + return Client(auth=auth) + + +@pytest.fixture +def client_mj31() -> Client: + auth: tuple[str, str] = ( + os.environ["MJ_APIKEY_PUBLIC"], + os.environ["MJ_APIKEY_PRIVATE"], + ) + return Client( + auth=auth, + version="v3.1", + ) + + +def test_json_data_str_or_bytes_with_ensure_ascii( + simple_data: tuple[dict[str, list[dict[str, str]]], str] +) -> None: + data, data_encoding = simple_data + ensure_ascii: bool = True + + if "application/json" and data is not None: + json_data: str | bytes | None = None + json_data = json.dumps(data, ensure_ascii=ensure_ascii) + + assert isinstance(json_data, str) + if not ensure_ascii: + json_data = json_data.encode(data_encoding) + assert isinstance(json_data, bytes) + + +def test_json_data_str_or_bytes_with_ensure_ascii_false( + simple_data: tuple[dict[str, list[dict[str, str]]], str] +) -> None: + data, data_encoding = simple_data + ensure_ascii: bool = False + + if "application/json" and data is not None: + json_data: str | bytes | None = None + json_data = json.dumps(data, ensure_ascii=ensure_ascii) + + assert isinstance(json_data, str) + if not ensure_ascii: + json_data = json_data.encode(data_encoding) + assert isinstance(json_data, bytes) + + +def test_json_data_is_none( + simple_data: tuple[dict[str, list[dict[str, str]]], str] +) -> None: + data, data_encoding = simple_data + ensure_ascii: bool = True + data: dict[str, list[dict[str, str]]] | None = None # type: ignore + + if "application/json" and data is not None: + json_data: str | bytes | None = None + json_data = json.dumps(data, ensure_ascii=ensure_ascii) + + assert isinstance(json_data, str) + if not ensure_ascii: + json_data = json_data.encode(data_encoding) + assert isinstance(json_data, bytes) + + +def test_prepare_url_list_splitting() -> None: + """Test prepare_url: list splitting""" + name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") + split: list[str] = name.split("_") # noqa: FURB184 + assert split == ["contact", "managecontactslists"] + + +def test_prepare_url_first_list_element() -> None: + """Test prepare_url: list splitting, the first element, url, and headers""" + name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") + fname: str = name.split("_")[0] + assert fname == "contact" + + +def test_prepare_url_headers_and_url() -> None: + """Test prepare_url: list splitting, the first element, url, and headers""" + name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") + config: Config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url == "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +# ======= TEST CLIENT ======== + + +def test_post_with_no_param(client_mj30: Client) -> None: + result = client_mj30.sender.create(data={}).json() + assert "StatusCode" in result and result["StatusCode"] == 400 diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..7eb79dd --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import pytest + +from mailjet_rest.utils.version import get_version, VERSION + + +def test_version_length_equal_three() -> None: + """Verify that the tuple contains 3 items.""" + assert len(VERSION) == 3 + + +def test_get_version_is_none() -> None: + """Test that package version is none.""" + version: None = None + result: str | tuple[int, ...] + result = get_version(version) + assert isinstance(result, str) + result = tuple(map(int, result.split("."))) + assert result == VERSION + assert isinstance(result, tuple) + + +def test_get_version() -> None: + """Test that package version is string. + Verify that if it's equal to tuple after splitting and mapped to tuple. + """ + result: str | tuple[int, ...] + result = get_version(VERSION) + assert isinstance(result, str) + result = tuple(map(int, result.split("."))) + assert result == VERSION + assert isinstance(result, tuple) + + +def test_get_version_raises_exception() -> None: + """Test that package version raise ValueError if its length is not equal 3.""" + version: tuple[int, int] = ( + 1, + 2, + ) + with pytest.raises(ValueError): + get_version(version) From bd7c624a7037ce400735179701bfa925e5b7e2a3 Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:05:00 +0200 Subject: [PATCH 83/87] PEP 257 enabled (#110) * docs: Add docstrings for packages, modules, classes, methods, and functions (following PEP 257 Docstring Conventions); apply linting * docs: Enable docstring lints * docs: Remove Return: None * docs: Remove unused or fixed ruff lint rules * ci: update pre-commit hooks * docs: Add more docstring to tests * docs: pydocstyle linting --- .pre-commit-config.yaml | 6 +- mailjet_rest/__init__.py | 16 ++ mailjet_rest/client.py | 329 +++++++++++++++++++++++++++++++-- mailjet_rest/utils/__init__.py | 7 + mailjet_rest/utils/version.py | 30 ++- pyproject.toml | 22 +-- test.py | 107 +++++++++++ tests/__init__.py | 6 + tests/test_client.py | 297 ++++++++++++++++++++++++++++- tests/test_version.py | 1 + 10 files changed, 783 insertions(+), 38 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86a1da6..9391846 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -117,7 +117,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: v0.7.1 + rev: v0.7.2 hooks: # Run the linter. - id: ruff @@ -141,7 +141,7 @@ repos: exclude: ^samples/ - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.387 + rev: v1.1.388 hooks: - id: pyright @@ -155,7 +155,7 @@ repos: additional_dependencies: [".[toml]"] - repo: https://github.com/crate-ci/typos - rev: v1.26.8 + rev: v1.27.0 hooks: - id: typos diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index cc550a8..df91474 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -1,3 +1,19 @@ +"""The `mailjet_rest` package provides a Python client for interacting with the Mailjet API. + +This package includes the main `Client` class for handling API requests, along with +utility functions for version management. The package exposes a consistent interface +for Mailjet API operations. + +Attributes: + __version__ (str): The current version of the `mailjet_rest` package. + __all__ (list): Specifies the public API of the package, including `Client` + for API interactions and `get_version` for retrieving version information. + +Modules: + - client: Defines the main API client. + - utils.version: Provides version management functionality. +""" + from mailjet_rest.client import Client from mailjet_rest.utils.version import get_version diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 26186bb..d369350 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -1,3 +1,31 @@ +"""This module provides the main client and helper classes for interacting with the Mailjet API. + +The `mailjet_rest.client` module includes the core `Client` class for managing +API requests, configuration, and error handling, as well as utility functions +and classes for building request headers, URLs, and parsing responses. + +Classes: + - Config: Manages configuration settings for the Mailjet API. + - Endpoint: Represents specific API endpoints and provides methods for + common HTTP operations like GET, POST, PUT, and DELETE. + - Client: The main API client for authenticating and making requests. + - ApiError: Base class for handling API-specific errors, with subclasses + for more specific error types (e.g., `AuthorizationError`, `TimeoutError`). + +Functions: + - prepare_url: Prepares URLs for API requests. + - api_call: A helper function that sends HTTP requests to the API and handles + responses. + - build_headers: Builds HTTP headers for the requests. + - build_url: Constructs the full API URL based on endpoint and parameters. + - parse_response: Parses API responses and handles error conditions. + +Exceptions: + - ApiError: Base exception for API errors, with subclasses to represent + specific error types, such as `AuthorizationError`, `TimeoutError`, + `ActionDeniedError`, and `ValidationError`. +""" + from __future__ import annotations import json @@ -23,7 +51,14 @@ def prepare_url(key: Match[str]) -> str: - """Replaces capital letters to lower one with dash prefix.""" + """Replace capital letters in the input string with a dash prefix and converts them to lowercase. + + Parameters: + key (Match[str]): A match object representing a substring from the input string. The substring should contain a single capital letter. + + Returns: + str: A string containing a dash followed by the lowercase version of the input capital letter. + """ char_elem = key.group(0) if char_elem.isupper(): return "-" + char_elem.lower() @@ -31,17 +66,58 @@ def prepare_url(key: Match[str]) -> str: class Config: + """Configuration settings for interacting with the Mailjet API. + + This class stores and manages API configuration details, including the API URL, + version, and user agent string. It provides methods for initializing these settings + and generating endpoint-specific URLs and headers as required for API interactions. + + Attributes: + DEFAULT_API_URL (str): The default base URL for Mailjet API requests. + API_REF (str): Reference URL for Mailjet's API documentation. + version (str): API version to use, defaulting to 'v3'. + user_agent (str): User agent string including the package version for tracking. + """ + DEFAULT_API_URL: str = "https://api.mailjet.com/" - API_REF: str = "http://dev.mailjet.com/email-api/v3/" + API_REF: str = "https://dev.mailjet.com/email-api/v3/" version: str = "v3" user_agent: str = "mailjet-apiv3-python/v" + get_version() def __init__(self, version: str | None = None, api_url: str | None = None) -> None: + """Initialize a new Config instance with specified or default API settings. + + This initializer sets the API version and base URL. If no version or URL + is provided, it defaults to the predefined class values. + + Parameters: + - version (str | None): The API version to use. If None, the default version ('v3') is used. + - api_url (str | None): The base URL for API requests. If None, the default URL (DEFAULT_API_URL) is used. + """ if version is not None: self.version = version self.api_url = api_url or self.DEFAULT_API_URL def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: + """Retrieve the API endpoint URL and headers for a given key. + + This method builds the URL and headers required for specific API interactions. + The URL is adjusted based on the API version, and additional headers are + appended depending on the endpoint type. Specific keys modify content-type + for endpoints expecting CSV or plain text. + + Parameters: + - key (str): The name of the API endpoint, which influences URL structure and header configuration. + + Returns: + - tuple[str, dict[str, str]]: A tuple containing the constructed URL and headers required for the specified endpoint. + + Examples: + For the "contactslist_csvdata" key, a URL pointing to 'DATA/' and a + 'Content-type' of 'text/plain' is returned. + For the "batchjob_csverror" key, a URL with 'DATA/' and a 'Content-type' + of 'text/csv' is returned. + """ # Append version to URL. # Forward slash is ignored if present in self.version. url = urljoin(self.api_url, self.version + "/") @@ -62,6 +138,27 @@ def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: class Endpoint: + """A class representing a specific Mailjet API endpoint. + + This class provides methods to perform HTTP requests to a given API endpoint, + including GET, POST, PUT, and DELETE requests. It manages URL construction, + headers, and authentication for interacting with the endpoint. + + Attributes: + - _url (str): The base URL of the endpoint. + - headers (dict[str, str]): The headers to be included in API requests. + - _auth (tuple[str, str] | None): The authentication credentials. + - action (str | None): The specific action to be performed on the endpoint. + + Methods: + - _get: Internal method to perform a GET request. + - get_many: Performs a GET request to retrieve multiple resources. + - get: Performs a GET request to retrieve a specific resource. + - create: Performs a POST request to create a new resource. + - update: Performs a PUT request to update an existing resource. + - delete: Performs a DELETE request to delete a resource. + """ + def __init__( self, url: str, @@ -69,6 +166,14 @@ def __init__( auth: tuple[str, str] | None, action: str | None = None, ) -> None: + """Initialize a new Endpoint instance. + + Args: + url (str): The base URL for the endpoint. + headers (dict[str, str]): Headers for API requests. + auth (tuple[str, str] | None): Authentication credentials. + action (str | None): Action to perform on the endpoint, if any. + """ self._url, self.headers, self._auth, self.action = url, headers, auth, action def _get( @@ -78,6 +183,20 @@ def _get( id: str | None = None, **kwargs: Any, ) -> Response: + """Perform an internal GET request to the endpoint. + + Constructs the URL with the provided filters and action_id to retrieve + specific data from the API. + + Parameters: + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID for the endpoint to be performed. + - id (str | None): The ID of the specific resource to be retrieved. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ return api_call( self._auth, "get", @@ -96,6 +215,16 @@ def get_many( action_id: str | None = None, **kwargs: Any, ) -> Response: + """Perform a GET request to retrieve multiple resources. + + Parameters: + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID to be performed. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call containing multiple resources. + """ return self._get(filters=filters, action_id=action_id, **kwargs) def get( @@ -105,6 +234,17 @@ def get( action_id: str | None = None, **kwargs: Any, ) -> Response: + """Perform a GET request to retrieve a specific resource. + + Parameters: + - id (str | None): The ID of the specific resource to be retrieved. + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID to be performed. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call containing the specific resource. + """ return self._get(id=id, filters=filters, action_id=action_id, **kwargs) def create( @@ -117,6 +257,20 @@ def create( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: + """Perform a POST request to create a new resource. + + Parameters: + - data (dict | None): The data to include in the request body. + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - id (str | None): The ID of the specific resource to be created. + - action_id (str | None): The specific action ID to be performed. + - ensure_ascii (bool): Whether to ensure ASCII characters in the data. + - data_encoding (str): The encoding to be used for the data. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ json_data: str | bytes | None = None if self.headers.get("Content-type") == "application/json" and data is not None: json_data = json.dumps(data, ensure_ascii=ensure_ascii) @@ -145,6 +299,20 @@ def update( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: + """Perform a PUT request to update an existing resource. + + Parameters: + - id (str | None): The ID of the specific resource to be updated. + - data (dict | None): The data to be sent in the request body. + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID to be performed. + - ensure_ascii (bool): Whether to ensure ASCII characters in the data. + - data_encoding (str): The encoding to be used for the data. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ json_data: str | bytes | None = None if self.headers.get("Content-type") == "application/json" and data is not None: json_data = json.dumps(data, ensure_ascii=ensure_ascii) @@ -164,6 +332,15 @@ def update( ) def delete(self, id: str | None, **kwargs: Any) -> Response: + """Perform a DELETE request to delete a resource. + + Parameters: + - id (str | None): The ID of the specific resource to be deleted. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ return api_call( self._auth, "delete", @@ -176,13 +353,54 @@ def delete(self, id: str | None, **kwargs: Any) -> Response: class Client: + """A client for interacting with the Mailjet API. + + This class manages authentication, configuration, and API endpoint access. + It initializes with API authentication details and uses dynamic attribute access + to allow flexible interaction with various Mailjet API endpoints. + + Attributes: + - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. + - config (Config): An instance of the Config class, which holds API configuration settings. + + Methods: + - __init__: Initializes a new Client instance with authentication and configuration settings. + - __getattr__: Handles dynamic attribute access, allowing for accessing API endpoints as attributes. + """ + def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None: + """Initialize a new Client instance for API interaction. + + This method sets up API authentication and configuration. The `auth` parameter + provides a tuple with the API key and secret. Additional keyword arguments can + specify configuration options like API version and URL. + + Parameters: + - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. If None, authentication is not required. + - **kwargs (Any): Additional keyword arguments, such as `version` and `api_url`, for configuring the client. + + Example: + client = Client(auth=("api_key", "api_secret"), version="v3") + """ self.auth = auth version: str | None = kwargs.get("version") api_url: str | None = kwargs.get("api_url") self.config = Config(version=version, api_url=api_url) def __getattr__(self, name: str) -> Any: + """Dynamically access API endpoints as attributes. + + This method allows for flexible, attribute-style access to API endpoints. + It constructs the appropriate endpoint URL and headers based on the attribute + name, which it parses to identify the resource and optional sub-resources. + + Parameters: + - name (str): The name of the attribute being accessed, corresponding to the Mailjet API endpoint. + + + Returns: + - Endpoint: An instance of the `Endpoint` class, initialized with the constructed URL, headers, action, and authentication details. + """ name_regex: str = re.sub(r"[A-Z]", prepare_url, name) split: list[str] = name_regex.split("_") # noqa: RUF100, FURB184 # identify the resource @@ -218,6 +436,25 @@ def api_call( action_id: str | None = None, **kwargs: Any, ) -> Response | Any: + """Make an API call to a specified URL using the provided method, headers, and other parameters. + + Parameters: + - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. + - method (str): The HTTP method to be used for the API call (e.g., 'get', 'post', 'put', 'delete'). + - url (str): The URL to which the API call will be made. + - headers (dict[str, str]): A dictionary containing the headers to be included in the API call. + - data (str | bytes | None): The data to be sent in the request body. + - filters (Mapping[str, str | Any] | None): A dictionary containing filters to be applied in the request. + - resource_id (str | None): The ID of the specific resource to be accessed. + - timeout (int): The timeout for the API call in seconds. + - debug (bool): A flag indicating whether debug mode is enabled. + - action (str | None): The specific action to be performed on the resource. + - action_id (str | None): The ID of the specific action to be performed. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response | Any: The response object from the API call if the request is successful, or an exception if an error occurs. + """ url = build_url( url, method=method, @@ -257,7 +494,16 @@ def build_headers( action: str, extra_headers: dict[str, str] | None = None, ) -> dict[str, str]: - """Build headers based on resource and action.""" + """Build headers based on resource and action. + + Parameters: + - resource (str): The name of the resource for which headers are being built. + - action (str): The specific action being performed on the resource. + - extra_headers (dict[str, str] | None): Additional headers to be included in the request. Defaults to None. + + Returns: + - dict[str, str]: A dictionary containing the headers to be included in the API request. + """ headers: dict[str, str] = {"Content-type": "application/json"} if resource.lower() == "contactslist" and action.lower() == "csvdata": @@ -278,6 +524,21 @@ def build_url( resource_id: str | None = None, action_id: str | None = None, ) -> str: + """Construct a URL for making an API request. + + This function takes the base URL, method, action, resource ID, and action ID as parameters + and constructs a URL by appending the resource ID, action, and action ID to the base URL. + + Parameters: + url (str): The base URL for the API request. + method (str | None): The HTTP method for the API request (e.g., 'get', 'post', 'put', 'delete'). + action (str | None): The specific action to be performed on the resource. Defaults to None. + resource_id (str | None): The ID of the specific resource to be accessed. Defaults to None. + action_id (str | None): The ID of the specific action to be performed. Defaults to None. + + Returns: + str: The constructed URL for the API request. + """ if resource_id: url += f"/{resource_id}" if action: @@ -288,6 +549,17 @@ def build_url( def parse_response(response: Response, debug: bool = False) -> Any: + """Parse the response from an API request. + + This function extracts the JSON data from the response and logs debug information if the `debug` flag is set to True. + + Parameters: + response (requests.models.Response): The response object from the API request. + debug (bool, optional): A flag indicating whether debug information should be logged. Defaults to False. + + Returns: + Any: The JSON data extracted from the response. + """ data = response.json() if debug: @@ -303,32 +575,67 @@ def parse_response(response: Response, debug: bool = False) -> Any: class ApiError(Exception): - pass + """Base class for all API-related errors. + + This exception serves as the root for all custom API error types, + allowing for more specific error handling based on the type of API + failure encountered. + """ class AuthorizationError(ApiError): - pass + """Error raised for authorization failures. + + This error is raised when the API request fails due to invalid + or missing authentication credentials. + """ class ActionDeniedError(ApiError): - pass + """Error raised when an action is denied by the API. + + This exception is triggered when an action is requested but is not + permitted, likely due to insufficient permissions. + """ class CriticalApiError(ApiError): - pass + """Error raised for critical API failures. + + This error represents severe issues with the API or infrastructure + that prevent requests from completing. + """ class ApiRateLimitError(ApiError): - pass + """Error raised when the API rate limit is exceeded. + + This exception is raised when the user has made too many requests + within a given time frame, as enforced by the API's rate limit policy. + """ class TimeoutError(ApiError): - pass + """Error raised when an API request times out. + + This error is raised if an API request does not complete within + the allowed timeframe, possibly due to network issues or server load. + """ class DoesNotExistError(ApiError): - pass + """Error raised when a requested resource does not exist. + + This exception is triggered when a specific resource is requested + but cannot be found in the API, indicating a potential data mismatch + or invalid identifier. + """ class ValidationError(ApiError): - pass + """Error raised for invalid input data. + + This exception is raised when the input data for an API request + does not meet validation requirements, such as incorrect data types + or missing fields. + """ diff --git a/mailjet_rest/utils/__init__.py b/mailjet_rest/utils/__init__.py index e69de29..21b7c8b 100644 --- a/mailjet_rest/utils/__init__.py +++ b/mailjet_rest/utils/__init__.py @@ -0,0 +1,7 @@ +"""The `mailjet_rest.utils` package provides utility functions for interacting with the package versionI. + +This package includes a module for managing the package version. + +Modules: + - version: Manages the package versioning. +""" diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 2955c71..48abae5 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -1,3 +1,16 @@ +"""Versioning utilities for the Mailjet REST API client. + +This module defines the current version of the Mailjet client and provides +a helper function, `get_version`, to retrieve the version as a formatted string. + +Attributes: + VERSION (tuple[int, int, int]): A tuple representing the major, minor, and patch + version of the package. + +Functions: + get_version: Returns the version as a string in the format "major.minor.patch". +""" + from __future__ import annotations @@ -5,9 +18,20 @@ def get_version(version: tuple[int, ...] | None = None) -> str: - """ - Calculate package version based on a 3 item tuple. - In addition verify that the tuple contains 3 items + """Calculate package version based on a 3 item tuple. + + In addition, verify that the tuple contains 3 items. + + Parameters: + version (tuple[int, ...], optional): A tuple representing the version of the package. + If not provided, the function will use the VERSION constant. + Default is None. + + Returns: + str: The version as a string in the format "major.minor.patch". + + Raises: + ValueError: If the provided tuple does not contain exactly 3 items. """ if version is None: version = VERSION diff --git a/pyproject.toml b/pyproject.toml index 83e5a6b..1c0593e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,33 +197,20 @@ ignore = [ "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` # pycodestyle (E, W) "CPY001", # Missing copyright notice at top of file - # TODO: Enable docstring lints when they will be available. - "D", - "DOC", + "DOC501", # DOC501 Raised exception `TimeoutError` and `ApiError` missing from docstring "E501", "FBT001", # Boolean-typed positional argument in function definition "FBT002", # Boolean default positional argument in function definition - "INP001", # INP001 File `samples/campaign_sample.py` is part of an implicit namespace package. Add an `__init__.py`. - "PD901", - "PD015", - # pep8-naming (N) - "N802", - "N806", - # TODO: PLE0604 Invalid object in `__all__`, must contain only strings - "PLE0604", + # TODO: Replace with http.HTTPStatus, see https://docs.python.org/3/library/http.html#http-status-codes "PLR2004", # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable "PLR0913", # PLR0913 Too many arguments in function definition (6 > 5) - #"PLR0917", # PLR0917 Too many positional arguments - # TODO: "Remove Q000 it before the next release - "Q000", + "PLR0917", # PLR0917 Too many positional arguments "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') - "RET504", # RET504 Unnecessary assignment to `response` before `return` statement - "RUF012", # TODO:" PT009 Use a regular `assert` instead of unittest-style `assertTrue` "PT009", "S311", # S311 Standard pseudo-random generators are not suitable for cryptographic purposes + # TODO: T201 Replace `print` with logging functions "T201", # T201 `print` found - "UP031", # pyupgrade (UP): Skip for logging: UP031 Use format specifiers instead of percent format ] @@ -289,7 +276,6 @@ ignore-overlong-task-comments = true [tool.ruff.lint.pydocstyle] convention = "google" - [tool.mypy] strict = true # Adapted from this StackOverflow post: diff --git a/test.py b/test.py index b327115..7b03ebc 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,5 @@ +"""A suite of tests for Mailjet API client functionality.""" + import os import random import string @@ -8,7 +10,26 @@ class TestSuite(unittest.TestCase): + """A suite of tests for Mailjet API client functionality. + + This class provides setup and teardown functionality for tests involving the + Mailjet API client, with authentication and client initialization handled + in `setUp`. Each test in this suite operates with the configured Mailjet client + instance to simulate API interactions. + """ + def setUp(self) -> None: + """Set up the test environment by initializing authentication credentials and the Mailjet client. + + This method is called before each test to ensure a consistent testing + environment. It retrieves the API keys from environment variables and + uses them to create an instance of the Mailjet `Client` for authenticated + API interactions. + + Attributes: + - self.auth (tuple[str, str]): A tuple containing the public and private API keys obtained from the environment variables 'MJ_APIKEY_PUBLIC' and 'MJ_APIKEY_PRIVATE' respectively. + - self.client (Client): An instance of the Mailjet Client class, initialized with the provided authentication credentials. + """ self.auth: tuple[str, str] = ( os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"], @@ -16,24 +37,71 @@ def setUp(self) -> None: self.client: Client = Client(auth=self.auth) def test_get_no_param(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts without any parameters. + + It verifies that the response contains 'Data' and 'Count' fields. + + Parameters: + None + """ result: Any = self.client.contact.get().json() self.assertTrue("Data" in result and "Count" in result) def test_get_valid_params(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with a valid parameter 'limit'. + + It verifies that the response contains a count of contacts that is within the range of 0 to 2. + + Parameters: + None + """ result: Any = self.client.contact.get(filters={"limit": 2}).json() self.assertTrue(result["Count"] >= 0 or result["Count"] <= 2) def test_get_invalid_parameters(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with an invalid parameter. + + It verifies that the response contains 'Count' field, demonstrating that invalid parameters are ignored. + + Parameters: + None + """ # invalid parameters are ignored result: Any = self.client.contact.get(filters={"invalid": "false"}).json() self.assertTrue("Count" in result) def test_get_with_data(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with 'data' parameter. + + It verifies that the request is successful (status code 200) and does not use the 'data' parameter. + + Parameters: + None + """ # it shouldn't use data result = self.client.contact.get(data={"Email": "api@mailjet.com"}) self.assertTrue(result.status_code == 200) def test_get_with_action(self) -> None: + """This function tests the functionality of adding a contact to a contact list using the Mailjet API client. + + It first retrieves a contact and a contact list from the API, then adds the contact to the list. + Finally, it verifies that the contact has been successfully added to the list. + + Parameters: + None + + Attributes: + - get_contact (Any): The result of the initial contact retrieval, containing a single contact. + - contact_id (str): The ID of the retrieved contact. + - post_contact (Response): The response from creating a new contact if no contact was found. + - get_contact_list (Any): The result of the contact list retrieval, containing a single contact list. + - list_id (str): The ID of the retrieved contact list. + - post_contact_list (Response): The response from creating a new contact list if no contact list was found. + - data (dict[str, list[dict[str, str]]]): The data for managing contact lists, containing the list ID and action to add the contact. + - result_add_list (Response): The response from adding the contact to the contact list. + - result (Any): The result of retrieving the contact's contact lists, containing the count of contact lists. + """ get_contact: Any = self.client.contact.get(filters={"limit": 1}).json() if get_contact["Count"] != 0: contact_id: str = get_contact["Data"][0]["ID"] @@ -83,6 +151,17 @@ def test_get_with_action(self) -> None: self.assertTrue("Count" in result) def test_get_with_id_filter(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with a specific email address obtained from a previous contact retrieval. + + It verifies that the response contains a contact with the same email address as the one used in the filter. + + Parameters: + None + + Attributes: + - result_contact (Any): The result of the initial contact retrieval, containing a single contact. + - result_contact_with_id (Any): The result of the contact retrieval using the email address from the initial contact as a filter. + """ result_contact: Any = self.client.contact.get(filters={"limit": 1}).json() result_contact_with_id: Any = self.client.contact.get( filter={"Email": result_contact["Data"][0]["Email"]}, @@ -93,10 +172,29 @@ def test_get_with_id_filter(self) -> None: ) def test_post_with_no_param(self) -> None: + """This function tests the behavior of the Mailjet API client when attempting to create a sender with no parameters. + + The function sends a POST request to the Mailjet API endpoint for creating a sender with an empty + data dictionary. It then verifies that the response contains a 'StatusCode' field with a value of 400, + indicating a bad request. This test ensures that the client handles missing required parameters + appropriately. + + Parameters: + None + """ result: Any = self.client.sender.create(data={}).json() self.assertTrue("StatusCode" in result and result["StatusCode"] == 400) def test_client_custom_version(self) -> None: + """This test function verifies the functionality of setting a custom version for the Mailjet API client. + + The function initializes a new instance of the Mailjet Client with custom version "v3.1". + It then asserts that the client's configuration version is correctly set to "v3.1". + Additionally, it verifies that the send endpoint URL in the client's configuration is updated to the correct version. + + Parameters: + None + """ self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.version, "v3.1") self.assertEqual( @@ -105,6 +203,15 @@ def test_client_custom_version(self) -> None: ) def test_user_agent(self) -> None: + """This function tests the user agent configuration of the Mailjet API client. + + The function initializes a new instance of the Mailjet Client with a custom version "v3.1". + It then asserts that the client's user agent is correctly set to "mailjet-apiv3-python/v1.3.5". + This test ensures that the client's user agent is properly configured and includes the correct version information. + + Parameters: + None + """ self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.5") diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..3e9246c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,6 @@ +"""The `tests` package contains unit and integration tests for the Mailjet REST API client. + +This package ensures that all core functionalities of the Mailjet client, including +authentication, API requests, error handling, and response parsing, work as expected. +Each module within this package tests a specific aspect or component of the client. +""" diff --git a/tests/test_client.py b/tests/test_client.py index 2efd2df..35a7943 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,6 +3,7 @@ import json import os import re +from typing import Any import pytest @@ -13,6 +14,16 @@ @pytest.fixture def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: + """This function provides a simple data structure and its encoding for testing purposes. + + Parameters: + None + + Returns: + tuple: A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + """ data: dict[str, list[dict[str, str]]] = { "Data": [{"Name": "first_name", "Value": "John"}] } @@ -22,6 +33,14 @@ def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: @pytest.fixture def client_mj30() -> Client: + """This function creates and returns a Mailjet API client instance for version 3.0. + + Parameters: + None + + Returns: + Client: An instance of the Mailjet API client configured for version 3.0. The client is authenticated using the public and private API keys provided as environment variables. + """ auth: tuple[str, str] = ( os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"], @@ -29,8 +48,40 @@ def client_mj30() -> Client: return Client(auth=auth) +@pytest.fixture +def client_mj30_invalid_auth() -> Client: + """This function creates and returns a Mailjet API client instance for version 3.0, but with invalid authentication credentials. + + Parameters: + None + + Returns: + Client: An instance of the Mailjet API client configured for version 3.0. + The client is authenticated using invalid public and private API keys. + If the client is used to make requests, it will raise a ValueError. + """ + auth: tuple[str, str] = ( + "invalid_public_key", + "invalid_private_key", + ) + return Client(auth=auth) + + @pytest.fixture def client_mj31() -> Client: + """This function creates and returns a Mailjet API client instance for version 3.1. + + Parameters: + None + + Returns: + Client: An instance of the Mailjet API client configured for version 3.1. + The client is authenticated using the public and private API keys provided as environment variables. + + Note: + - The function retrieves the public and private API keys from the environment variables 'MJ_APIKEY_PUBLIC' and 'MJ_APIKEY_PRIVATE' respectively. + - The client is initialized with the provided authentication credentials and the version set to 'v3.1'. + """ auth: tuple[str, str] = ( os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"], @@ -44,6 +95,17 @@ def client_mj31() -> Client: def test_json_data_str_or_bytes_with_ensure_ascii( simple_data: tuple[dict[str, list[dict[str, str]]], str] ) -> None: + """ + This function tests the conversion of structured data into JSON format with the specified encoding settings. + + Parameters: + simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + + Returns: + None: The function does not return any value. It performs assertions to validate the JSON conversion. + """ data, data_encoding = simple_data ensure_ascii: bool = True @@ -60,6 +122,18 @@ def test_json_data_str_or_bytes_with_ensure_ascii( def test_json_data_str_or_bytes_with_ensure_ascii_false( simple_data: tuple[dict[str, list[dict[str, str]]], str] ) -> None: + """This function tests the conversion of structured data into JSON format with the specified encoding settings. + + It specifically tests the case where the 'ensure_ascii' parameter is set to False. + + Parameters: + simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + + Returns: + None: The function does not return any value. It performs assertions to validate the JSON conversion. + """ data, data_encoding = simple_data ensure_ascii: bool = False @@ -76,6 +150,17 @@ def test_json_data_str_or_bytes_with_ensure_ascii_false( def test_json_data_is_none( simple_data: tuple[dict[str, list[dict[str, str]]], str] ) -> None: + """ + This function tests the conversion of structured data into JSON format when the data is None. + + Parameters: + simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + + Returns: + None: The function does not return any value. It performs assertions to validate the JSON conversion. + """ data, data_encoding = simple_data ensure_ascii: bool = True data: dict[str, list[dict[str, str]]] | None = None # type: ignore @@ -91,21 +176,53 @@ def test_json_data_is_none( def test_prepare_url_list_splitting() -> None: - """Test prepare_url: list splitting""" + """This function tests the prepare_url function by splitting a string containing underscores and converting the first letter of each word to uppercase. + + The function then compares the resulting list with an expected list. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It splits the resulting string into a list using the underscore as the delimiter. + - It asserts that the resulting list is equal to the expected list ["contact", "managecontactslists"]. + """ name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") split: list[str] = name.split("_") # noqa: FURB184 assert split == ["contact", "managecontactslists"] def test_prepare_url_first_list_element() -> None: - """Test prepare_url: list splitting, the first element, url, and headers""" + """This function tests the prepare_url function by splitting a string containing underscores, and retrieving the first element of the resulting list. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It splits the resulting string into a list using the underscore as the delimiter. + - It asserts that the first element of the split list is equal to "contact". + """ name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") fname: str = name.split("_")[0] assert fname == "contact" def test_prepare_url_headers_and_url() -> None: - """Test prepare_url: list splitting, the first element, url, and headers""" + """Test the prepare_url function by splitting a string containing underscores, and retrieving the first element of the resulting list. + + Additionally, this test verifies the URL and headers generated by the prepare_url function. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is equal to "https://api.mailjet.com/v3/REST/contact" and that the headers match the expected headers. + """ name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") config: Config = Config(version="v3", api_url="https://api.mailjet.com/") url, headers = config[name] @@ -120,5 +237,179 @@ def test_prepare_url_headers_and_url() -> None: def test_post_with_no_param(client_mj30: Client) -> None: + """Tests a POST request with an empty data payload. + + This test sends a POST request to the 'create' endpoint using an empty dictionary + as the data payload. It checks that the API responds with a 400 status code, + indicating a bad request due to missing required parameters. + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + + Raises: + AssertionError: If "StatusCode" is not in the result or if its value + is not 400. + """ result = client_mj30.sender.create(data={}).json() assert "StatusCode" in result and result["StatusCode"] == 400 + + +def test_get_no_param(client_mj30: Client) -> None: + """Tests a GET request to retrieve contact data without any parameters. + + This test sends a GET request to the 'contact' endpoint without filters or + additional parameters. It verifies that the response includes both "Data" + and "Count" fields, confirming the endpoint returns a valid structure. + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + + Raises: + AssertionError: If "Data" or "Count" are not present in the response. + """ + result: Any = client_mj30.contact.get().json() + assert "Data" in result and "Count" in result + + +def test_client_initialization_with_invalid_api_key( + client_mj30_invalid_auth: Client, +) -> None: + """This function tests the initialization of a Mailjet API client with invalid authentication credentials. + + Parameters: + client_mj30_invalid_auth (Client): An instance of the Mailjet API client configured for version 3.0. + The client is authenticated using invalid public and private API keys. + + Returns: + None: The function does not return any value. It is expected to raise a ValueError when the client is used to make requests. + + Note: + - The function uses the pytest.raises context manager to assert that a ValueError is raised when the client's contact.get() method is called. + """ + with pytest.raises(ValueError): + client_mj30_invalid_auth.contact.get().json() + + +def test_prepare_url_mixed_case_input() -> None: + """Test prepare_url function with mixed case input. + + This function tests the prepare_url function by providing a string with mixed case characters. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is equal to the expected URL and that the headers match the expected headers. + """ + name: str = re.sub(r"[A-Z]", prepare_url, "contact") + config: Config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url == "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_empty_input() -> None: + """Test prepare_url function with empty input. + + This function tests the prepare_url function by providing an empty string as input. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is equal to the expected URL and that the headers match the expected headers. + """ + name = re.sub(r"[A-Z]", prepare_url, "") + config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url == "https://api.mailjet.com/v3/REST/" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_with_numbers_input_bad() -> None: + """Test the prepare_url function with input containing numbers. + + This function tests the prepare_url function by providing a string with numbers. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. + """ + name = re.sub(r"[A-Z]", prepare_url, "contact1_managecontactslists1") + config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url != "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_leading_trailing_underscores_input_bad() -> None: + """Test prepare_url function with input containing leading and trailing underscores. + + This function tests the prepare_url function by providing a string with leading and trailing underscores. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. + """ + name: str = re.sub(r"[A-Z]", prepare_url, "_contact_managecontactslists_") + config: Config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url != "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_mixed_case_input_bad() -> None: + """Test prepare_url function with mixed case input. + + This function tests the prepare_url function by providing a string with mixed case characters. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. + """ + name: str = re.sub(r"[A-Z]", prepare_url, "cOntact") + config: Config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url != "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } diff --git a/tests/test_version.py b/tests/test_version.py index 7eb79dd..e74e9f0 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -23,6 +23,7 @@ def test_get_version_is_none() -> None: def test_get_version() -> None: """Test that package version is string. + Verify that if it's equal to tuple after splitting and mapped to tuple. """ result: str | tuple[int, ...] From f342db8d5db91ad7d1810bb6e130ff77536d174f Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:13:12 +0000 Subject: [PATCH 84/87] Enable debug logging (#111) * Formatting, timezone, type hints, docstrings * Rename logging function * Add tests for debug logging by retrieving message if MESSAGE_ID isn't correct; update pre-commit hooks * Fix duplication of the log handlers * Enable flake8 and add configs to pyproject.toml, apply flake8 linter --- .pre-commit-config.yaml | 38 +++++----- mailjet_rest/client.py | 75 +++++++++++++++---- pyproject.toml | 11 +++ tests/test_client.py | 162 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 248 insertions(+), 38 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9391846..b3381a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,10 +66,16 @@ repos: args: [--write] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.4 + rev: 0.30.0 hooks: - id: check-github-workflows + - repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.4 + hooks: + - id: autopep8 + exclude: ^docs/ + - repo: https://github.com/akaihola/darker rev: v2.1.1 hooks: @@ -85,25 +91,19 @@ repos: - --remove-unused-variable - --ignore-init-module-imports - - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.4 + - repo: https://github.com/pycqa/flake8 + rev: 7.1.1 hooks: - - id: autopep8 + - id: flake8 + additional_dependencies: + - radon + - flake8-docstrings + - Flake8-pyproject exclude: ^docs/ -# - repo: https://github.com/pycqa/flake8 -# rev: 7.1.1 -# hooks: -# - id: flake8 -# additional_dependencies: -# - radon -# - flake8-docstrings -# - Flake8-pyproject -# exclude: ^docs/ - - repo: https://github.com/PyCQA/pylint - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pylint args: @@ -117,7 +117,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: v0.7.2 + rev: v0.8.2 hooks: # Run the linter. - id: ruff @@ -141,12 +141,12 @@ repos: exclude: ^samples/ - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.388 + rev: v1.1.390 hooks: - id: pyright - repo: https://github.com/PyCQA/bandit - rev: 1.7.10 + rev: 1.8.0 hooks: - id: bandit args: ["-c", "pyproject.toml", "-r", "."] @@ -155,7 +155,7 @@ repos: additional_dependencies: [".[toml]"] - repo: https://github.com/crate-ci/typos - rev: v1.27.0 + rev: typos-dict-v0.11.37 hooks: - id: typos diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index d369350..c4d297a 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -31,14 +31,18 @@ import json import logging import re +import sys +from datetime import datetime +from datetime import timezone from re import Match from typing import TYPE_CHECKING from typing import Any +from typing import Callable import requests # type: ignore[import-untyped] from requests.compat import urljoin # type: ignore[import-untyped] -from .utils.version import get_version +from mailjet_rest.utils.version import get_version if TYPE_CHECKING: @@ -548,28 +552,71 @@ def build_url( return url -def parse_response(response: Response, debug: bool = False) -> Any: - """Parse the response from an API request. +def logging_handler( + to_file: bool = False, +) -> logging.Logger: + """Create and configure a logger for logging API requests. - This function extracts the JSON data from the response and logs debug information if the `debug` flag is set to True. + This function creates a logger object and configures it to handle both + standard output (stdout) and a file if the `to_file` parameter is set to True. + The logger is set to log at the DEBUG level and uses a custom formatter to + include the log level and message. Parameters: - response (requests.models.Response): The response object from the API request. - debug (bool, optional): A flag indicating whether debug information should be logged. Defaults to False. + to_file (bool): A flag indicating whether to log to a file. If True, logs will be written to a file. + Defaults to False. Returns: - Any: The JSON data extracted from the response. + logging.Logger: A configured logger object for logging API requests. + """ + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter("%(levelname)s | %(message)s") + + if to_file: + now = datetime.now(tz=timezone.utc) + date_time = now.strftime("%Y%m%d_%H%M%S") + + log_file = f"{date_time}.log" + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setFormatter(formatter) + logger.addHandler(stdout_handler) + + return logger + + +def parse_response( + response: Response, + log: Callable, + debug: bool = False, +) -> Any: + """Parse the response from an API request and return the JSON data. + + Parameters: + response (Response): The response object from the API request. + log (Callable): A function or method that logs debug information. + debug (bool): A flag indicating whether debug mode is enabled. Defaults to False. + + Returns: + Any: The JSON data from the API response. """ data = response.json() if debug: - logging.debug("REQUEST: %s", response.request.url) - logging.debug("REQUEST_HEADERS: %s", response.request.headers) - logging.debug("REQUEST_CONTENT: %s", response.request.body) - - logging.debug("RESPONSE: %s", response.content) - logging.debug("RESP_HEADERS: %s", response.headers) - logging.debug("RESP_CODE: %s", response.status_code) + lgr = log() + lgr.debug("REQUEST: %s", response.request.url) + lgr.debug("REQUEST_HEADERS: %s", response.request.headers) + lgr.debug("REQUEST_CONTENT: %s", response.request.body) + + lgr.debug("RESPONSE: %s", response.content) + lgr.debug("RESP_HEADERS: %s", response.headers) + lgr.debug("RESP_CODE: %s", response.status_code) + # Clear logger handlers to prevent making log duplications + logging.getLogger().handlers.clear() return data diff --git a/pyproject.toml b/pyproject.toml index 1c0593e..a4c9c6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -276,6 +276,17 @@ ignore-overlong-task-comments = true [tool.ruff.lint.pydocstyle] convention = "google" +[tool.flake8] +exclude = ["samples/*"] +# TODO: D100 - create docstrings for modules test_client.py and test_version.py +ignore = ['E501', "D100"] +extend-ignore = "W503" +per-file-ignores = [ + '__init__.py:F401', +] +max-line-length = 88 +count = true + [tool.mypy] strict = true # Adapted from this StackOverflow post: diff --git a/tests/test_client.py b/tests/test_client.py index 35a7943..9c103dc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,20 +1,62 @@ from __future__ import annotations +from functools import partial +import glob import json import os import re +from datetime import datetime +from pathlib import Path from typing import Any import pytest +from _pytest.logging import LogCaptureFixture from mailjet_rest.utils.version import get_version from mailjet_rest import Client -from mailjet_rest.client import prepare_url, Config +from mailjet_rest.client import prepare_url, parse_response, logging_handler, Config + + +def debug_entries() -> tuple[str, str, str, str, str, str, str]: + """Provide a simple tuples with debug entries for testing purposes. + + Parameters: + None + + Returns: + tuple: A tuple containing seven debug entries + """ + entries = ( + "DEBUG", + "REQUEST:", + "REQUEST_HEADERS:", + "REQUEST_CONTENT:", + "RESPONSE:", + "RESP_HEADERS:", + "RESP_CODE:", + ) + return entries + + +def validate_datetime_format(date_text: str, datetime_format: str) -> None: + """Validate the format of a given date string against a specified datetime format. + + Parameters: + date_text (str): The date string to be validated. + datetime_format (str): The datetime format to which the date string should be validated. + + Raises: + ValueError: If the date string does not match the specified datetime format. + """ + try: + datetime.strptime(date_text, datetime_format) + except ValueError: + raise ValueError("Incorrect data format, should be %Y%m%d_%H%M%S") @pytest.fixture def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: - """This function provides a simple data structure and its encoding for testing purposes. + """Provide a simple data structure and its encoding for testing purposes. Parameters: None @@ -33,7 +75,7 @@ def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: @pytest.fixture def client_mj30() -> Client: - """This function creates and returns a Mailjet API client instance for version 3.0. + """Create and return a Mailjet API client instance for version 3.0. Parameters: None @@ -50,7 +92,7 @@ def client_mj30() -> Client: @pytest.fixture def client_mj30_invalid_auth() -> Client: - """This function creates and returns a Mailjet API client instance for version 3.0, but with invalid authentication credentials. + """Create and return a Mailjet API client instance for version 3.0, but with invalid authentication credentials. Parameters: None @@ -69,7 +111,7 @@ def client_mj30_invalid_auth() -> Client: @pytest.fixture def client_mj31() -> Client: - """This function creates and returns a Mailjet API client instance for version 3.1. + """Create and return a Mailjet API client instance for version 3.1. Parameters: None @@ -413,3 +455,113 @@ def test_prepare_url_mixed_case_input_bad() -> None: "Content-type": "application/json", "User-agent": f"mailjet-apiv3-python/v{get_version()}", } + + +def test_debug_logging_to_stdout_has_all_debug_entries( + client_mj30: Client, + caplog: LogCaptureFixture, +) -> None: + """This function tests the debug logging to stdout, ensuring that all debug entries are present. + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + caplog (LogCaptureFixture): A fixture for capturing log entries. + """ + result = client_mj30.contact.get() + parse_response(result, lambda: logging_handler(to_file=False), debug=True) + + assert result.status_code == 200 + assert len(caplog.records) == 6 + assert all(x in caplog.text for x in debug_entries()) + + +def test_debug_logging_to_stdout_has_all_debug_entries_when_unknown_or_not_found( + client_mj30: Client, + caplog: LogCaptureFixture, +) -> None: + """This function tests the debug logging to stdout, ensuring that all debug entries are present. + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + caplog (LogCaptureFixture): A fixture for capturing log entries. + """ + # A wrong "cntact" endpoint to get 400 "Unknown resource" error message + result = client_mj30.cntact.get() + parse_response(result, lambda: logging_handler(to_file=False), debug=True) + + assert 400 <= result.status_code <= 404 + assert len(caplog.records) == 8 + assert all(x in caplog.text for x in debug_entries()) + + +def test_debug_logging_to_stdout_when_retrieve_message_with_id_type_mismatch( + client_mj30: Client, + caplog: LogCaptureFixture, +) -> None: + """This function tests the debug logging to stdout by retrieving message if id type mismatch, ensuring that all debug entries are present. + + GET https://api.mailjet.com/v3/REST/message/$MESSAGE_ID + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + caplog (LogCaptureFixture): A fixture for capturing log entries. + """ + _id = "*************" # $MESSAGE_ID with all "*" will cause "Incorrect ID provided - ID type mismatch" (Error 400). + result = client_mj30.message.get(_id) + parse_response(result, lambda: logging_handler(to_file=False), debug=True) + + assert result.status_code == 400 + assert len(caplog.records) == 8 + assert all(x in caplog.text for x in debug_entries()) + + +def test_debug_logging_to_stdout_when_retrieve_message_with_object_not_found( + client_mj30: Client, + caplog: LogCaptureFixture, +) -> None: + """This function tests the debug logging to stdout by retrieving message if object not found, ensuring that all debug entries are present. + + GET https://api.mailjet.com/v3/REST/message/$MESSAGE_ID + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + caplog (LogCaptureFixture): A fixture for capturing log entries. + """ + _id = "0000000000000" # $MESSAGE_ID with all zeros "0" will cause "Object not found" (Error 404). + result = client_mj30.message.get(_id) + parse_response(result, lambda: logging_handler(to_file=False), debug=True) + + assert result.status_code == 404 + assert len(caplog.records) == 8 + assert all(x in caplog.text for x in debug_entries()) + + +def test_debug_logging_to_log_file( + client_mj30: Client, caplog: LogCaptureFixture +) -> None: + """This function tests the debug logging to a log file. + + It sends a GET request to the 'contact' endpoint of the Mailjet API client, parses the response, + logs the debug information to a log file, validates that the log filename has the correct datetime format provided, + and then verifies the existence and removal of the log file. + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + caplog (LogCaptureFixture): A fixture for capturing log entries. + """ + result = client_mj30.contact.get() + parse_response(result, logging_handler, debug=True) + partial(logging_handler, to_file=True) + cwd = Path.cwd() + log_files = glob.glob("*.log") + for log_file in log_files: + log_file_name = Path(log_file).stem + validate_datetime_format(log_file_name, "%Y%m%d_%H%M%S") + log_file_path = os.path.join(cwd, log_file) + + assert result.status_code == 200 + assert Path(log_file_path).exists() + + print(f"Removing log file {log_file}...") + Path(log_file_path).unlink() + print(f"The log file {log_file} has been removed.") From 4ae456efd8be8c1c13aef43e2b3303ee06ef128c Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Sat, 18 Jan 2025 06:46:13 +0000 Subject: [PATCH 85/87] docs: Update README (#114) --- .pre-commit-config.yaml | 16 ++++++------- Makefile | 13 +++++++---- README.md | 52 +++++++++++++++++++++++++++++++++++++---- environment-dev.yaml | 2 +- pyproject.toml | 2 +- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3381a3..9eaf0e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: args: [--write] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.0 hooks: - id: check-github-workflows @@ -103,21 +103,21 @@ repos: - repo: https://github.com/PyCQA/pylint - rev: v3.3.2 + rev: v3.3.3 hooks: - id: pylint args: - --exit-zero - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py38-plus, --keep-runtime-typing] - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: v0.8.2 + rev: v0.9.2 hooks: # Run the linter. - id: ruff @@ -131,7 +131,7 @@ repos: - id: refurb - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.1 hooks: - id: mypy args: @@ -141,12 +141,12 @@ repos: exclude: ^samples/ - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.390 + rev: v1.1.392.post0 hooks: - id: pyright - repo: https://github.com/PyCQA/bandit - rev: 1.8.0 + rev: 1.8.2 hooks: - id: bandit args: ["-c", "pyproject.toml", "-r", "."] @@ -155,7 +155,7 @@ repos: additional_dependencies: [".[toml]"] - repo: https://github.com/crate-ci/typos - rev: typos-dict-v0.11.37 + rev: dictgen-v0.3.1 hooks: - id: typos diff --git a/Makefile b/Makefile index bb111c1..48a3ef5 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,11 @@ environment: ## handles environment creation conda env create -f environment.yaml --name $(CONDA_ENV_NAME) --yes conda run --name $(CONDA_ENV_NAME) pip install . +environment-dev: ## Handles environment creation + conda env create -n $(CONDA_ENV_NAME)-dev -y --file environment-dev.yml + conda run --name $(CONDA_ENV_NAME)-dev pip install -e . + $(CONDA_ACTIVATE) $(CONDA_ENV_NAME)-dev + install: clean ## install the package to the active Python's site-packages pip install . @@ -100,12 +105,12 @@ dist: clean ## builds source and wheel package dev: clean ## install the package's development version to a fresh environment conda env create -f environment.yaml --name $(CONDA_ENV_NAME) --yes conda run --name $(CONDA_ENV_NAME) pip install -e . - $(CONDA_ACTIVATE) $(CONDA_ENV_NAME) && pre-commit install + $(CONDA_ACTIVATE) $(CONDA_ENV_NAME) dev-full: clean ## install the package's development version to a fresh environment - conda env create -f environment-dev.yaml --name $(CONDA_ENV_NAME) --yes - conda run --name $(CONDA_ENV_NAME) pip install -e . - $(CONDA_ACTIVATE) $(CONDA_ENV_NAME) && pre-commit install + conda env create -f environment-dev.yaml --name $(CONDA_ENV_NAME)-dev --yes + conda run --name $(CONDA_ENV_NAME)-dev pip install -e . + $(CONDA_ACTIVATE) $(CONDA_ENV_NAME)-dev && pre-commit install pre-commit: ## runs pre-commit against files. NOTE: older files are disabled in the pre-commit config. diff --git a/README.md b/README.md index 8670dcc..e2d942f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ Check out all the resources and Python code examples in the official [Mailjet Do ## Table of contents - [Compatibility](#compatibility) +- [Requirements](#requirements) + - [Build backend](#build-backend) + - [Runtime](#runtime) - [Installation](#installation) - [Authentication](#authentication) - [Make your first call](#make-your-first-call) @@ -42,18 +45,57 @@ Check out all the resources and Python code examples in the official [Mailjet Do ## Compatibility -This library officially supports the following Python versions: +This library `mailjet_rest` officially supports the following Python versions: - - v2.7 - - v3.5 - - v3.6+ + - v3.9+ + +It's tested up to 3.12 (including). + +## Requirements + +### Build backend + +To build the `mailjet_rest` package you need `setuptools` (as a build backend) and `wheel`. + +### Runtime + +At runtime the package requires only `requests`. ## Installation Use the below code to install the wrapper: ``` bash -(sudo) pip install mailjet_rest +git clone https://github.com/mailjet/mailjet-apiv3-python +cd mailjet-apiv3-python +``` + +``` bash +pip install . +``` + +or using `conda` and `make` on Unix platforms: + +``` bash +make install +``` + +### For development + +on Linux or macOS: + +- A basic environment with a minimum number of dependencies: + +``` bash +make dev +conda activate mailjet +``` + +- A full dev environment: + +``` bash +make dev-full +conda activate mailjet-dev ``` ## Authentication diff --git a/environment-dev.yaml b/environment-dev.yaml index 04e3f94..9b619c3 100644 --- a/environment-dev.yaml +++ b/environment-dev.yaml @@ -1,5 +1,5 @@ --- -name: mailjet_dev +name: mailjet-dev channels: - defaults dependencies: diff --git a/pyproject.toml b/pyproject.toml index a4c9c6e..5c752a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ mailjet_rest = ["py.typed", "*.pyi"] [project] name = "mailjet_rest" -version = "1.3.5" +version = "1.4.0rc1" description = "Mailjet V3 API wrapper" authors = [ { name = "starenka", email = "starenka0@gmail.com" }, From 1e478ef61fe3c48b30797fb8be57d7a988bd5c00 Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Sat, 18 Jan 2025 07:04:29 +0000 Subject: [PATCH 86/87] Add a conda recipe (#115) * ci: Add a conda recipe --- conda.recipe/meta.yaml | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 conda.recipe/meta.yaml diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml new file mode 100644 index 0000000..ca59c3f --- /dev/null +++ b/conda.recipe/meta.yaml @@ -0,0 +1,53 @@ +{% set pyproject = load_file_data('../pyproject.toml', from_recipe_dir=True) %} +{% set project = pyproject['project'] %} + +{% set name = project['name'] %} +{% set version = project['version'] %} + +package: + name: {{ name|lower }} + version: {{ version }} + +source: + path: .. + +build: + number: 0 + skip: True # [py<39] + script: {{ PYTHON }} -m pip install . --no-deps --no-build-isolation -vv + +requirements: + host: + - python + - pip + {% for dep in pyproject['build-system']['requires'] %} + - {{ dep.lower() }} + {% endfor %} + run: + - python + - requests >=2.32.3 + +test: + imports: + - mailjet_rest + source_files: + - tests/test_client.py + requires: + - pip + - pytest + commands: + - pip check + # TODO: Add environment variables for tests + #- pytest tests/test_client.py -vv + +about: + home: {{ project['urls']['Homepage'] }} + dev_url: {{ project['urls']['Repository'] }} + doc_url: {{ project['urls']['Documentation'] }} + summary: {{ project['description'] }} + # TODO: Add the description + # description: | + # + license: {{ project['license']['text'] }} + license_family: {{ project['license']['text'] }} + license_file: LICENSE From df816739d4ca6372da09773a7860746e8ff2a4b4 Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:36:57 +0300 Subject: [PATCH 87/87] Release 1.4.0 (#117) (#118) * Improve CI Automation and package management (#116) * ci: Fix conda.recipe, add samples * ci: Disable TestPyPI job; Add samples to stdist * docs: Update CHANGELOG, fix Python support versions in README * docs: Fix urls * docs: Update badges * Release v1.4.0 --- .gitattributes | 14 ++ .github/ISSUE_TEMPLATE/bug_report.yml | 73 +++++++ .github/ISSUE_TEMPLATE/documentation.yml | 32 +++ .github/ISSUE_TEMPLATE/feature_request.yml | 57 ++++++ .github/workflows/commit_checks.yaml | 13 +- .github/workflows/issue-triage.yml | 47 +++++ .github/workflows/pr_validation.yml | 28 +++ .github/workflows/publish.yml | 84 ++++++++ .pre-commit-config.yaml | 67 +++---- .travis.yml | 23 --- CHANGELOG.md | 128 ++++++++---- Makefile | 7 +- README.md | 218 +++++++++++++-------- conda.recipe/meta.yaml | 17 +- environment-dev.yaml | 42 ++-- mailjet_rest/_version.py | 1 + mailjet_rest/utils/version.py | 27 ++- pyproject.toml | 72 ++++++- test.py | 2 +- 19 files changed, 717 insertions(+), 235 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/workflows/issue-triage.yml create mode 100644 .github/workflows/pr_validation.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .travis.yml create mode 100644 mailjet_rest/_version.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8c24181 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Automatically detect text files and perform LF normalization +* text=auto + +# Source files should have Unix line endings +*.py text eol=lf +*.md text eol=lf +*.toml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +# Exclude some files from exporting +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..90b657b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,73 @@ +name: 🐛 Bug Report +description: Create a bug report. +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: version + attributes: + label: Version + description: What version of our package are you running? + placeholder: ex. 1.0.0 + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you using? + options: + - Windows + - macOS + - Linux + - Other + validations: + required: true + - type: dropdown + id: python-version + attributes: + label: Python Version + description: What Python version are you using? + options: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Install package '...' + 2. Run command '...' + 3. See error + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code. + render: shell + - type: textarea + id: additional + attributes: + label: Additional information + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..13365b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,32 @@ +name: Documentation +description: Create a documentation-related issue. +labels: + - type::documentation +body: + - type: markdown + attributes: + value: | + > [!NOTE] + > Documentation requests that are incomplete or missing information may be closed as inactionable. + - type: checkboxes + id: checks + attributes: + label: Checklist + description: Please confirm and check all of the following options. + options: + - label: I added a descriptive title + required: true + - label: I searched open reports and couldn't find a duplicate + required: true + - type: textarea + id: what + attributes: + label: What happened? + description: Mention here any typos, broken links, or missing, incomplete, or outdated information that you have noticed in the docs. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Include any additional information (or screenshots) that you think would be valuable. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..2cf6658 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,57 @@ +name: 🚀 Feature Request +description: Suggest an idea for this project +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest a new feature! + + > [!NOTE] + > Feature requests that are incomplete or missing information may be closed as inactionable. + - type: checkboxes + id: checks + attributes: + label: Checklist + description: Please confirm and check all of the following options. + options: + - label: I added a descriptive title + required: true + - label: I searched open requests and couldn't find a duplicate + required: true + - type: textarea + id: problem + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of the problem. Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this feature to you? + options: + - Low (nice to have) + - Medium + - High (would significantly improve my workflow) + validations: + required: true diff --git a/.github/workflows/commit_checks.yaml b/.github/workflows/commit_checks.yaml index 3993ccd..10a1a83 100644 --- a/.github/workflows/commit_checks.yaml +++ b/.github/workflows/commit_checks.yaml @@ -13,6 +13,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' # Specify a Python version explicitly - uses: pre-commit/action@v3.0.1 test: @@ -25,13 +27,14 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - python-version: ["3.9", "3.10", "3.11", "3.12"] - #environment: mailjet + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: MJ_APIKEY_PUBLIC: ${{ secrets.MJ_APIKEY_PUBLIC }} MJ_APIKEY_PRIVATE: ${{ secrets.MJ_APIKEY_PRIVATE }} steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Get full history with tags (required for setuptools-scm) - uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python-version }} @@ -41,7 +44,7 @@ jobs: - name: Install the package run: | - pip install -e . + pip install . conda info - name: Test package imports - run: python -c "import mailjet_rest; print('mailjet_rest version is', mailjet_rest.utils.version.get_version())" + run: python -c "import mailjet_rest" diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 0000000..e500793 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,47 @@ +name: Issue Triage + +on: + issues: + types: [opened, labeled, unlabeled, reopened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Initial triage + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issue = context.payload.issue; + // Check if this is a bug report + if (issue.title.includes('[Bug]')) { + // Add priority labels based on content + if (issue.body.toLowerCase().includes('crash') || + issue.body.toLowerCase().includes('data loss')) { + github.rest.issues.addLabels({ + issue_number: issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['priority: high'] + }); + } + // Assign to bug team + github.rest.issues.addAssignees({ + issue_number: issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + assignees: [''] + }); + } + // Check if this is a feature request + if (issue.title.includes('[Feature]')) { + github.rest.issues.addLabels({ + issue_number: issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['needs-review'] + }); + } diff --git a/.github/workflows/pr_validation.yml b/.github/workflows/pr_validation.yml new file mode 100644 index 0000000..a699c1f --- /dev/null +++ b/.github/workflows/pr_validation.yml @@ -0,0 +1,28 @@ +name: PR Validation + +on: + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Build package + run: | + pip install --upgrade build setuptools wheel setuptools-scm + python -m build + + - name: Test installation + run: | + pip install dist/*.whl + python -c "from importlib.metadata import version; print(version('mailjet_rest'))" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..14b1b1c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,84 @@ +name: Publish Package + +on: + push: + tags: ['v*'] # Triggers on any tag push + release: + types: [published] # Triggers when a GitHub release is published + workflow_dispatch: # Manual trigger + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write # Required for trusted publishing + contents: read + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build tools + run: pip install --upgrade build setuptools wheel setuptools-scm twine + + - name: Extract version + id: get_version + run: | + # Get clean version from the tag or release + if [[ "${{ github.event_name }}" == "release" ]]; then + # For releases, get the version from the release tag + TAG_NAME="${{ github.event.release.tag_name }}" + else + # For tags, get version from the tag + TAG_NAME="${{ github.ref_name }}" + fi + + # Remove 'v' prefix + VERSION=$(echo $TAG_NAME | sed 's/^v//') + + # Check if this is a stable version (no rc, alpha, beta, dev, etc.) + if [[ $TAG_NAME =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "IS_STABLE=true" >> $GITHUB_ENV + else + echo "IS_STABLE=false" >> $GITHUB_ENV + fi + + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Build package + run: | + # Force clean version + export SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION + python -m build + + - name: Check dist + run: | + ls -alh + twine check dist/* + + # Always publish to TestPyPI for all tags and releases + # TODO: Enable it later. +# - name: Publish to TestPyPI +# uses: pypa/gh-action-pypi-publish@release/v1 +# with: +# repository-url: https://test.pypi.org/legacy/ +# password: ${{ secrets.TEST_PYPI_API_TOKEN }} +# skip-existing: true +# verbose: true + + # Only publish to PyPI for stable GitHub releases (no RC/alpha/beta) + - name: Publish to PyPI + # TODO: Enable '&& env.IS_STABLE == 'true' only publish to PyPI for stable GitHub releases (no RC/alpha/beta) + if: github.event_name == 'release' #&& env.IS_STABLE == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + verbose: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9eaf0e5..2758d73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,14 +32,11 @@ repos: # git-lfs rather than committing them directly to the git history - id: check-added-large-files args: [ "--maxkb=500" ] - - id: fix-byte-order-marker - - id: check-case-conflict # Fails if there are any ">>>>>" lines in files due to merge conflicts. - id: check-merge-conflict # ensure syntaxes are valid - id: check-toml - id: debug-statements - - id: detect-private-key # Makes sure files end in a newline and only a newline; - id: end-of-file-fixer - id: mixed-line-ending @@ -60,18 +57,19 @@ repos: - id: gitlint - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell args: [--write] + exclude: ^tests - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.0 + rev: 0.33.0 hooks: - id: check-github-workflows - - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.4 + - repo: https://github.com/hhatto/autopep8 + rev: v2.3.2 hooks: - id: autopep8 exclude: ^docs/ @@ -92,7 +90,7 @@ repos: - --ignore-init-module-imports - repo: https://github.com/pycqa/flake8 - rev: 7.1.1 + rev: 7.2.0 hooks: - id: flake8 additional_dependencies: @@ -103,7 +101,7 @@ repos: - repo: https://github.com/PyCQA/pylint - rev: v3.3.3 + rev: v3.3.7 hooks: - id: pylint args: @@ -113,11 +111,11 @@ repos: rev: v3.19.1 hooks: - id: pyupgrade - args: [--py38-plus, --keep-runtime-typing] + args: [--py39-plus, --keep-runtime-typing] - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: v0.9.2 + rev: v0.11.8 hooks: # Run the linter. - id: ruff @@ -131,7 +129,7 @@ repos: - id: refurb - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.15.0 hooks: - id: mypy args: @@ -141,12 +139,12 @@ repos: exclude: ^samples/ - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.392.post0 + rev: v1.1.400 hooks: - id: pyright - repo: https://github.com/PyCQA/bandit - rev: 1.8.2 + rev: 1.8.3 hooks: - id: bandit args: ["-c", "pyproject.toml", "-r", "."] @@ -155,36 +153,17 @@ repos: additional_dependencies: [".[toml]"] - repo: https://github.com/crate-ci/typos - rev: dictgen-v0.3.1 + # Important: Keep an exact version (not v1) to avoid pre-commit issues + # after running 'pre-commit autoupdate' + rev: v1.31.1 hooks: - id: typos -# - repo: https://github.com/google/yapf -# rev: v0.40.2 -# hooks: -# - id: yapf -# name: "yapf" -# additional_dependencies: [toml] - -# - repo: https://github.com/mattseymour/pre-commit-pytype -# rev: '2023.5.8' -# hooks: -# - id: pytype - -# - repo: https://github.com/executablebooks/mdformat -# rev: 0.7.17 -# hooks: -# - id: mdformat -# additional_dependencies: -# # gfm = GitHub Flavored Markdown -# - mdformat-gfm -# - mdformat-black - -# - repo: https://github.com/adrienverge/yamllint.git -# rev: v1.35.1 -# hooks: -# - id: yamllint -# #args: [--strict] -# entry: yamllint -# language: python -# types: [ file, yaml ] + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.22 + hooks: + - id: mdformat + additional_dependencies: + # gfm = GitHub Flavored Markdown + - mdformat-gfm + - mdformat-black diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 315ac8f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -language: python -python: -- '2.7' -- '3.5' -- '3.8' -deploy: - provider: pypi - user: api.mailjet - password: - secure: gI8oqHszMz4dTfs8g6aKKZgOMN04HK26mPrrsO2iz6gsUB1b7zM8RwLSWCPH2YjmWaAF9Bx5VrQ6YayLHX3svRWUsWDgBGGCcsogKEfIEmMasKMnjGrVJRS5fb0j/r9d4qXaITdoWeRh7/HBdv6u0gAwMV/fyrT6XiiDwWxbYfyLwbJx9IH79Gkyh6Xwq8nkexd5oQDybE0IQ94Q9/6t0CHbmCLrTinR1q0diV/bKK5+zSqiCmatzIq5kMre32iurs4LPZ4QTWJkQqZSh4UzQAJ42hjH8w+YrDxB4kRjOraqDNKHnr06dcOUFK9bJ0/wZU2MJh0II3hrOo2MUphhChcrKQDWuyCPTcZhDUxVBhcy0wWz3RfxXZ442VxHpCrNyNesxmJZZc9AxMfzerQvCIFGeeJCTunUz5I245FNIsnIqDJ5IiM9Ukn/WIfHsqh5fVVejQotfl6YVmlJ+nmxNQnBIXPWlpg4CXJN/TvFTjLsPeWxGZZJKrMGsVmEqHgtH+31pn/ec6frXbx8cDDcoAteEantQWcM3AJj32BL8mA8RpwWONKfFNKFpWyCtoTq+ERbWjv7EvKgudLqX50WPi2uaGFp8Ik6tWAi7hcgNYneyAhOHv7FZJf582n1dTSB0Fv4izphsmJKjIC02EGXYtKcDFA0m1zWLuqLavuFFO0= - on: - tags: true - repo: mailjet/mailjet-apiv3-python -install: pip install -r requirements.txt -script: python test.py -notifications: - slack: - secure: Y6dTB+/gVfUn7z84et8HAS8gb21+FJoeDmz9f6Lkc9VUk8fL5lqrsV5cZedEWH1sXzzbN8GN2qOoLDKFa+HpxxA/27urSSVKYxYzqdZQZtVZBRSjVzflDJAUzUk1zfn1SmkhMJZO+EF7SOGLBSqbR2LgIzac6P6lKP6dtI8ZjS++O8VtaPOWPzxb8AuByOO6YR7sl6ZWO3OK30ovEOnAAjeiAC8nD0isjZylzhABQj2AKSelFf0zczMTJBMlB8gIXh+gf+sIG4RZknDb2qnFxstHU8p8FD54KdfkOA4W8UTGBc+5DUx0z8fVIo9JcBwIbbFIlcvZ5LY7atu7baBQO7jURXk7uI/w9gDFoE+NhrqF06NqeCHgySc3KSNf2NaxCiKSD5K4WDxumV16KliMqJzjumwG08+/TqgIzC9/Aj/b+5skxzhWkRP/H5iz8sOqCPHGk1pk7B1PwxuOHwIuzeLQ9aELYQgFIBVMqM2bFLLRk9eRKwpkpagApyAhTjV3hqAmUanL6fiPqIar0f4QQ5vFFronFCp08hVQ/b+WA8cLJ6r3FqBiP0XUzea8gsdbgXO4HB/hXtWx7oWzuPL39kx+aJHdt/rSI90shkk7dzhI3FQy+iSC5QnfVhNPyc3OprzWNlNcZjbI7ElS106LjrKcr8ilAMgpuFa06wuybxo= -env: - global: - - secure: x2agpUFUHBYcTgmuHeYuqxuNIMFz1V0PG9a09BpDmYrj0PT2BsecaMCtsWyWOUkzTb9S2/fE6oJnWjyNewp936px681K1aBa5GKKCkifnbxkRRY0dmUkFcrgWG8JZH9hXjzmgjUzwTefJ+xKkeVCySRCNgFm6MP0TmzKZWHjAxOHIHz1akJBfuAhCyVJU0grw7JSAPHSwMj/7erj7ub1pEvWjRjtJe9pMYTtrSYJRtt2qCkSRNK3/i+YZ7mKfigcaIcDgdaPYw0osUqB1DZHJK4RtC80pHkZNAosWIIMU7WohpYijGTIZRPjY4le3uiGqTJcaDhL84Jnmervczd62Iqhf9TE6YfKYksnfbk4YYy9GcTPggBSUV0COB9vApncVENZgeFUO8nSodL0ru5PO0KHcX6Er2zMdqieKseuJY1iLDidyqbTHqR9bZ9dck9CLA4KfLUycwaXYHb9c35iA9caOn+Xm7G1UvVSh3uE63FBsG1ubATe6pIfWZFgVo6zbH52hOTXQhLeximCD7CiSPef69iNzH9jTi9LU04hTuk69X8BWqvHTiYwPoLhdQueXwy2/LaOvSGex8qS5cGYBnWkeb0y99Qm6WpQjCkZhmcDvSXBlBnbbhm6Xc4NnLdX8cp/nVBYrYsgVsf2T7vUnmJBngv1Vsqa4+r9QRxBbxk= - - secure: Dwmug98uIhT3i03nai2Ufa0fBJS4i4zYclqywZJkqEVS84UrKDTZOLJFCDgJv3NlZ29BxvrnH8QqPHn9J6hJb++DGT6Ak7vonfuMYedxNAADn/RjBun+esQsPQYdko/GwGw1Z6kPucBT6Jp3Owd+GDjbnTXFmwSfag/sbTc38lp3mgvDAvcUiyTmQD0hsbHLw6GrpRte9BpSfnJ5ImCgz9nacuZPC084FQcMi0PyV4Ug9L32jnfVePwFD1pCjZpc41m3M2SP89XBjUrBRwyDzgTS8jxt82LN3mQKpxl3EguWLYuKCrK0vJQphrWlhUcRdAkCroSgTtWa9MAGb1DT44OqcbAGEPcMqJKNR7MtzsFnPq6QLO5IDXn05OmOZi1BChxXpENq/gmj2c8OmPkndZTHBwfG3sP0iuC7xrlSBr/++UWQYi07oIYgWfpxYMnXUS0ZEHOUg4oCvQBydB+mcVgRinN2cgXzwfOoVvAofjuT52mzyMHJz9OBqbcyqnCdEZzxS8cn7LTf6ZoWrFFSDQg5Fn8G/6U+yzPT87iz3di0uuCU7VsltXMs25HXdLLQxxbqBkPOhawtIUuzxmq/1eLBb0R+GzjgAsqMJl40iJdnDiYaOAgb6LE6Ix7II80I5nfLkE/60N0AgzZHJy6c/RhYy39eI7D9HQBDLcyW8AM= diff --git a/CHANGELOG.md b/CHANGELOG.md index 0471a47..6f9468a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,73 @@ -# Changelog +# CHANGELOG -## [Unreleased](https://github.com/mailjet/mailjet-apiv3-python/tree/HEAD) +We [keep a changelog.](http://keepachangelog.com/) -[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.3.2...HEAD) +## [Unreleased] + +## [1.4.0] - 2025-05-07 + +### Added + +- Enabled debug logging +- Support for Python >=3.9,\<3.14 +- CI Automation (commit checks, issue-triage, PR validation, publish) +- Issue templates for bug report, feature request, documentation +- Type hinting +- Docstrings +- A conda recipe (meta.yaml) +- Package management stuff: pyproject.toml, .editorconfig, .gitattributes, .gitignore, .pre-commit-config.yaml, Makefile, environment-dev.yaml, environment.yaml +- Linting: py.typed +- New samples +- New tests + +### Changed + +- Update README.md +- Improved tests + +### Removed + +- requirements.txt and setup.py are replaced by pyproject.toml +- .travis.yml was obsolete + +### Pull Requests Merged + +- [PR_105](https://github.com/mailjet/mailjet-apiv3-python/pull/105) - Update README.md, fix the license name in setup.py +- [PR_107](https://github.com/mailjet/mailjet-apiv3-python/pull/107) - PEP8 enabled +- [PR_108](https://github.com/mailjet/mailjet-apiv3-python/pull/108) - Support py>=39,\=3.9,\<3.14 -It's tested up to 3.12 (including). +It's tested up to 3.13 (including). ## Requirements -### Build backend +### Build backend dependencies -To build the `mailjet_rest` package you need `setuptools` (as a build backend) and `wheel`. +To build the `mailjet_rest` package from the sources you need `setuptools` (as a build backend), `wheel`, and `setuptools-scm`. -### Runtime +### Runtime dependencies -At runtime the package requires only `requests`. +At runtime the package requires only `requests >=2.32.3`. + +### Test dependencies + +For running test you need `pytest >=7.0.0` at least. +Make sure to provide the environment variables from [Authentication](#authentication). ## Installation -Use the below code to install the wrapper: +### pip install + +Use the below code to install the the wrapper: + +```bash +pip install mailjet-rest +``` + +#### git clone & pip install locally -``` bash +Use the below code to install the wrapper locally by cloning this repository: + +```bash git clone https://github.com/mailjet/mailjet-apiv3-python cd mailjet-apiv3-python ``` -``` bash +```bash pip install . ``` -or using `conda` and `make` on Unix platforms: +#### conda & make + +Use the below code to install it locally by `conda` and `make` on Unix platforms: -``` bash +```bash make install ``` ### For development +#### Using conda + on Linux or macOS: - A basic environment with a minimum number of dependencies: -``` bash +```bash make dev conda activate mailjet ``` - A full dev environment: -``` bash +```bash make dev-full conda activate mailjet-dev ``` @@ -107,7 +137,7 @@ export MJ_APIKEY_PUBLIC='your api key' export MJ_APIKEY_PRIVATE='your api secret' ``` -Initialize your [Mailjet][mailjet] client: +Initialize your [Mailjet] client: ```python # import the mailjet wrapper @@ -115,8 +145,8 @@ from mailjet_rest import Client import os # Get your environment Mailjet keys -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) ``` @@ -128,16 +158,17 @@ Here's an example on how to send an email: ```python from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) data = { - 'FromEmail': '$SENDER_EMAIL', - 'FromName': '$SENDER_NAME', - 'Subject': 'Your email flight plan!', - 'Text-part': 'Dear passenger, welcome to Mailjet! May the delivery force be with you!', - 'Html-part': '

Dear passenger, welcome to Mailjet!
May the delivery force be with you!', - 'Recipients': [{'Email': '$RECIPIENT_EMAIL'}] + "FromEmail": "$SENDER_EMAIL", + "FromName": "$SENDER_NAME", + "Subject": "Your email flight plan!", + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!", + "Html-part": '

Dear passenger, welcome to Mailjet!
May the delivery force be with you!', + "Recipients": [{"Email": "$RECIPIENT_EMAIL"}], } result = mailjet.send.create(data=data) print(result.status_code) @@ -156,16 +187,16 @@ The Mailjet API is spread among three distinct versions: Since most Email API endpoints are located under `v3`, it is set as the default one and does not need to be specified when making your request. For the others you need to specify the version using `version`. For example, if using Send API `v3.1`: -``` python +```python # import the mailjet wrapper from mailjet_rest import Client import os # Get your environment Mailjet keys -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] -mailjet = Client(auth=(api_key, api_secret), version='v3.1') +mailjet = Client(auth=(api_key, api_secret), version="v3.1") ``` For additional information refer to our [API Reference](https://dev.mailjet.com/reference/overview/versioning/). @@ -175,7 +206,7 @@ For additional information refer to our [API Reference](https://dev.mailjet.com/ The default base domain name for the Mailjet API is `api.mailjet.com`. You can modify this base URL by setting a value for `api_url` in your call: ```python -mailjet = Client(auth=(api_key, api_secret),api_url="https://api.us.mailjet.com/") +mailjet = Client(auth=(api_key, api_secret), api_url="https://api.us.mailjet.com/") ``` If your account has been moved to Mailjet's **US architecture**, the URL value you need to set is `https://api.us.mailjet.com`. @@ -188,9 +219,7 @@ For example, to reach `statistics/link-click` path you should call `statistics_l ```python # GET `statistics/link-click` mailjet = Client(auth=(api_key, api_secret)) -filters = { - 'CampaignId': 'xxxxxxx' -} +filters = {"CampaignId": "xxxxxxx"} result = mailjet.statistics_linkClick.get(filters=filters) print(result.status_code) print(result.json()) @@ -198,6 +227,11 @@ print(result.json()) ## Request examples +### Full list of supported endpoints + +> [!IMPORTANT]\ +> This is a full list of supported endpoints this wrapper provides [samples](samples) + ### POST request #### Simple POST request @@ -206,14 +240,14 @@ print(result.json()) """ Create a new contact: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -data = { - 'Email': 'Mister@mailjet.com' -} +data = {"Email": "Mister@mailjet.com"} result = mailjet.contact.create(data=data) print(result.status_code) print(result.json()) @@ -225,23 +259,19 @@ print(result.json()) """ Manage the subscription status of a contact to multiple lists: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id = '$ID' +id = "$ID" data = { - 'ContactsLists': [ - { - "ListID": "$ListID_1", - "Action": "addnoforce" - }, - { - "ListID": "$ListID_2", - "Action": "addforce" - } - ] + "ContactsLists": [ + {"ListID": "$ListID_1", "Action": "addnoforce"}, + {"ListID": "$ListID_2", "Action": "addforce"}, + ] } result = mailjet.contact_managecontactslists.create(id=id, data=data) print(result.status_code) @@ -256,10 +286,12 @@ print(result.json()) """ Retrieve all contacts: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) result = mailjet.contact.get() print(result.status_code) @@ -272,13 +304,15 @@ print(result.json()) """ Retrieve all contacts that are not in the campaign exclusion list: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) filters = { - 'IsExcludedFromCampaigns': 'false', + "IsExcludedFromCampaigns": "false", } result = mailjet.contact.get(filters=filters) print(result.status_code) @@ -317,12 +351,14 @@ print(result.json()) """ Retrieve a specific contact ID: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id_ = 'Contact_ID' +id_ = "Contact_ID" result = mailjet.contact.get(id=id_) print(result.status_code) print(result.json()) @@ -338,23 +374,19 @@ Here's an example of a `PUT` request: """ Update the contact properties for a contact: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id_ = '$CONTACT_ID' +id_ = "$CONTACT_ID" data = { - 'Data': [ - { - "Name": "first_name", - "value": "John" - }, - { - "Name": "last_name", - "value": "Smith" - } - ] + "Data": [ + {"Name": "first_name", "value": "John"}, + {"Name": "last_name", "value": "Smith"}, + ] } result = mailjet.contactdata.update(id=id_, data=data) print(result.status_code) @@ -371,17 +403,23 @@ Here's an example of a `DELETE` request: """ Delete an email template: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id_ = 'Template_ID' +id_ = "Template_ID" result = mailjet.template.delete(id=id_) print(result.status_code) print(result.json()) ``` +## License + +[MIT](https://choosealicense.com/licenses/mit/) + ## Contribute Mailjet loves developers. You can be part of this project! @@ -397,3 +435,13 @@ Feel free to ask anything, and contribute: - Commit, push, open a pull request and voila. If you have suggestions on how to improve the guides, please submit an issue in our [Official API Documentation repo](https://github.com/mailjet/api-documentation). + +## Contributors + +- [@diskovod](https://github.com/diskovod) +- [@DanyilNefodov](https://github.com/DanyilNefodov) +- [@skupriienko](https://github.com/skupriienko) + +[api_credential]: https://app.mailjet.com/account/apikeys +[doc]: http://dev.mailjet.com/guides/?python# +[mailjet]: (http://www.mailjet.com/) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index ca59c3f..a160382 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -2,7 +2,10 @@ {% set project = pyproject['project'] %} {% set name = project['name'] %} -{% set version = project['version'] %} +{% set version_match = load_file_regex( + load_file=name.replace('-', '_') + "/_version.py", + regex_pattern='__version__ = "(.+)"') %} +{% set version = version_match[1] %} package: name: {{ name|lower }} @@ -15,6 +18,8 @@ build: number: 0 skip: True # [py<39] script: {{ PYTHON }} -m pip install . --no-deps --no-build-isolation -vv + script_env: + - SETUPTOOLS_SCM_PRETEND_VERSION={{ version }} requirements: host: @@ -25,11 +30,15 @@ requirements: {% endfor %} run: - python - - requests >=2.32.3 + {% for dep in pyproject['project']['dependencies'] %} + - {{ dep.lower() }} + {% endfor %} test: imports: - mailjet_rest + - mailjet_rest.utils + - samples source_files: - tests/test_client.py requires: @@ -38,7 +47,7 @@ test: commands: - pip check # TODO: Add environment variables for tests - #- pytest tests/test_client.py -vv + - pytest tests/test_client.py -vv about: home: {{ project['urls']['Homepage'] }} @@ -49,5 +58,5 @@ about: # description: | # license: {{ project['license']['text'] }} - license_family: {{ project['license']['text'] }} + license_family: {{ project['license']['text'].split('-')[0] }} license_file: LICENSE diff --git a/environment-dev.yaml b/environment-dev.yaml index 9b619c3..cb42993 100644 --- a/environment-dev.yaml +++ b/environment-dev.yaml @@ -6,49 +6,57 @@ dependencies: - python >=3.9 # build & host deps - pip + - setuptools-scm + - # PyPI publishing only + - python-build # runtime deps - requests >=2.32.3 # tests + - conda-forge::pyfakefs + - coverage >=4.5.4 - pytest + - pytest-benchmark - pytest-cov - pytest-xdist - - conda-forge::pyfakefs - - pytest-benchmark - - coverage >=4.5.4 # linters & formatters - - pylint - autopep8 - black + - flake8 - isort - make + - conda-forge::monkeytype + - mypy + - pandas-stubs + - pep8-naming - pycodestyle - pydocstyle - - flake8 - - pep8-naming - - yapf + - pylint + - pyright + - radon - ruff - - mypy - toml - types-requests - - pandas-stubs - - pyright - - radon + - yapf # other - - pre-commit - conda - conda-build - jsonschema - - types-jsonschema + - pre-commit - python-dotenv >=0.19.2 + - types-jsonschema - pip: - - pyupgrade - - bandit - autoflake8 - - refurb + - bandit + - docconvert - monkeytype - pyment >=0.3.3 - pytype - - vulture + - pyupgrade + # refurb doesn't support py39 + #- refurb - scalene >=1.3.16 - snakeviz - typos + - vulture + # PyPI publishing only + - twine diff --git a/mailjet_rest/_version.py b/mailjet_rest/_version.py new file mode 100644 index 0000000..d60e0c1 --- /dev/null +++ b/mailjet_rest/_version.py @@ -0,0 +1 @@ +__version__ = "1.4.0" \ No newline at end of file diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 48abae5..814c6a2 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -13,8 +13,33 @@ from __future__ import annotations +import re -VERSION: tuple[int, int, int] = (1, 3, 5) +from mailjet_rest._version import __version__ as package_version + + +def clean_version(version_str: str) -> tuple[int, ...]: + """Clean package version string into 3 item tuple. + + Parameters: + version_str (str): A string of the package version. + + Returns: + tuple: A tuple representing the version of the package. + """ + if not version_str: + return 0, 0, 0 + # Extract just the X.Y.Z part using regex + match = re.match(r"^(\d+\.\d+\.\d+)", version_str) + if match: + version_part = match.group(1) + return tuple(map(int, version_part.split("."))) + + return 0, 0, 0 # type: ignore[unreachable] + + +# VERSION is a tuple of integers (1, 3, 2). +VERSION: tuple[int, ...] = clean_version(package_version) def get_version(version: tuple[int, ...] | None = None) -> str: diff --git a/pyproject.toml b/pyproject.toml index 5c752a3..9289de6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,39 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=61.0", "wheel", "setuptools-scm"] build-backend = "setuptools.build_meta" +[tool.setuptools_scm] +version_scheme = "no-guess-dev" # Don't try to guess next version +local_scheme = "no-local-version" +# Ignore uncommitted changes +git_describe_command = "git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*'" +write_to = "mailjet_rest/_version.py" +write_to_template = '__version__ = "{version}"' +#fallback_version = "X.Y.ZrcN.postN.devN" # Explicit fallback + +[tool.setuptools] +py-modules = ["mailjet_rest._version"] + [tool.setuptools.packages.find] -include = ["mailjet_rest", "mailjet_rest.*", "tests", "test.py"] +include = ["mailjet_rest", "mailjet_rest.*", "samples", "tests", "test.py"] [tool.setuptools.package-data] mailjet_rest = ["py.typed", "*.pyi"] [project] -name = "mailjet_rest" -version = "1.4.0rc1" +name = "mailjet-rest" +dynamic = ["version"] description = "Mailjet V3 API wrapper" authors = [ { name = "starenka", email = "starenka0@gmail.com" }, { name = "Mailjet", email = "api@mailjet.com" }, ] +maintainers = [ + {name = "Serhii Kupriienko", email = "kupriienko.serhii@gmail.com"} +] license = {text = "MIT"} +# TODO: Enable license-files when setuptools >=77.0.0 will be available +#license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.9" @@ -34,7 +51,6 @@ classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", @@ -84,11 +100,17 @@ linting = [ "pyment>=0.3.3", # for generating docstrings "pytype", # a static type checker for any type hints you have put in your code "radon", + "safety", # Checks installed dependencies for known vulnerabilities and licenses. "vulture", # env variables "python-dotenv>=0.19.2", ] +docs = [ + "docconvert", + "pyment>=0.3.3", # for generating docstrings +] + metrics = [ "pystra", # provides functionalities to enable structural reliability analysis "wily>=1.2.0", # a tool for reporting code complexity metrics @@ -114,7 +136,7 @@ other = ["toml"] [tool.black] line-length = 88 -target-version = ["py39", "py310", "py311", "py312"] +target-version = ["py39", "py310", "py311", "py312", "py313"] skip-string-normalization = false skip-magic-trailing-comma = false extend-exclude = ''' @@ -170,6 +192,9 @@ line-length = 88 # Assume Python 3.9. target-version = "py39" +# Enumerate all fixed violations. +show-fixes = true + [tool.ruff.lint] # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or @@ -276,6 +301,11 @@ ignore-overlong-task-comments = true [tool.ruff.lint.pydocstyle] convention = "google" +[tool.pydocstyle] +convention = "google" +match = ".*.py" +match_dir = '^samples/' + [tool.flake8] exclude = ["samples/*"] # TODO: D100 - create docstrings for modules test_client.py and test_version.py @@ -287,6 +317,17 @@ per-file-ignores = [ max-line-length = 88 count = true +[tool.yapf] +based_on_style = "facebook" +SPLIT_BEFORE_BITWISE_OPERATOR = true +SPLIT_BEFORE_ARITHMETIC_OPERATOR = true +SPLIT_BEFORE_LOGICAL_OPERATOR = true +SPLIT_BEFORE_DOT = true + +[tool.yapfignore] +ignore_patterns = [ +] + [tool.mypy] strict = true # Adapted from this StackOverflow post: @@ -334,7 +375,6 @@ exclude = [ ] # Configuring error messages -show-fixes = true show_error_context = false show_column_numbers = false show_error_codes = true @@ -393,3 +433,21 @@ subprocess = [ "subprocess.check_call", "subprocess.check_output" ] + +[tool.coverage.run] +source_pkgs = ["mailjet_rest"] +branch = true +parallel = true +omit = [ + "samples/*", +] + +[tool.coverage.paths] +tests = ["tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/test.py b/test.py index 7b03ebc..d232aaa 100644 --- a/test.py +++ b/test.py @@ -213,7 +213,7 @@ def test_user_agent(self) -> None: None """ self.client = Client(auth=self.auth, version="v3.1") - self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.5") + self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.4.0") if __name__ == "__main__":