├── .editorconfig ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── conda.recipe └── meta.yaml ├── environment-dev.yaml ├── environment.yaml ├── mailjet_rest ├── __init__.py ├── _version.py ├── client.py └── utils │ ├── __init__.py │ └── version.py ├── py.typed ├── pyproject.toml ├── samples ├── __init__.py ├── campaign_sample.py ├── contacts_sample.py ├── email_template_sample.py ├── getting_started_sample.py ├── new_sample.py ├── parse_api_sample.py ├── segments_sample.py ├── sender_and_domain_samples.py └── statistic_sample.py ├── test.py └── tests ├── __init__.py ├── test_client.py └── test_version.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.bat] 17 | indent_style = tab 18 | end_of_line = crlf 19 | 20 | [LICENSE] 21 | insert_final_newline = false 22 | 23 | [Makefile] 24 | indent_style = tab 25 | trim_trailing_whitespace = false 26 | 27 | [*.{yaml,yml}] 28 | indent_size = 2 29 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Apply to all files without committing: 3 | # pre-commit run --all-files 4 | # Update this file: 5 | # pre-commit autoupdate 6 | exclude: ^(.*/versioneer\.py|.*/_version\.py|.*/.*\.svg) 7 | 8 | ci: 9 | autofix_commit_msg: | 10 | [pre-commit.ci] auto fixes from pre-commit.com hooks 11 | 12 | for more information, see https://pre-commit.ci 13 | autofix_prs: true 14 | autoupdate_branch: '' 15 | autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' 16 | autoupdate_schedule: weekly 17 | skip: [] 18 | submodules: false 19 | 20 | repos: 21 | - repo: https://github.com/pre-commit/pre-commit-hooks 22 | rev: v5.0.0 23 | hooks: 24 | - id: check-ast 25 | - id: check-builtin-literals 26 | - id: fix-byte-order-marker 27 | - id: check-case-conflict 28 | - id: check-docstring-first 29 | - id: check-vcs-permalinks 30 | # Fail if staged files are above a certain size. 31 | # To add a large file, use 'git lfs track ; git add to track large files with 32 | # git-lfs rather than committing them directly to the git history 33 | - id: check-added-large-files 34 | args: [ "--maxkb=500" ] 35 | # Fails if there are any ">>>>>" lines in files due to merge conflicts. 36 | - id: check-merge-conflict 37 | # ensure syntaxes are valid 38 | - id: check-toml 39 | - id: debug-statements 40 | # Makes sure files end in a newline and only a newline; 41 | - id: end-of-file-fixer 42 | - id: mixed-line-ending 43 | # Trims trailing whitespace. Allow a single space on the end of .md lines for hard line breaks. 44 | - id: trailing-whitespace 45 | args: [ --markdown-linebreak-ext=md ] 46 | # Sort requirements in requirements.txt files. 47 | - id: requirements-txt-fixer 48 | # Prevent committing directly to trunk 49 | - id: no-commit-to-branch 50 | args: [ "--branch=master" ] 51 | # Detects the presence of private keys 52 | - id: detect-private-key 53 | 54 | - repo: https://github.com/jorisroovers/gitlint 55 | rev: v0.19.1 56 | hooks: 57 | - id: gitlint 58 | 59 | - repo: https://github.com/codespell-project/codespell 60 | rev: v2.4.1 61 | hooks: 62 | - id: codespell 63 | args: [--write] 64 | exclude: ^tests 65 | 66 | - repo: https://github.com/python-jsonschema/check-jsonschema 67 | rev: 0.33.0 68 | hooks: 69 | - id: check-github-workflows 70 | 71 | - repo: https://github.com/hhatto/autopep8 72 | rev: v2.3.2 73 | hooks: 74 | - id: autopep8 75 | exclude: ^docs/ 76 | 77 | - repo: https://github.com/akaihola/darker 78 | rev: v2.1.1 79 | hooks: 80 | - id: darker 81 | 82 | - repo: https://github.com/PyCQA/autoflake 83 | rev: v2.3.1 84 | hooks: 85 | - id: autoflake 86 | args: 87 | - --in-place 88 | - --remove-all-unused-imports 89 | - --remove-unused-variable 90 | - --ignore-init-module-imports 91 | 92 | - repo: https://github.com/pycqa/flake8 93 | rev: 7.2.0 94 | hooks: 95 | - id: flake8 96 | additional_dependencies: 97 | - radon 98 | - flake8-docstrings 99 | - Flake8-pyproject 100 | exclude: ^docs/ 101 | 102 | 103 | - repo: https://github.com/PyCQA/pylint 104 | rev: v3.3.7 105 | hooks: 106 | - id: pylint 107 | args: 108 | - --exit-zero 109 | 110 | - repo: https://github.com/asottile/pyupgrade 111 | rev: v3.19.1 112 | hooks: 113 | - id: pyupgrade 114 | args: [--py39-plus, --keep-runtime-typing] 115 | 116 | - repo: https://github.com/charliermarsh/ruff-pre-commit 117 | # Ruff version. 118 | rev: v0.11.8 119 | hooks: 120 | # Run the linter. 121 | - id: ruff 122 | args: [--fix, --exit-non-zero-on-fix] 123 | # Run the formatter. 124 | - id: ruff-format 125 | 126 | - repo: https://github.com/dosisod/refurb 127 | rev: v2.0.0 128 | hooks: 129 | - id: refurb 130 | 131 | - repo: https://github.com/pre-commit/mirrors-mypy 132 | rev: v1.15.0 133 | hooks: 134 | - id: mypy 135 | args: 136 | [ 137 | --config-file=./pyproject.toml, 138 | ] 139 | exclude: ^samples/ 140 | 141 | - repo: https://github.com/RobertCraigie/pyright-python 142 | rev: v1.1.400 143 | hooks: 144 | - id: pyright 145 | 146 | - repo: https://github.com/PyCQA/bandit 147 | rev: 1.8.3 148 | hooks: 149 | - id: bandit 150 | args: ["-c", "pyproject.toml", "-r", "."] 151 | # ignore all tests, not just tests data 152 | exclude: ^tests/ 153 | additional_dependencies: [".[toml]"] 154 | 155 | - repo: https://github.com/crate-ci/typos 156 | # Important: Keep an exact version (not v1) to avoid pre-commit issues 157 | # after running 'pre-commit autoupdate' 158 | rev: v1.31.1 159 | hooks: 160 | - id: typos 161 | 162 | - repo: https://github.com/executablebooks/mdformat 163 | rev: 0.7.22 164 | hooks: 165 | - id: mdformat 166 | additional_dependencies: 167 | # gfm = GitHub Flavored Markdown 168 | - mdformat-gfm 169 | - mdformat-black 170 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | We [keep a changelog.](http://keepachangelog.com/) 4 | 5 | ## [Unreleased] 6 | 7 | ## [1.4.0] - 2025-05-07 8 | 9 | ### Added 10 | 11 | - Enabled debug logging 12 | - Support for Python >=3.9,\<3.14 13 | - CI Automation (commit checks, issue-triage, PR validation, publish) 14 | - Issue templates for bug report, feature request, documentation 15 | - Type hinting 16 | - Docstrings 17 | - A conda recipe (meta.yaml) 18 | - Package management stuff: pyproject.toml, .editorconfig, .gitattributes, .gitignore, .pre-commit-config.yaml, Makefile, environment-dev.yaml, environment.yaml 19 | - Linting: py.typed 20 | - New samples 21 | - New tests 22 | 23 | ### Changed 24 | 25 | - Update README.md 26 | - Improved tests 27 | 28 | ### Removed 29 | 30 | - requirements.txt and setup.py are replaced by pyproject.toml 31 | - .travis.yml was obsolete 32 | 33 | ### Pull Requests Merged 34 | 35 | - [PR_105](https://github.com/mailjet/mailjet-apiv3-python/pull/105) - Update README.md, fix the license name in setup.py 36 | - [PR_107](https://github.com/mailjet/mailjet-apiv3-python/pull/107) - PEP8 enabled 37 | - [PR_108](https://github.com/mailjet/mailjet-apiv3-python/pull/108) - Support py>=39,\=3.9,\<3.14 62 | 63 | It's tested up to 3.13 (including). 64 | 65 | ## Requirements 66 | 67 | ### Build backend dependencies 68 | 69 | To build the `mailjet_rest` package from the sources you need `setuptools` (as a build backend), `wheel`, and `setuptools-scm`. 70 | 71 | ### Runtime dependencies 72 | 73 | At runtime the package requires only `requests >=2.32.3`. 74 | 75 | ### Test dependencies 76 | 77 | For running test you need `pytest >=7.0.0` at least. 78 | Make sure to provide the environment variables from [Authentication](#authentication). 79 | 80 | ## Installation 81 | 82 | ### pip install 83 | 84 | Use the below code to install the the wrapper: 85 | 86 | ```bash 87 | pip install mailjet-rest 88 | ``` 89 | 90 | #### git clone & pip install locally 91 | 92 | Use the below code to install the wrapper locally by cloning this repository: 93 | 94 | ```bash 95 | git clone https://github.com/mailjet/mailjet-apiv3-python 96 | cd mailjet-apiv3-python 97 | ``` 98 | 99 | ```bash 100 | pip install . 101 | ``` 102 | 103 | #### conda & make 104 | 105 | Use the below code to install it locally by `conda` and `make` on Unix platforms: 106 | 107 | ```bash 108 | make install 109 | ``` 110 | 111 | ### For development 112 | 113 | #### Using conda 114 | 115 | on Linux or macOS: 116 | 117 | - A basic environment with a minimum number of dependencies: 118 | 119 | ```bash 120 | make dev 121 | conda activate mailjet 122 | ``` 123 | 124 | - A full dev environment: 125 | 126 | ```bash 127 | make dev-full 128 | conda activate mailjet-dev 129 | ``` 130 | 131 | ## Authentication 132 | 133 | The Mailjet Email API uses your API and Secret keys for authentication. [Grab][api_credential] and save your Mailjet API credentials. 134 | 135 | ```bash 136 | export MJ_APIKEY_PUBLIC='your api key' 137 | export MJ_APIKEY_PRIVATE='your api secret' 138 | ``` 139 | 140 | Initialize your [Mailjet] client: 141 | 142 | ```python 143 | # import the mailjet wrapper 144 | from mailjet_rest import Client 145 | import os 146 | 147 | # Get your environment Mailjet keys 148 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 149 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 150 | 151 | mailjet = Client(auth=(api_key, api_secret)) 152 | ``` 153 | 154 | ## Make your first call 155 | 156 | Here's an example on how to send an email: 157 | 158 | ```python 159 | from mailjet_rest import Client 160 | import os 161 | 162 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 163 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 164 | mailjet = Client(auth=(api_key, api_secret)) 165 | data = { 166 | "FromEmail": "$SENDER_EMAIL", 167 | "FromName": "$SENDER_NAME", 168 | "Subject": "Your email flight plan!", 169 | "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!", 170 | "Html-part": '

Dear passenger, welcome to Mailjet!
May the delivery force be with you!', 171 | "Recipients": [{"Email": "$RECIPIENT_EMAIL"}], 172 | } 173 | result = mailjet.send.create(data=data) 174 | print(result.status_code) 175 | print(result.json()) 176 | ``` 177 | 178 | ## Client / Call Configuration Specifics 179 | 180 | ### API Versioning 181 | 182 | The Mailjet API is spread among three distinct versions: 183 | 184 | - `v3` - The Email API 185 | - `v3.1` - Email Send API v3.1, which is the latest version of our Send API 186 | - `v4` - SMS API (not supported in Python) 187 | 188 | 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`: 189 | 190 | ```python 191 | # import the mailjet wrapper 192 | from mailjet_rest import Client 193 | import os 194 | 195 | # Get your environment Mailjet keys 196 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 197 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 198 | 199 | mailjet = Client(auth=(api_key, api_secret), version="v3.1") 200 | ``` 201 | 202 | For additional information refer to our [API Reference](https://dev.mailjet.com/reference/overview/versioning/). 203 | 204 | ### Base URL 205 | 206 | 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: 207 | 208 | ```python 209 | mailjet = Client(auth=(api_key, api_secret), api_url="https://api.us.mailjet.com/") 210 | ``` 211 | 212 | If your account has been moved to Mailjet's **US architecture**, the URL value you need to set is `https://api.us.mailjet.com`. 213 | 214 | ### URL path 215 | 216 | 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. 217 | For example, to reach `statistics/link-click` path you should call `statistics_linkClick` attribute of python client. 218 | 219 | ```python 220 | # GET `statistics/link-click` 221 | mailjet = Client(auth=(api_key, api_secret)) 222 | filters = {"CampaignId": "xxxxxxx"} 223 | result = mailjet.statistics_linkClick.get(filters=filters) 224 | print(result.status_code) 225 | print(result.json()) 226 | ``` 227 | 228 | ## Request examples 229 | 230 | ### Full list of supported endpoints 231 | 232 | > [!IMPORTANT]\ 233 | > This is a full list of supported endpoints this wrapper provides [samples](samples) 234 | 235 | ### POST request 236 | 237 | #### Simple POST request 238 | 239 | ```python 240 | """ 241 | Create a new contact: 242 | """ 243 | 244 | from mailjet_rest import Client 245 | import os 246 | 247 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 248 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 249 | mailjet = Client(auth=(api_key, api_secret)) 250 | data = {"Email": "Mister@mailjet.com"} 251 | result = mailjet.contact.create(data=data) 252 | print(result.status_code) 253 | print(result.json()) 254 | ``` 255 | 256 | #### Using actions 257 | 258 | ```python 259 | """ 260 | Manage the subscription status of a contact to multiple lists: 261 | """ 262 | 263 | from mailjet_rest import Client 264 | import os 265 | 266 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 267 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 268 | mailjet = Client(auth=(api_key, api_secret)) 269 | id = "$ID" 270 | data = { 271 | "ContactsLists": [ 272 | {"ListID": "$ListID_1", "Action": "addnoforce"}, 273 | {"ListID": "$ListID_2", "Action": "addforce"}, 274 | ] 275 | } 276 | result = mailjet.contact_managecontactslists.create(id=id, data=data) 277 | print(result.status_code) 278 | print(result.json()) 279 | ``` 280 | 281 | ### GET Request 282 | 283 | #### Retrieve all objects 284 | 285 | ```python 286 | """ 287 | Retrieve all contacts: 288 | """ 289 | 290 | from mailjet_rest import Client 291 | import os 292 | 293 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 294 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 295 | mailjet = Client(auth=(api_key, api_secret)) 296 | result = mailjet.contact.get() 297 | print(result.status_code) 298 | print(result.json()) 299 | ``` 300 | 301 | #### Using filtering 302 | 303 | ```python 304 | """ 305 | Retrieve all contacts that are not in the campaign exclusion list: 306 | """ 307 | 308 | from mailjet_rest import Client 309 | import os 310 | 311 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 312 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 313 | mailjet = Client(auth=(api_key, api_secret)) 314 | filters = { 315 | "IsExcludedFromCampaigns": "false", 316 | } 317 | result = mailjet.contact.get(filters=filters) 318 | print(result.status_code) 319 | print(result.json()) 320 | ``` 321 | 322 | #### Using pagination 323 | 324 | 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. 325 | `limit` `int` Limit the response to a select number of returned objects. Default value: `10`. Maximum value: `1000` 326 | `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` 327 | `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` 328 | Next example returns 40 contacts starting from 51th record sorted by `Email` field descendally: 329 | 330 | ```python 331 | import os 332 | from mailjet_rest import Client 333 | 334 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 335 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 336 | mailjet = Client(auth=(api_key, api_secret)) 337 | 338 | filters = { 339 | "limit": 40, 340 | "offset": 50, 341 | "sort": "Email desc", 342 | } 343 | result = mailjet.contact.get(filters=filters) 344 | print(result.status_code) 345 | print(result.json()) 346 | ``` 347 | 348 | #### Retrieve a single object 349 | 350 | ```python 351 | """ 352 | Retrieve a specific contact ID: 353 | """ 354 | 355 | from mailjet_rest import Client 356 | import os 357 | 358 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 359 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 360 | mailjet = Client(auth=(api_key, api_secret)) 361 | id_ = "Contact_ID" 362 | result = mailjet.contact.get(id=id_) 363 | print(result.status_code) 364 | print(result.json()) 365 | ``` 366 | 367 | ### PUT request 368 | 369 | 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. 370 | 371 | Here's an example of a `PUT` request: 372 | 373 | ```python 374 | """ 375 | Update the contact properties for a contact: 376 | """ 377 | 378 | from mailjet_rest import Client 379 | import os 380 | 381 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 382 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 383 | mailjet = Client(auth=(api_key, api_secret)) 384 | id_ = "$CONTACT_ID" 385 | data = { 386 | "Data": [ 387 | {"Name": "first_name", "value": "John"}, 388 | {"Name": "last_name", "value": "Smith"}, 389 | ] 390 | } 391 | result = mailjet.contactdata.update(id=id_, data=data) 392 | print(result.status_code) 393 | print(result.json()) 394 | ``` 395 | 396 | ### DELETE request 397 | 398 | Upon a successful `DELETE` request the response will not include a response body, but only a `204 No Content` response code. 399 | 400 | Here's an example of a `DELETE` request: 401 | 402 | ```python 403 | """ 404 | Delete an email template: 405 | """ 406 | 407 | from mailjet_rest import Client 408 | import os 409 | 410 | api_key = os.environ["MJ_APIKEY_PUBLIC"] 411 | api_secret = os.environ["MJ_APIKEY_PRIVATE"] 412 | mailjet = Client(auth=(api_key, api_secret)) 413 | id_ = "Template_ID" 414 | result = mailjet.template.delete(id=id_) 415 | print(result.status_code) 416 | print(result.json()) 417 | ``` 418 | 419 | ## License 420 | 421 | [MIT](https://choosealicense.com/licenses/mit/) 422 | 423 | ## Contribute 424 | 425 | Mailjet loves developers. You can be part of this project! 426 | 427 | This wrapper is a great introduction to the open source world, check out the code! 428 | 429 | Feel free to ask anything, and contribute: 430 | 431 | - Fork the project. 432 | - Create a new branch. 433 | - Implement your feature or bug fix. 434 | - Add documentation to it. 435 | - Commit, push, open a pull request and voila. 436 | 437 | 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). 438 | 439 | ## Contributors 440 | 441 | - [@diskovod](https://github.com/diskovod) 442 | - [@DanyilNefodov](https://github.com/DanyilNefodov) 443 | - [@skupriienko](https://github.com/skupriienko) 444 | 445 | [api_credential]: https://app.mailjet.com/account/apikeys 446 | [doc]: http://dev.mailjet.com/guides/?python# 447 | [mailjet]: (http://www.mailjet.com/) 448 | -------------------------------------------------------------------------------- /conda.recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set pyproject = load_file_data('../pyproject.toml', from_recipe_dir=True) %} 2 | {% set project = pyproject['project'] %} 3 | 4 | {% set name = project['name'] %} 5 | {% set version_match = load_file_regex( 6 | load_file=name.replace('-', '_') + "/_version.py", 7 | regex_pattern='__version__ = "(.+)"') %} 8 | {% set version = version_match[1] %} 9 | 10 | package: 11 | name: {{ name|lower }} 12 | version: {{ version }} 13 | 14 | source: 15 | path: .. 16 | 17 | build: 18 | number: 0 19 | skip: True # [py<39] 20 | script: {{ PYTHON }} -m pip install . --no-deps --no-build-isolation -vv 21 | script_env: 22 | - SETUPTOOLS_SCM_PRETEND_VERSION={{ version }} 23 | 24 | requirements: 25 | host: 26 | - python 27 | - pip 28 | {% for dep in pyproject['build-system']['requires'] %} 29 | - {{ dep.lower() }} 30 | {% endfor %} 31 | run: 32 | - python 33 | {% for dep in pyproject['project']['dependencies'] %} 34 | - {{ dep.lower() }} 35 | {% endfor %} 36 | 37 | test: 38 | imports: 39 | - mailjet_rest 40 | - mailjet_rest.utils 41 | - samples 42 | source_files: 43 | - tests/test_client.py 44 | requires: 45 | - pip 46 | - pytest 47 | commands: 48 | - pip check 49 | # TODO: Add environment variables for tests 50 | - pytest tests/test_client.py -vv 51 | 52 | about: 53 | home: {{ project['urls']['Homepage'] }} 54 | dev_url: {{ project['urls']['Repository'] }} 55 | doc_url: {{ project['urls']['Documentation'] }} 56 | summary: {{ project['description'] }} 57 | # TODO: Add the description 58 | # description: | 59 | # 60 | license: {{ project['license']['text'] }} 61 | license_family: {{ project['license']['text'].split('-')[0] }} 62 | license_file: LICENSE 63 | -------------------------------------------------------------------------------- /environment-dev.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: mailjet-dev 3 | channels: 4 | - defaults 5 | dependencies: 6 | - python >=3.9 7 | # build & host deps 8 | - pip 9 | - setuptools-scm 10 | - # PyPI publishing only 11 | - python-build 12 | # runtime deps 13 | - requests >=2.32.3 14 | # tests 15 | - conda-forge::pyfakefs 16 | - coverage >=4.5.4 17 | - pytest 18 | - pytest-benchmark 19 | - pytest-cov 20 | - pytest-xdist 21 | # linters & formatters 22 | - autopep8 23 | - black 24 | - flake8 25 | - isort 26 | - make 27 | - conda-forge::monkeytype 28 | - mypy 29 | - pandas-stubs 30 | - pep8-naming 31 | - pycodestyle 32 | - pydocstyle 33 | - pylint 34 | - pyright 35 | - radon 36 | - ruff 37 | - toml 38 | - types-requests 39 | - yapf 40 | # other 41 | - conda 42 | - conda-build 43 | - jsonschema 44 | - pre-commit 45 | - python-dotenv >=0.19.2 46 | - types-jsonschema 47 | - pip: 48 | - autoflake8 49 | - bandit 50 | - docconvert 51 | - monkeytype 52 | - pyment >=0.3.3 53 | - pytype 54 | - pyupgrade 55 | # refurb doesn't support py39 56 | #- refurb 57 | - scalene >=1.3.16 58 | - snakeviz 59 | - typos 60 | - vulture 61 | # PyPI publishing only 62 | - twine 63 | -------------------------------------------------------------------------------- /environment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: mailjet 3 | channels: 4 | - defaults 5 | dependencies: 6 | - python >=3.9 7 | # build & host deps 8 | - pip 9 | # runtime deps 10 | - requests >=2.32.3 11 | # tests 12 | - pytest >=7.0.0 13 | # other 14 | - pre-commit 15 | - toml 16 | -------------------------------------------------------------------------------- /mailjet_rest/__init__.py: -------------------------------------------------------------------------------- 1 | """The `mailjet_rest` package provides a Python client for interacting with the Mailjet API. 2 | 3 | This package includes the main `Client` class for handling API requests, along with 4 | utility functions for version management. The package exposes a consistent interface 5 | for Mailjet API operations. 6 | 7 | Attributes: 8 | __version__ (str): The current version of the `mailjet_rest` package. 9 | __all__ (list): Specifies the public API of the package, including `Client` 10 | for API interactions and `get_version` for retrieving version information. 11 | 12 | Modules: 13 | - client: Defines the main API client. 14 | - utils.version: Provides version management functionality. 15 | """ 16 | 17 | from mailjet_rest.client import Client 18 | from mailjet_rest.utils.version import get_version 19 | 20 | 21 | __version__: str = get_version() 22 | 23 | __all__ = ["Client", "get_version"] 24 | -------------------------------------------------------------------------------- /mailjet_rest/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.4.0" -------------------------------------------------------------------------------- /mailjet_rest/client.py: -------------------------------------------------------------------------------- 1 | """This module provides the main client and helper classes for interacting with the Mailjet API. 2 | 3 | The `mailjet_rest.client` module includes the core `Client` class for managing 4 | API requests, configuration, and error handling, as well as utility functions 5 | and classes for building request headers, URLs, and parsing responses. 6 | 7 | Classes: 8 | - Config: Manages configuration settings for the Mailjet API. 9 | - Endpoint: Represents specific API endpoints and provides methods for 10 | common HTTP operations like GET, POST, PUT, and DELETE. 11 | - Client: The main API client for authenticating and making requests. 12 | - ApiError: Base class for handling API-specific errors, with subclasses 13 | for more specific error types (e.g., `AuthorizationError`, `TimeoutError`). 14 | 15 | Functions: 16 | - prepare_url: Prepares URLs for API requests. 17 | - api_call: A helper function that sends HTTP requests to the API and handles 18 | responses. 19 | - build_headers: Builds HTTP headers for the requests. 20 | - build_url: Constructs the full API URL based on endpoint and parameters. 21 | - parse_response: Parses API responses and handles error conditions. 22 | 23 | Exceptions: 24 | - ApiError: Base exception for API errors, with subclasses to represent 25 | specific error types, such as `AuthorizationError`, `TimeoutError`, 26 | `ActionDeniedError`, and `ValidationError`. 27 | """ 28 | 29 | from __future__ import annotations 30 | 31 | import json 32 | import logging 33 | import re 34 | import sys 35 | from datetime import datetime 36 | from datetime import timezone 37 | from re import Match 38 | from typing import TYPE_CHECKING 39 | from typing import Any 40 | from typing import Callable 41 | 42 | import requests # type: ignore[import-untyped] 43 | from requests.compat import urljoin # type: ignore[import-untyped] 44 | 45 | from mailjet_rest.utils.version import get_version 46 | 47 | 48 | if TYPE_CHECKING: 49 | from collections.abc import Mapping 50 | 51 | from requests.models import Response # type: ignore[import-untyped] 52 | 53 | 54 | requests.packages.urllib3.disable_warnings() 55 | 56 | 57 | def prepare_url(key: Match[str]) -> str: 58 | """Replace capital letters in the input string with a dash prefix and converts them to lowercase. 59 | 60 | Parameters: 61 | key (Match[str]): A match object representing a substring from the input string. The substring should contain a single capital letter. 62 | 63 | Returns: 64 | str: A string containing a dash followed by the lowercase version of the input capital letter. 65 | """ 66 | char_elem = key.group(0) 67 | if char_elem.isupper(): 68 | return "-" + char_elem.lower() 69 | return "" 70 | 71 | 72 | class Config: 73 | """Configuration settings for interacting with the Mailjet API. 74 | 75 | This class stores and manages API configuration details, including the API URL, 76 | version, and user agent string. It provides methods for initializing these settings 77 | and generating endpoint-specific URLs and headers as required for API interactions. 78 | 79 | Attributes: 80 | DEFAULT_API_URL (str): The default base URL for Mailjet API requests. 81 | API_REF (str): Reference URL for Mailjet's API documentation. 82 | version (str): API version to use, defaulting to 'v3'. 83 | user_agent (str): User agent string including the package version for tracking. 84 | """ 85 | 86 | DEFAULT_API_URL: str = "https://api.mailjet.com/" 87 | API_REF: str = "https://dev.mailjet.com/email-api/v3/" 88 | version: str = "v3" 89 | user_agent: str = "mailjet-apiv3-python/v" + get_version() 90 | 91 | def __init__(self, version: str | None = None, api_url: str | None = None) -> None: 92 | """Initialize a new Config instance with specified or default API settings. 93 | 94 | This initializer sets the API version and base URL. If no version or URL 95 | is provided, it defaults to the predefined class values. 96 | 97 | Parameters: 98 | - version (str | None): The API version to use. If None, the default version ('v3') is used. 99 | - api_url (str | None): The base URL for API requests. If None, the default URL (DEFAULT_API_URL) is used. 100 | """ 101 | if version is not None: 102 | self.version = version 103 | self.api_url = api_url or self.DEFAULT_API_URL 104 | 105 | def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: 106 | """Retrieve the API endpoint URL and headers for a given key. 107 | 108 | This method builds the URL and headers required for specific API interactions. 109 | The URL is adjusted based on the API version, and additional headers are 110 | appended depending on the endpoint type. Specific keys modify content-type 111 | for endpoints expecting CSV or plain text. 112 | 113 | Parameters: 114 | - key (str): The name of the API endpoint, which influences URL structure and header configuration. 115 | 116 | Returns: 117 | - tuple[str, dict[str, str]]: A tuple containing the constructed URL and headers required for the specified endpoint. 118 | 119 | Examples: 120 | For the "contactslist_csvdata" key, a URL pointing to 'DATA/' and a 121 | 'Content-type' of 'text/plain' is returned. 122 | For the "batchjob_csverror" key, a URL with 'DATA/' and a 'Content-type' 123 | of 'text/csv' is returned. 124 | """ 125 | # Append version to URL. 126 | # Forward slash is ignored if present in self.version. 127 | url = urljoin(self.api_url, self.version + "/") 128 | headers: dict[str, str] = { 129 | "Content-type": "application/json", 130 | "User-agent": self.user_agent, 131 | } 132 | if key.lower() == "contactslist_csvdata": 133 | url = urljoin(url, "DATA/") 134 | headers["Content-type"] = "text/plain" 135 | elif key.lower() == "batchjob_csverror": 136 | url = urljoin(url, "DATA/") 137 | headers["Content-type"] = "text/csv" 138 | elif key.lower() != "send" and self.version != "v4": 139 | url = urljoin(url, "REST/") 140 | url += key.split("_")[0].lower() 141 | return url, headers 142 | 143 | 144 | class Endpoint: 145 | """A class representing a specific Mailjet API endpoint. 146 | 147 | This class provides methods to perform HTTP requests to a given API endpoint, 148 | including GET, POST, PUT, and DELETE requests. It manages URL construction, 149 | headers, and authentication for interacting with the endpoint. 150 | 151 | Attributes: 152 | - _url (str): The base URL of the endpoint. 153 | - headers (dict[str, str]): The headers to be included in API requests. 154 | - _auth (tuple[str, str] | None): The authentication credentials. 155 | - action (str | None): The specific action to be performed on the endpoint. 156 | 157 | Methods: 158 | - _get: Internal method to perform a GET request. 159 | - get_many: Performs a GET request to retrieve multiple resources. 160 | - get: Performs a GET request to retrieve a specific resource. 161 | - create: Performs a POST request to create a new resource. 162 | - update: Performs a PUT request to update an existing resource. 163 | - delete: Performs a DELETE request to delete a resource. 164 | """ 165 | 166 | def __init__( 167 | self, 168 | url: str, 169 | headers: dict[str, str], 170 | auth: tuple[str, str] | None, 171 | action: str | None = None, 172 | ) -> None: 173 | """Initialize a new Endpoint instance. 174 | 175 | Args: 176 | url (str): The base URL for the endpoint. 177 | headers (dict[str, str]): Headers for API requests. 178 | auth (tuple[str, str] | None): Authentication credentials. 179 | action (str | None): Action to perform on the endpoint, if any. 180 | """ 181 | self._url, self.headers, self._auth, self.action = url, headers, auth, action 182 | 183 | def _get( 184 | self, 185 | filters: Mapping[str, str | Any] | None = None, 186 | action_id: str | None = None, 187 | id: str | None = None, 188 | **kwargs: Any, 189 | ) -> Response: 190 | """Perform an internal GET request to the endpoint. 191 | 192 | Constructs the URL with the provided filters and action_id to retrieve 193 | specific data from the API. 194 | 195 | Parameters: 196 | - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. 197 | - action_id (str | None): The specific action ID for the endpoint to be performed. 198 | - id (str | None): The ID of the specific resource to be retrieved. 199 | - **kwargs (Any): Additional keyword arguments to be passed to the API call. 200 | 201 | Returns: 202 | - Response: The response object from the API call. 203 | """ 204 | return api_call( 205 | self._auth, 206 | "get", 207 | self._url, 208 | headers=self.headers, 209 | action=self.action, 210 | action_id=action_id, 211 | filters=filters, 212 | resource_id=id, 213 | **kwargs, 214 | ) 215 | 216 | def get_many( 217 | self, 218 | filters: Mapping[str, str | Any] | None = None, 219 | action_id: str | None = None, 220 | **kwargs: Any, 221 | ) -> Response: 222 | """Perform a GET request to retrieve multiple resources. 223 | 224 | Parameters: 225 | - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. 226 | - action_id (str | None): The specific action ID to be performed. 227 | - **kwargs (Any): Additional keyword arguments to be passed to the API call. 228 | 229 | Returns: 230 | - Response: The response object from the API call containing multiple resources. 231 | """ 232 | return self._get(filters=filters, action_id=action_id, **kwargs) 233 | 234 | def get( 235 | self, 236 | id: str | None = None, 237 | filters: Mapping[str, str | Any] | None = None, 238 | action_id: str | None = None, 239 | **kwargs: Any, 240 | ) -> Response: 241 | """Perform a GET request to retrieve a specific resource. 242 | 243 | Parameters: 244 | - id (str | None): The ID of the specific resource to be retrieved. 245 | - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. 246 | - action_id (str | None): The specific action ID to be performed. 247 | - **kwargs (Any): Additional keyword arguments to be passed to the API call. 248 | 249 | Returns: 250 | - Response: The response object from the API call containing the specific resource. 251 | """ 252 | return self._get(id=id, filters=filters, action_id=action_id, **kwargs) 253 | 254 | def create( 255 | self, 256 | data: dict | None = None, 257 | filters: Mapping[str, str | Any] | None = None, 258 | id: str | None = None, 259 | action_id: str | None = None, 260 | ensure_ascii: bool = True, 261 | data_encoding: str = "utf-8", 262 | **kwargs: Any, 263 | ) -> Response: 264 | """Perform a POST request to create a new resource. 265 | 266 | Parameters: 267 | - data (dict | None): The data to include in the request body. 268 | - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. 269 | - id (str | None): The ID of the specific resource to be created. 270 | - action_id (str | None): The specific action ID to be performed. 271 | - ensure_ascii (bool): Whether to ensure ASCII characters in the data. 272 | - data_encoding (str): The encoding to be used for the data. 273 | - **kwargs (Any): Additional keyword arguments to be passed to the API call. 274 | 275 | Returns: 276 | - Response: The response object from the API call. 277 | """ 278 | json_data: str | bytes | None = None 279 | if self.headers.get("Content-type") == "application/json" and data is not None: 280 | json_data = json.dumps(data, ensure_ascii=ensure_ascii) 281 | if not ensure_ascii: 282 | json_data = json_data.encode(data_encoding) 283 | return api_call( 284 | self._auth, 285 | "post", 286 | self._url, 287 | headers=self.headers, 288 | resource_id=id, 289 | data=json_data, 290 | action=self.action, 291 | action_id=action_id, 292 | filters=filters, 293 | **kwargs, 294 | ) 295 | 296 | def update( 297 | self, 298 | id: str | None, 299 | data: dict | None = None, 300 | filters: Mapping[str, str | Any] | None = None, 301 | action_id: str | None = None, 302 | ensure_ascii: bool = True, 303 | data_encoding: str = "utf-8", 304 | **kwargs: Any, 305 | ) -> Response: 306 | """Perform a PUT request to update an existing resource. 307 | 308 | Parameters: 309 | - id (str | None): The ID of the specific resource to be updated. 310 | - data (dict | None): The data to be sent in the request body. 311 | - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. 312 | - action_id (str | None): The specific action ID to be performed. 313 | - ensure_ascii (bool): Whether to ensure ASCII characters in the data. 314 | - data_encoding (str): The encoding to be used for the data. 315 | - **kwargs (Any): Additional keyword arguments to be passed to the API call. 316 | 317 | Returns: 318 | - Response: The response object from the API call. 319 | """ 320 | json_data: str | bytes | None = None 321 | if self.headers.get("Content-type") == "application/json" and data is not None: 322 | json_data = json.dumps(data, ensure_ascii=ensure_ascii) 323 | if not ensure_ascii: 324 | json_data = json_data.encode(data_encoding) 325 | return api_call( 326 | self._auth, 327 | "put", 328 | self._url, 329 | resource_id=id, 330 | headers=self.headers, 331 | data=json_data, 332 | action=self.action, 333 | action_id=action_id, 334 | filters=filters, 335 | **kwargs, 336 | ) 337 | 338 | def delete(self, id: str | None, **kwargs: Any) -> Response: 339 | """Perform a DELETE request to delete a resource. 340 | 341 | Parameters: 342 | - id (str | None): The ID of the specific resource to be deleted. 343 | - **kwargs (Any): Additional keyword arguments to be passed to the API call. 344 | 345 | Returns: 346 | - Response: The response object from the API call. 347 | """ 348 | return api_call( 349 | self._auth, 350 | "delete", 351 | self._url, 352 | action=self.action, 353 | headers=self.headers, 354 | resource_id=id, 355 | **kwargs, 356 | ) 357 | 358 | 359 | class Client: 360 | """A client for interacting with the Mailjet API. 361 | 362 | This class manages authentication, configuration, and API endpoint access. 363 | It initializes with API authentication details and uses dynamic attribute access 364 | to allow flexible interaction with various Mailjet API endpoints. 365 | 366 | Attributes: 367 | - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. 368 | - config (Config): An instance of the Config class, which holds API configuration settings. 369 | 370 | Methods: 371 | - __init__: Initializes a new Client instance with authentication and configuration settings. 372 | - __getattr__: Handles dynamic attribute access, allowing for accessing API endpoints as attributes. 373 | """ 374 | 375 | def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None: 376 | """Initialize a new Client instance for API interaction. 377 | 378 | This method sets up API authentication and configuration. The `auth` parameter 379 | provides a tuple with the API key and secret. Additional keyword arguments can 380 | specify configuration options like API version and URL. 381 | 382 | Parameters: 383 | - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. If None, authentication is not required. 384 | - **kwargs (Any): Additional keyword arguments, such as `version` and `api_url`, for configuring the client. 385 | 386 | Example: 387 | client = Client(auth=("api_key", "api_secret"), version="v3") 388 | """ 389 | self.auth = auth 390 | version: str | None = kwargs.get("version") 391 | api_url: str | None = kwargs.get("api_url") 392 | self.config = Config(version=version, api_url=api_url) 393 | 394 | def __getattr__(self, name: str) -> Any: 395 | """Dynamically access API endpoints as attributes. 396 | 397 | This method allows for flexible, attribute-style access to API endpoints. 398 | It constructs the appropriate endpoint URL and headers based on the attribute 399 | name, which it parses to identify the resource and optional sub-resources. 400 | 401 | Parameters: 402 | - name (str): The name of the attribute being accessed, corresponding to the Mailjet API endpoint. 403 | 404 | 405 | Returns: 406 | - Endpoint: An instance of the `Endpoint` class, initialized with the constructed URL, headers, action, and authentication details. 407 | """ 408 | name_regex: str = re.sub(r"[A-Z]", prepare_url, name) 409 | split: list[str] = name_regex.split("_") # noqa: RUF100, FURB184 410 | # identify the resource 411 | fname: str = split[0] 412 | action: str | None = None 413 | if len(split) > 1: 414 | # identify the sub resource (action) 415 | action = split[1] 416 | if action == "csvdata": 417 | action = "csvdata/text:plain" 418 | if action == "csverror": 419 | action = "csverror/text:csv" 420 | url, headers = self.config[name] 421 | return type(fname, (Endpoint,), {})( 422 | url=url, 423 | headers=headers, 424 | action=action, 425 | auth=self.auth, 426 | ) 427 | 428 | 429 | def api_call( 430 | auth: tuple[str, str] | None, 431 | method: str, 432 | url: str, 433 | headers: dict[str, str], 434 | data: str | bytes | None = None, 435 | filters: Mapping[str, str | Any] | None = None, 436 | resource_id: str | None = None, 437 | timeout: int = 60, 438 | debug: bool = False, 439 | action: str | None = None, 440 | action_id: str | None = None, 441 | **kwargs: Any, 442 | ) -> Response | Any: 443 | """Make an API call to a specified URL using the provided method, headers, and other parameters. 444 | 445 | Parameters: 446 | - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. 447 | - method (str): The HTTP method to be used for the API call (e.g., 'get', 'post', 'put', 'delete'). 448 | - url (str): The URL to which the API call will be made. 449 | - headers (dict[str, str]): A dictionary containing the headers to be included in the API call. 450 | - data (str | bytes | None): The data to be sent in the request body. 451 | - filters (Mapping[str, str | Any] | None): A dictionary containing filters to be applied in the request. 452 | - resource_id (str | None): The ID of the specific resource to be accessed. 453 | - timeout (int): The timeout for the API call in seconds. 454 | - debug (bool): A flag indicating whether debug mode is enabled. 455 | - action (str | None): The specific action to be performed on the resource. 456 | - action_id (str | None): The ID of the specific action to be performed. 457 | - **kwargs (Any): Additional keyword arguments to be passed to the API call. 458 | 459 | Returns: 460 | - Response | Any: The response object from the API call if the request is successful, or an exception if an error occurs. 461 | """ 462 | url = build_url( 463 | url, 464 | method=method, 465 | action=action, 466 | resource_id=resource_id, 467 | action_id=action_id, 468 | ) 469 | req_method = getattr(requests, method) 470 | 471 | try: 472 | filters_str: str | None = None 473 | if filters: 474 | filters_str = "&".join(f"{k}={v}" for k, v in filters.items()) 475 | response = req_method( 476 | url, 477 | data=data, 478 | params=filters_str, 479 | headers=headers, 480 | auth=auth, 481 | timeout=timeout, 482 | verify=True, 483 | stream=False, 484 | ) 485 | 486 | except requests.exceptions.Timeout: 487 | raise TimeoutError 488 | except requests.RequestException as e: 489 | raise ApiError(e) # noqa: RUF100, B904 490 | except Exception: 491 | raise 492 | else: 493 | return response 494 | 495 | 496 | def build_headers( 497 | resource: str, 498 | action: str, 499 | extra_headers: dict[str, str] | None = None, 500 | ) -> dict[str, str]: 501 | """Build headers based on resource and action. 502 | 503 | Parameters: 504 | - resource (str): The name of the resource for which headers are being built. 505 | - action (str): The specific action being performed on the resource. 506 | - extra_headers (dict[str, str] | None): Additional headers to be included in the request. Defaults to None. 507 | 508 | Returns: 509 | - dict[str, str]: A dictionary containing the headers to be included in the API request. 510 | """ 511 | headers: dict[str, str] = {"Content-type": "application/json"} 512 | 513 | if resource.lower() == "contactslist" and action.lower() == "csvdata": 514 | headers = {"Content-type": "text/plain"} 515 | elif resource.lower() == "batchjob" and action.lower() == "csverror": 516 | headers = {"Content-type": "text/csv"} 517 | 518 | if extra_headers: 519 | headers.update(extra_headers) 520 | 521 | return headers 522 | 523 | 524 | def build_url( 525 | url: str, 526 | method: str | None, 527 | action: str | None = None, 528 | resource_id: str | None = None, 529 | action_id: str | None = None, 530 | ) -> str: 531 | """Construct a URL for making an API request. 532 | 533 | This function takes the base URL, method, action, resource ID, and action ID as parameters 534 | and constructs a URL by appending the resource ID, action, and action ID to the base URL. 535 | 536 | Parameters: 537 | url (str): The base URL for the API request. 538 | method (str | None): The HTTP method for the API request (e.g., 'get', 'post', 'put', 'delete'). 539 | action (str | None): The specific action to be performed on the resource. Defaults to None. 540 | resource_id (str | None): The ID of the specific resource to be accessed. Defaults to None. 541 | action_id (str | None): The ID of the specific action to be performed. Defaults to None. 542 | 543 | Returns: 544 | str: The constructed URL for the API request. 545 | """ 546 | if resource_id: 547 | url += f"/{resource_id}" 548 | if action: 549 | url += f"/{action}" 550 | if action_id: 551 | url += f"/{action_id}" 552 | return url 553 | 554 | 555 | def logging_handler( 556 | to_file: bool = False, 557 | ) -> logging.Logger: 558 | """Create and configure a logger for logging API requests. 559 | 560 | This function creates a logger object and configures it to handle both 561 | standard output (stdout) and a file if the `to_file` parameter is set to True. 562 | The logger is set to log at the DEBUG level and uses a custom formatter to 563 | include the log level and message. 564 | 565 | Parameters: 566 | to_file (bool): A flag indicating whether to log to a file. If True, logs will be written to a file. 567 | Defaults to False. 568 | 569 | Returns: 570 | logging.Logger: A configured logger object for logging API requests. 571 | """ 572 | logger = logging.getLogger() 573 | logger.setLevel(logging.DEBUG) 574 | formatter = logging.Formatter("%(levelname)s | %(message)s") 575 | 576 | if to_file: 577 | now = datetime.now(tz=timezone.utc) 578 | date_time = now.strftime("%Y%m%d_%H%M%S") 579 | 580 | log_file = f"{date_time}.log" 581 | file_handler = logging.FileHandler(log_file) 582 | file_handler.setFormatter(formatter) 583 | logger.addHandler(file_handler) 584 | 585 | stdout_handler = logging.StreamHandler(sys.stdout) 586 | stdout_handler.setFormatter(formatter) 587 | logger.addHandler(stdout_handler) 588 | 589 | return logger 590 | 591 | 592 | def parse_response( 593 | response: Response, 594 | log: Callable, 595 | debug: bool = False, 596 | ) -> Any: 597 | """Parse the response from an API request and return the JSON data. 598 | 599 | Parameters: 600 | response (Response): The response object from the API request. 601 | log (Callable): A function or method that logs debug information. 602 | debug (bool): A flag indicating whether debug mode is enabled. Defaults to False. 603 | 604 | Returns: 605 | Any: The JSON data from the API response. 606 | """ 607 | data = response.json() 608 | 609 | if debug: 610 | lgr = log() 611 | lgr.debug("REQUEST: %s", response.request.url) 612 | lgr.debug("REQUEST_HEADERS: %s", response.request.headers) 613 | lgr.debug("REQUEST_CONTENT: %s", response.request.body) 614 | 615 | lgr.debug("RESPONSE: %s", response.content) 616 | lgr.debug("RESP_HEADERS: %s", response.headers) 617 | lgr.debug("RESP_CODE: %s", response.status_code) 618 | # Clear logger handlers to prevent making log duplications 619 | logging.getLogger().handlers.clear() 620 | 621 | return data 622 | 623 | 624 | class ApiError(Exception): 625 | """Base class for all API-related errors. 626 | 627 | This exception serves as the root for all custom API error types, 628 | allowing for more specific error handling based on the type of API 629 | failure encountered. 630 | """ 631 | 632 | 633 | class AuthorizationError(ApiError): 634 | """Error raised for authorization failures. 635 | 636 | This error is raised when the API request fails due to invalid 637 | or missing authentication credentials. 638 | """ 639 | 640 | 641 | class ActionDeniedError(ApiError): 642 | """Error raised when an action is denied by the API. 643 | 644 | This exception is triggered when an action is requested but is not 645 | permitted, likely due to insufficient permissions. 646 | """ 647 | 648 | 649 | class CriticalApiError(ApiError): 650 | """Error raised for critical API failures. 651 | 652 | This error represents severe issues with the API or infrastructure 653 | that prevent requests from completing. 654 | """ 655 | 656 | 657 | class ApiRateLimitError(ApiError): 658 | """Error raised when the API rate limit is exceeded. 659 | 660 | This exception is raised when the user has made too many requests 661 | within a given time frame, as enforced by the API's rate limit policy. 662 | """ 663 | 664 | 665 | class TimeoutError(ApiError): 666 | """Error raised when an API request times out. 667 | 668 | This error is raised if an API request does not complete within 669 | the allowed timeframe, possibly due to network issues or server load. 670 | """ 671 | 672 | 673 | class DoesNotExistError(ApiError): 674 | """Error raised when a requested resource does not exist. 675 | 676 | This exception is triggered when a specific resource is requested 677 | but cannot be found in the API, indicating a potential data mismatch 678 | or invalid identifier. 679 | """ 680 | 681 | 682 | class ValidationError(ApiError): 683 | """Error raised for invalid input data. 684 | 685 | This exception is raised when the input data for an API request 686 | does not meet validation requirements, such as incorrect data types 687 | or missing fields. 688 | """ 689 | -------------------------------------------------------------------------------- /mailjet_rest/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """The `mailjet_rest.utils` package provides utility functions for interacting with the package versionI. 2 | 3 | This package includes a module for managing the package version. 4 | 5 | Modules: 6 | - version: Manages the package versioning. 7 | """ 8 | -------------------------------------------------------------------------------- /mailjet_rest/utils/version.py: -------------------------------------------------------------------------------- 1 | """Versioning utilities for the Mailjet REST API client. 2 | 3 | This module defines the current version of the Mailjet client and provides 4 | a helper function, `get_version`, to retrieve the version as a formatted string. 5 | 6 | Attributes: 7 | VERSION (tuple[int, int, int]): A tuple representing the major, minor, and patch 8 | version of the package. 9 | 10 | Functions: 11 | get_version: Returns the version as a string in the format "major.minor.patch". 12 | """ 13 | 14 | from __future__ import annotations 15 | 16 | import re 17 | 18 | from mailjet_rest._version import __version__ as package_version 19 | 20 | 21 | def clean_version(version_str: str) -> tuple[int, ...]: 22 | """Clean package version string into 3 item tuple. 23 | 24 | Parameters: 25 | version_str (str): A string of the package version. 26 | 27 | Returns: 28 | tuple: A tuple representing the version of the package. 29 | """ 30 | if not version_str: 31 | return 0, 0, 0 32 | # Extract just the X.Y.Z part using regex 33 | match = re.match(r"^(\d+\.\d+\.\d+)", version_str) 34 | if match: 35 | version_part = match.group(1) 36 | return tuple(map(int, version_part.split("."))) 37 | 38 | return 0, 0, 0 # type: ignore[unreachable] 39 | 40 | 41 | # VERSION is a tuple of integers (1, 3, 2). 42 | VERSION: tuple[int, ...] = clean_version(package_version) 43 | 44 | 45 | def get_version(version: tuple[int, ...] | None = None) -> str: 46 | """Calculate package version based on a 3 item tuple. 47 | 48 | In addition, verify that the tuple contains 3 items. 49 | 50 | Parameters: 51 | version (tuple[int, ...], optional): A tuple representing the version of the package. 52 | If not provided, the function will use the VERSION constant. 53 | Default is None. 54 | 55 | Returns: 56 | str: The version as a string in the format "major.minor.patch". 57 | 58 | Raises: 59 | ValueError: If the provided tuple does not contain exactly 3 items. 60 | """ 61 | if version is None: 62 | version = VERSION 63 | if len(version) != 3: 64 | msg = "The tuple 'version' must contain 3 items" 65 | raise ValueError(msg) 66 | return "{}.{}.{}".format(*(x for x in version)) 67 | -------------------------------------------------------------------------------- /py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailjet/mailjet-apiv3-python/cb0e474bd422291e069e0cdbddf11b07d4e06763/py.typed -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "wheel", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | version_scheme = "no-guess-dev" # Don't try to guess next version 7 | local_scheme = "no-local-version" 8 | # Ignore uncommitted changes 9 | git_describe_command = "git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*'" 10 | write_to = "mailjet_rest/_version.py" 11 | write_to_template = '__version__ = "{version}"' 12 | #fallback_version = "X.Y.ZrcN.postN.devN" # Explicit fallback 13 | 14 | [tool.setuptools] 15 | py-modules = ["mailjet_rest._version"] 16 | 17 | [tool.setuptools.packages.find] 18 | include = ["mailjet_rest", "mailjet_rest.*", "samples", "tests", "test.py"] 19 | 20 | [tool.setuptools.package-data] 21 | mailjet_rest = ["py.typed", "*.pyi"] 22 | 23 | [project] 24 | name = "mailjet-rest" 25 | dynamic = ["version"] 26 | description = "Mailjet V3 API wrapper" 27 | authors = [ 28 | { name = "starenka", email = "starenka0@gmail.com" }, 29 | { name = "Mailjet", email = "api@mailjet.com" }, 30 | ] 31 | maintainers = [ 32 | {name = "Serhii Kupriienko", email = "kupriienko.serhii@gmail.com"} 33 | ] 34 | license = {text = "MIT"} 35 | # TODO: Enable license-files when setuptools >=77.0.0 will be available 36 | #license-files = ["LICENSE"] 37 | readme = "README.md" 38 | requires-python = ">=3.9" 39 | 40 | dependencies = ["requests>=2.32.3"] 41 | 42 | keywords = [ 43 | "Mailjet API v3 / v3.1 Python Wrapper", 44 | "wrapper", 45 | "email python-wrapper", 46 | "transactional-emails", 47 | "mailjet", 48 | "mailjet-api", 49 | ] 50 | classifiers = [ 51 | "Development Status :: 4 - Beta", 52 | "Environment :: Console", 53 | "Intended Audience :: Developers", 54 | "Natural Language :: English", 55 | "Operating System :: OS Independent", 56 | "Programming Language :: Python :: 3", 57 | "Programming Language :: Python :: 3.9", 58 | "Programming Language :: Python :: 3.10", 59 | "Programming Language :: Python :: 3.11", 60 | "Programming Language :: Python :: 3.12", 61 | "Programming Language :: Python :: 3.13", 62 | "Programming Language :: Python :: 3 :: Only", 63 | "Programming Language :: Python :: Implementation :: CPython", 64 | "Topic :: Communications :: Email", 65 | "Topic :: Utilities", 66 | ] 67 | 68 | [project.urls] 69 | "Issue Tracker" = "https://github.com/mailjet/mailjet-apiv3-python" 70 | "Repository" = "https://github.com/mailjet/mailjet-apiv3-python" 71 | "Homepage" = "https://dev.mailjet.com" 72 | "Documentation" = "https://dev.mailjet.com" 73 | 74 | [project.optional-dependencies] 75 | linting = [ 76 | # dev tools 77 | "make", 78 | "toml", 79 | "autopep8", 80 | "bandit", 81 | "black>=21.7", 82 | "autoflake", 83 | "flake8>=3.7.8", 84 | "pep8-naming", 85 | "isort", 86 | "yapf", 87 | "pycodestyle", 88 | "pydocstyle", 89 | "pyupgrade", 90 | "refurb", 91 | "pre-commit", 92 | "ruff", 93 | "mypy", 94 | "types-requests", # mypy requests stub 95 | "pandas-stubs", # mypy pandas stub 96 | "types-PyYAML", 97 | "monkeytype", # It can generate type hints based on the observed behavior of your code. 98 | "pyright", 99 | "pylint", 100 | "pyment>=0.3.3", # for generating docstrings 101 | "pytype", # a static type checker for any type hints you have put in your code 102 | "radon", 103 | "safety", # Checks installed dependencies for known vulnerabilities and licenses. 104 | "vulture", 105 | # env variables 106 | "python-dotenv>=0.19.2", 107 | ] 108 | 109 | docs = [ 110 | "docconvert", 111 | "pyment>=0.3.3", # for generating docstrings 112 | ] 113 | 114 | metrics = [ 115 | "pystra", # provides functionalities to enable structural reliability analysis 116 | "wily>=1.2.0", # a tool for reporting code complexity metrics 117 | ] 118 | 119 | profilers = ["scalene>=1.3.16", "snakeviz"] 120 | 121 | tests = [ 122 | # tests 123 | "pytest>=7.0.0", 124 | "pytest-benchmark", 125 | "pytest-cov", 126 | "coverage>=4.5.4", 127 | "codecov", 128 | ] 129 | 130 | conda_build = ["conda-build"] 131 | 132 | spelling = ["typos"] 133 | 134 | other = ["toml"] 135 | 136 | 137 | [tool.black] 138 | line-length = 88 139 | target-version = ["py39", "py310", "py311", "py312", "py313"] 140 | skip-string-normalization = false 141 | skip-magic-trailing-comma = false 142 | extend-exclude = ''' 143 | /( 144 | | docs 145 | | setup.py 146 | | venv 147 | )/ 148 | ''' 149 | 150 | [tool.autopep8] 151 | max_line_length = 88 152 | ignore = "" # or ["E501", "W6"] 153 | in-place = true 154 | recursive = true 155 | aggressive = 3 156 | 157 | [tool.ruff] 158 | # Exclude a variety of commonly ignored directories. 159 | exclude = [ 160 | ".bzr", 161 | ".direnv", 162 | ".eggs", 163 | ".git", 164 | ".git-rewrite", 165 | ".hg", 166 | ".ipynb_checkpoints", 167 | ".mypy_cache", 168 | ".nox", 169 | ".pants.d", 170 | ".pyenv", 171 | ".pytest_cache", 172 | ".pytype", 173 | ".ruff_cache", 174 | ".svn", 175 | ".tox", 176 | ".venv", 177 | ".vscode", 178 | "__pypackages__", 179 | "_build", 180 | "buck-out", 181 | "build", 182 | "dist", 183 | "node_modules", 184 | "site-packages", 185 | "venv", 186 | ] 187 | extend-exclude = ["tests", "test"] 188 | 189 | # Same as Black. 190 | line-length = 88 191 | #indent-width = 4 192 | 193 | # Assume Python 3.9. 194 | target-version = "py39" 195 | # Enumerate all fixed violations. 196 | show-fixes = true 197 | 198 | 199 | [tool.ruff.lint] 200 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or 201 | # McCabe complexity (`C901`) by default. 202 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default, ('UP') is pyupgrade. 203 | # "ERA" - Found commented-out code 204 | # see https://docs.astral.sh/ruff/rules/#rules 205 | select = ["ALL"] 206 | #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"] 207 | 208 | external = ["DOC", "PLR"] 209 | 210 | exclude = ["samples/*"] 211 | 212 | #extend-select = ["W", "N", "UP", "B", "A", "C4", "PT", "SIM", "PD", "PLE", "RUF"] 213 | # Never enforce `E501` (line length violations). 214 | ignore = [ 215 | # TODO: Fix unused function argument: `debug`, `kwargs`, and `method` in class Client 216 | "ARG001", # ARG001 Unused function argument: `debug`, `kwargs`, and `method` in class Client 217 | # TODO: Fix A001 Variable `TimeoutError` is shadowing a Python builtin 218 | "A001" , 219 | # TODO: Fix A002 Argument `id` is shadowing a Python builtin 220 | "A002", 221 | "ANN401", # ANN401 Dynamically typed expressions (typing.Any) are disallowed in `**kwargs` 222 | "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` 223 | # pycodestyle (E, W) 224 | "CPY001", # Missing copyright notice at top of file 225 | "DOC501", # DOC501 Raised exception `TimeoutError` and `ApiError` missing from docstring 226 | "E501", 227 | "FBT001", # Boolean-typed positional argument in function definition 228 | "FBT002", # Boolean default positional argument in function definition 229 | # TODO: Replace with http.HTTPStatus, see https://docs.python.org/3/library/http.html#http-status-codes 230 | "PLR2004", # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable 231 | "PLR0913", # PLR0913 Too many arguments in function definition (6 > 5) 232 | "PLR0917", # PLR0917 Too many positional arguments 233 | "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') 234 | # TODO:" PT009 Use a regular `assert` instead of unittest-style `assertTrue` 235 | "PT009", 236 | "S311", # S311 Standard pseudo-random generators are not suitable for cryptographic purposes 237 | # TODO: T201 Replace `print` with logging functions 238 | "T201", # T201 `print` found 239 | ] 240 | 241 | 242 | # Allow fix for all enabled rules (when `--fix`) is provided. 243 | fixable = ["ALL"] 244 | unfixable = ["B"] 245 | 246 | # Allow unused variables when underscore-prefixed. 247 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 248 | 249 | [tool.ruff.lint.flake8-annotations] 250 | allow-star-arg-any = false 251 | ignore-fully-untyped = false 252 | 253 | [tool.ruff.lint.flake8-quotes] 254 | docstring-quotes = "double" 255 | 256 | [tool.ruff.format] 257 | exclude = ["*.pyi"] 258 | # Like Black, use double quotes for strings. 259 | quote-style = "double" 260 | 261 | # Like Black, indent with spaces, rather than tabs. 262 | indent-style = "space" 263 | 264 | # Like Black, respect magic trailing commas. 265 | skip-magic-trailing-comma = false 266 | 267 | # Like Black, automatically detect the appropriate line ending. 268 | line-ending = "auto" 269 | 270 | # Enable auto-formatting of code examples in docstrings. Markdown, 271 | # reStructuredText code/literal blocks and doctests are all supported. 272 | # 273 | # This is currently disabled by default, but it is planned for this 274 | # to be opt-out in the future. 275 | #docstring-code-format = false 276 | 277 | # Set the line length limit used when formatting code snippets in 278 | # docstrings. 279 | # 280 | # This only has an effect when the `docstring-code-format` setting is 281 | # enabled. 282 | #docstring-code-line-length = "dynamic" 283 | 284 | # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. 285 | [tool.ruff.lint.per-file-ignores] 286 | "__init__.py" = ["E402"] 287 | #"path/to/file.py" = ["E402"] 288 | 289 | [tool.ruff.lint.isort] 290 | force-single-line = true 291 | force-sort-within-sections = false 292 | lines-after-imports = 2 293 | 294 | [tool.ruff.lint.mccabe] 295 | # Unlike Flake8, default to a complexity level of 10. 296 | max-complexity = 10 297 | 298 | [tool.ruff.lint.pycodestyle] 299 | ignore-overlong-task-comments = true 300 | 301 | [tool.ruff.lint.pydocstyle] 302 | convention = "google" 303 | 304 | [tool.pydocstyle] 305 | convention = "google" 306 | match = ".*.py" 307 | match_dir = '^samples/' 308 | 309 | [tool.flake8] 310 | exclude = ["samples/*"] 311 | # TODO: D100 - create docstrings for modules test_client.py and test_version.py 312 | ignore = ['E501', "D100"] 313 | extend-ignore = "W503" 314 | per-file-ignores = [ 315 | '__init__.py:F401', 316 | ] 317 | max-line-length = 88 318 | count = true 319 | 320 | [tool.yapf] 321 | based_on_style = "facebook" 322 | SPLIT_BEFORE_BITWISE_OPERATOR = true 323 | SPLIT_BEFORE_ARITHMETIC_OPERATOR = true 324 | SPLIT_BEFORE_LOGICAL_OPERATOR = true 325 | SPLIT_BEFORE_DOT = true 326 | 327 | [tool.yapfignore] 328 | ignore_patterns = [ 329 | ] 330 | 331 | [tool.mypy] 332 | strict = true 333 | # Adapted from this StackOverflow post: 334 | # https://stackoverflow.com/questions/55944201/python-type-hinting-how-do-i-enforce-that-project-wide 335 | python_version = "3.9" 336 | mypy_path = "type_stubs" 337 | namespace_packages = true 338 | # This flag enhances the user feedback for error messages 339 | pretty = true 340 | # 3rd party import 341 | ignore_missing_imports = true 342 | # flag to suppress Name already defined on line 343 | allow_redefinition = false 344 | # Disallow dynamic typing 345 | disallow_any_unimported = false 346 | disallow_any_expr = false 347 | disallow_any_decorated = false 348 | disallow_any_explicit = false 349 | disallow_any_generics = false 350 | disallow_subclassing_any = true 351 | # Disallow untyped definitions and calls 352 | disallow_untyped_calls = true 353 | disallow_untyped_defs = true 354 | check_untyped_defs = true 355 | disallow_incomplete_defs = true 356 | disallow_untyped_decorators = true 357 | # None and optional handling 358 | no_implicit_optional = true 359 | # Configuring warnings 360 | warn_return_any = false 361 | warn_no_return = true 362 | warn_unreachable = true 363 | warn_unused_configs = true 364 | warn_redundant_casts = true 365 | warn_unused_ignores = false 366 | # Misc 367 | follow_imports = "silent" 368 | strict_optional = false 369 | strict_equality = true 370 | #exclude = '''(?x)( 371 | # (^|/)test[^/]*\.py$ # files named "test*.py" 372 | # )''' 373 | exclude = [ 374 | "samples", 375 | ] 376 | 377 | # Configuring error messages 378 | show_error_context = false 379 | show_column_numbers = false 380 | show_error_codes = true 381 | disable_error_code = 'misc' 382 | 383 | [tool.pyright] 384 | include = ["mailjet_rest"] 385 | exclude = ["samples/*", "**/__pycache__"] 386 | reportAttributeAccessIssue = false 387 | reportMissingImports = false 388 | 389 | 390 | [tool.bandit] 391 | # usage: bandit -c pyproject.toml -r . 392 | exclude_dirs = ["tests", "test.py"] 393 | tests = ["B201", "B301"] 394 | skips = ["B101", "B601"] 395 | 396 | [tool.bandit.any_other_function_with_shell_equals_true] 397 | no_shell = [ 398 | "os.execl", 399 | "os.execle", 400 | "os.execlp", 401 | "os.execlpe", 402 | "os.execv", 403 | "os.execve", 404 | "os.execvp", 405 | "os.execvpe", 406 | "os.spawnl", 407 | "os.spawnle", 408 | "os.spawnlp", 409 | "os.spawnlpe", 410 | "os.spawnv", 411 | "os.spawnve", 412 | "os.spawnvp", 413 | "os.spawnvpe", 414 | "os.startfile" 415 | ] 416 | shell = [ 417 | "os.system", 418 | "os.popen", 419 | "os.popen2", 420 | "os.popen3", 421 | "os.popen4", 422 | "popen2.popen2", 423 | "popen2.popen3", 424 | "popen2.popen4", 425 | "popen2.Popen3", 426 | "popen2.Popen4", 427 | "commands.getoutput", 428 | "commands.getstatusoutput" 429 | ] 430 | subprocess = [ 431 | "subprocess.Popen", 432 | "subprocess.call", 433 | "subprocess.check_call", 434 | "subprocess.check_output" 435 | ] 436 | 437 | [tool.coverage.run] 438 | source_pkgs = ["mailjet_rest"] 439 | branch = true 440 | parallel = true 441 | omit = [ 442 | "samples/*", 443 | ] 444 | 445 | [tool.coverage.paths] 446 | tests = ["tests"] 447 | 448 | [tool.coverage.report] 449 | exclude_lines = [ 450 | "no cov", 451 | "if __name__ == .__main__.:", 452 | "if TYPE_CHECKING:", 453 | ] 454 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailjet/mailjet-apiv3-python/cb0e474bd422291e069e0cdbddf11b07d4e06763/samples/__init__.py -------------------------------------------------------------------------------- /samples/campaign_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | 17 | def create_a_campaign_draft(): 18 | """POST https://api.mailjet.com/v3/REST/campaigndraft""" 19 | data = { 20 | "Locale": "en_US", 21 | "Sender": "MisterMailjet", 22 | "SenderEmail": "Mister@mailjet.com", 23 | "Subject": "Greetings from Mailjet", 24 | "ContactsListID": "$ID_CONTACTSLIST", 25 | "Title": "Friday newsletter", 26 | } 27 | return mailjet30.campaigndraft.create(data=data) 28 | 29 | 30 | def by_adding_custom_content(): 31 | """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/detailcontent""" 32 | _id = "$draft_ID" 33 | data = { 34 | "Headers": "object", 35 | "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", 36 | "MJMLContent": "", 37 | "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!", 38 | } 39 | return mailjet30.campaigndraft_detailcontent.create(id=_id, data=data) 40 | 41 | 42 | def test_your_campaign(): 43 | """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/test""" 44 | _id = "$draft_ID" 45 | data = {"Recipients": [{"Email": "passenger@mailjet.com", "Name": "Passenger 1"}]} 46 | return mailjet30.campaigndraft_test.create(id=_id, data=data) 47 | 48 | 49 | def schedule_the_sending(): 50 | """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/schedule""" 51 | _id = "$draft_ID" 52 | data = {"Date": "2018-01-01T00:00:00"} 53 | return mailjet30.campaigndraft_schedule.create(id=_id, data=data) 54 | 55 | 56 | def send_the_campaign_right_away(): 57 | """POST https://api.mailjet.com/v3/REST/campaigndraft/$draft_ID/send""" 58 | _id = "$draft_ID" 59 | return mailjet30.campaigndraft_send.create(id=_id) 60 | 61 | 62 | def api_call_requirements(): 63 | """POST https://api.mailjet.com/v3.1/send""" 64 | # fmt: off 65 | data = { 66 | "Messages": [ 67 | { 68 | "From": { 69 | "Email": "pilot@mailjet.com", 70 | "Name": "Mailjet Pilot"}, 71 | "To": [ 72 | { 73 | "Email": "passenger1@mailjet.com", 74 | "Name": "passenger 1"}], 75 | "Subject": "Your email flight plan!", 76 | "TextPart": "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!", 77 | "HTMLPart": "

Dear passenger 1, welcome to Mailjet!


May the delivery force be with " 79 | "you!", 80 | "CustomCampaign": "SendAPI_campaign", 81 | "DeduplicateCampaign": True}]} 82 | # fmt: on 83 | return mailjet31.send.create(data=data) 84 | 85 | 86 | if __name__ == "__main__": 87 | result = create_a_campaign_draft() 88 | print(result.status_code) 89 | try: 90 | print(json.dumps(result.json(), indent=4)) 91 | except json.decoder.JSONDecodeError: 92 | print(result.text) 93 | -------------------------------------------------------------------------------- /samples/contacts_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | from mailjet_rest import Client 6 | 7 | 8 | mailjet30 = Client( 9 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 10 | ) 11 | 12 | mailjet31 = Client( 13 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 14 | version="v3.1", 15 | ) 16 | 17 | 18 | def create_a_contact(): 19 | """POST https://api.mailjet.com/v3/REST/contact""" 20 | data = { 21 | "IsExcludedFromCampaigns": "true", 22 | "Name": "New Contact", 23 | "Email": "passenger@mailjet.com", 24 | } 25 | return mailjet30.contact.create(data=data) 26 | 27 | 28 | def create_contact_metadata(): 29 | """POST https://api.mailjet.com/v3/REST/contactmetadata""" 30 | data = {"Datatype": "str", "Name": "first_name", "NameSpace": "static"} 31 | return mailjet30.contactmetadata.create(data=data) 32 | 33 | 34 | def edit_contact_data(): 35 | """PUT https://api.mailjet.com/v3/REST/contactdata/$contact_ID""" 36 | _id = "*********" # Put real ID to make it work. 37 | data = {"Data": [{"Name": "first_name", "Value": "John"}]} 38 | return mailjet30.contactdata.update(id=_id, data=data) 39 | 40 | 41 | def manage_contact_properties(): 42 | """POST https://api.mailjet.com/v3/REST/contactmetadata""" 43 | _id = "$contact_ID" 44 | data = {"Data": [{"Name": "first_name", "Value": "John"}]} 45 | return mailjet30.contactdata.update(id=_id, data=data) 46 | 47 | 48 | def create_a_contact_list(): 49 | """POST https://api.mailjet.com/v3/REST/contactslist""" 50 | data = {"Name": "my_contactslist"} 51 | return mailjet30.contactslist.create(data=data) 52 | 53 | 54 | def add_a_contact_to_a_contact_list(): 55 | """POST https://api.mailjet.com/v3/REST/listrecipient""" 56 | data = { 57 | "IsUnsubscribed": "true", 58 | "ContactID": "987654321", 59 | "ContactAlt": "passenger@mailjet.com", 60 | "ListID": "123456", 61 | "ListAlt": "abcdef123", 62 | } 63 | return mailjet30.listrecipient.create(data=data) 64 | 65 | 66 | def manage_the_subscription_status_of_an_existing_contact(): 67 | """POST https://api.mailjet.com/v3/REST/contact/$contact_ID/managecontactslists""" 68 | _id = "$contact_ID" 69 | data = { 70 | "ContactsLists": [ 71 | {"Action": "addforce", "ListID": "987654321"}, 72 | {"Action": "addnoforce", "ListID": "987654321"}, 73 | {"Action": "remove", "ListID": "987654321"}, 74 | {"Action": "unsub", "ListID": "987654321"}, 75 | ], 76 | } 77 | return mailjet30.contact_managecontactslists.create(id=_id, data=data) 78 | 79 | 80 | def manage_multiple_contacts_in_a_list(): 81 | """POST https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" 82 | _id = "$list_ID" 83 | data = { 84 | "Action": "addnoforce", 85 | "Contacts": [ 86 | { 87 | "Email": "passenger@mailjet.com", 88 | "IsExcludedFromCampaigns": "false", 89 | "Name": "Passenger 1", 90 | "Properties": "object", 91 | }, 92 | ], 93 | } 94 | return mailjet30.contactslist_managemanycontacts.create(id=_id, data=data) 95 | 96 | 97 | def monitor_the_upload_job(): 98 | """GET https://api.mailjet.com/v3/REST/contactslist/$list_ID/managemanycontacts""" 99 | _id = "$list_ID" 100 | return mailjet30.contactslist_managemanycontacts.get(id=_id) 101 | 102 | 103 | def manage_multiple_contacts_across_multiple_lists(): 104 | """POST https://api.mailjet.com/v3/REST/contact/managemanycontacts""" 105 | data = { 106 | "Contacts": [ 107 | { 108 | "Email": "passenger@mailjet.com", 109 | "IsExcludedFromCampaigns": "false", 110 | "Name": "Passenger 1", 111 | "Properties": "object", 112 | }, 113 | ], 114 | "ContactsLists": [ 115 | {"Action": "addforce", "ListID": "987654321"}, 116 | {"Action": "addnoforce", "ListID": "987654321"}, 117 | {"Action": "remove", "ListID": "987654321"}, 118 | {"Action": "unsub", "ListID": "987654321"}, 119 | ], 120 | } 121 | return mailjet30.contact_managemanycontacts.create(data=data) 122 | 123 | 124 | def upload_the_csv(): 125 | """POST https://api.mailjet.com/v3/DATA/contactslist 126 | /$ID_CONTACTLIST/CSVData/text:plain""" 127 | return mailjet30.contactslist_csvdata.create( 128 | id="$ID_CONTACTLIST", 129 | data=Path("./data.csv").read_text(encoding="utf-8"), 130 | ) 131 | 132 | 133 | def import_csv_content_to_a_list(): 134 | """POST https://api.mailjet.com/v3/REST/csvimport""" 135 | data = { 136 | "ErrThreshold": "1", 137 | "ImportOptions": "", 138 | "Method": "addnoforce", 139 | "ContactsListID": "123456", 140 | "DataID": "98765432123456789", 141 | } 142 | return mailjet30.csvimport.create(data=data) 143 | 144 | 145 | def using_csv_with_atetime_contact_data(): 146 | """POST https://api.mailjet.com/v3/REST/csvimport""" 147 | # fmt: off 148 | data = { 149 | "ContactsListID": "$ID_CONTACTLIST", 150 | "DataID": "$ID_DATA", 151 | "Method": "addnoforce", 152 | "ImportOptions": "{\"DateTimeFormat\": \"yyyy/mm/dd\"," 153 | "\"TimezoneOffset\": 2,\"FieldNames\": " 154 | "[\"email\", \"birthday\"]} ", 155 | } 156 | # fmt: on 157 | return mailjet30.csvimport.create(data=data) 158 | 159 | 160 | def monitor_the_import_progress(): 161 | """GET https://api.mailjet.com/v3/REST/csvimport/$importjob_ID""" 162 | _id = "$importjob_ID" 163 | return mailjet30.csvimport.get(id=_id) 164 | 165 | 166 | def error_handling(): 167 | """https://api.mailjet.com/v3/DATA/BatchJob/$job_id/CSVError/text:csv""" 168 | """Not available in Python, please refer to Curl""" 169 | 170 | 171 | def single_contact_exclusion(): 172 | """PUT https://api.mailjet.com/v3/REST/contact/$ID_OR_EMAIL""" 173 | _id = "$ID_OR_EMAIL" 174 | data = {"IsExcludedFromCampaigns": "true"} 175 | return mailjet30.contact.update(id=_id, data=data) 176 | 177 | 178 | def using_contact_managemanycontacts(): 179 | """POST https://api.mailjet.com/v3/REST/contact/managemanycontacts""" 180 | data = { 181 | "Contacts": [ 182 | { 183 | "Email": "jimsmith@example.com", 184 | "Name": "Jim", 185 | "IsExcludedFromCampaigns": "true", 186 | "Properties": {"Property1": "value", "Property2": "value2"}, 187 | }, 188 | { 189 | "Email": "janetdoe@example.com", 190 | "Name": "Janet", 191 | "IsExcludedFromCampaigns": "true", 192 | "Properties": {"Property1": "value", "Property2": "value2"}, 193 | }, 194 | ], 195 | } 196 | return mailjet30.contact_managemanycontacts.create(data=data) 197 | 198 | 199 | def using_csvimport(): 200 | """POST https://api.mailjet.com/v3/REST/csvimport""" 201 | data = {"DataID": "$ID_DATA", "Method": "excludemarketing"} 202 | return mailjet30.csvimport.create(data=data) 203 | 204 | 205 | def retrieve_a_contact(): 206 | """GET https://api.mailjet.com/v3/REST/contact/$CONTACT_EMAIL""" 207 | _id = "$CONTACT_EMAIL" 208 | return mailjet30.contact.get(id=_id) 209 | 210 | 211 | def delete_the_contact(): 212 | """DELETE https://api.mailjet.com/v4/contacts/{contact_ID}""" 213 | 214 | 215 | if __name__ == "__main__": 216 | result = edit_contact_data() 217 | print(result.status_code) 218 | try: 219 | print(json.dumps(result.json(), indent=4)) 220 | except json.decoder.JSONDecodeError: 221 | print(result.text) 222 | -------------------------------------------------------------------------------- /samples/email_template_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | 17 | def create_a_template(): 18 | """POST https://api.mailjet.com/v3/REST/template""" 19 | data = { 20 | "Author": "John Doe", 21 | "Categories": "array", 22 | "Copyright": "Mailjet", 23 | "Description": "Used to send out promo codes.", 24 | "EditMode": "1", 25 | "IsStarred": "false", 26 | "IsTextPartGenerationEnabled": "true", 27 | "Locale": "en_US", 28 | "Name": "Promo Codes", 29 | "OwnerType": "user", 30 | "Presets": "string", 31 | "Purposes": "array", 32 | } 33 | return mailjet30.template.create(data=data) 34 | 35 | 36 | def create_a_template_detailcontent(): 37 | """POST https://api.mailjet.com/v3/REST/template/$template_ID/detailcontent""" 38 | _id = "$template_ID" 39 | data = { 40 | "Headers": "", 41 | "Html-part": "

Dear passenger, welcome to Mailjet!


May the delivery force be with you!", 42 | "MJMLContent": "", 43 | "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!", 44 | } 45 | return mailjet30.template_detailcontent.create(id=_id, data=data) 46 | 47 | 48 | def use_templates_with_send_api(): 49 | """POST https://api.mailjet.com/v3.1/send""" 50 | data = { 51 | "Messages": [ 52 | { 53 | "From": {"Email": "pilot@mailjet.com", "Name": "Mailjet Pilot"}, 54 | "To": [{"Email": "passenger1@mailjet.com", "Name": "passenger 1"}], 55 | "TemplateID": 1, 56 | "TemplateLanguage": True, 57 | "Subject": "Your email flight plan!", 58 | }, 59 | ], 60 | } 61 | return mailjet31.send.create(data=data) 62 | 63 | 64 | if __name__ == "__main__": 65 | result = create_a_template() 66 | print(result.status_code) 67 | try: 68 | print(json.dumps(result.json(), indent=4)) 69 | except json.decoder.JSONDecodeError: 70 | print(result.text) 71 | -------------------------------------------------------------------------------- /samples/getting_started_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | 17 | def send_messages(): 18 | """POST https://api.mailjet.com/v3.1/send""" 19 | # fmt: off; pylint; noqa 20 | data = { 21 | "Messages": [ 22 | { 23 | "From": {"Email": "pilot@mailjet.com", "Name": "Mailjet Pilot"}, 24 | "To": [{"Email": "passenger1@mailjet.com", "Name": "passenger 1"}], 25 | "Subject": "Your email flight plan!", 26 | "TextPart": "Dear passenger 1, welcome to Mailjet! May the " 27 | "delivery force be with you!", 28 | "HTMLPart": '

Dear passenger 1, welcome to Mailjet!
May the ' 30 | "delivery force be with you!", 31 | }, 32 | ], 33 | "SandboxMode": True, # Remove to send real message. 34 | } 35 | # fmt: on; pylint; noqa 36 | return mailjet31.send.create(data=data) 37 | 38 | 39 | def retrieve_messages_from_campaign(): 40 | """GET https://api.mailjet.com/v3/REST/message?CampaignID=$CAMPAIGNID""" 41 | filters = { 42 | "CampaignID": "*****", # Put real ID to make it work. 43 | } 44 | return mailjet30.message.get(filters=filters) 45 | 46 | 47 | def retrieve_message(): 48 | """GET https://api.mailjet.com/v3/REST/message/$MESSAGE_ID""" 49 | _id = "*****************" # Put real ID to make it work. 50 | return mailjet30.message.get(_id) 51 | 52 | 53 | def view_message_history(): 54 | """GET https://api.mailjet.com/v3/REST/messagehistory/$MESSAGE_ID""" 55 | _id = "*****************" # Put real ID to make it work. 56 | return mailjet30.messagehistory.get(_id) 57 | 58 | 59 | def retrieve_statistic(): 60 | """GET https://api.mailjet.com/v3/REST/statcounters?CounterSource=APIKey 61 | \\&CounterTiming=Message\\&CounterResolution=Lifetime 62 | """ 63 | filters = { 64 | "CounterSource": "APIKey", 65 | "CounterTiming": "Message", 66 | "CounterResolution": "Lifetime", 67 | } 68 | return mailjet30.statcounters.get(filters=filters) 69 | 70 | 71 | if __name__ == "__main__": 72 | result = retrieve_statistic() 73 | print(result.status_code) 74 | try: 75 | print(json.dumps(result.json(), indent=4)) 76 | except json.decoder.JSONDecodeError: 77 | print(result.text) 78 | -------------------------------------------------------------------------------- /samples/new_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | if __name__ == "__main__": 17 | from samples.contacts_sample import edit_contact_data 18 | 19 | result = edit_contact_data() 20 | print(result.status_code) 21 | try: 22 | print(json.dumps(result.json(), indent=4)) 23 | except json.decoder.JSONDecodeError: 24 | print(result.text) 25 | -------------------------------------------------------------------------------- /samples/parse_api_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | 17 | def basic_setup(): 18 | """POST https://api.mailjet.com/v3/REST/parseroute""" 19 | data = {"Url": "https://www.mydomain.com/mj_parse.php"} 20 | return mailjet30.parseroute.create(data=data) 21 | 22 | 23 | if __name__ == "__main__": 24 | result = basic_setup() 25 | print(result.status_code) 26 | try: 27 | print(json.dumps(result.json(), indent=4)) 28 | except json.decoder.JSONDecodeError: 29 | print(result.text) 30 | -------------------------------------------------------------------------------- /samples/segments_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | 17 | def create_your_segment(): 18 | """POST https://api.mailjet.com/v3/REST/contactfilter""" 19 | data = { 20 | "Description": "Will send only to contacts under 35 years of age.", 21 | "Expression": "(age<35)", 22 | "Name": "Customers under 35", 23 | } 24 | return mailjet30.contactfilter.create(data=data) 25 | 26 | 27 | def create_a_campaign_with_a_segmentation_filter(): 28 | """POST https://api.mailjet.com/v3/REST/newsletter""" 29 | data = { 30 | "Title": "Mailjet greets every contact over 40", 31 | "Locale": "en_US", 32 | "Sender": "MisterMailjet", 33 | "SenderEmail": "Mister@mailjet.com", 34 | "Subject": "Greetings from Mailjet", 35 | "ContactsListID": "$ID_CONTACTLIST", 36 | "SegmentationID": "$ID_CONTACT_FILTER", 37 | } 38 | return mailjet30.newsletter.create(data=data) 39 | 40 | 41 | if __name__ == "__main__": 42 | result = create_your_segment() 43 | print(result.status_code) 44 | try: 45 | print(json.dumps(result.json(), indent=4)) 46 | except json.decoder.JSONDecodeError: 47 | print(result.text) 48 | -------------------------------------------------------------------------------- /samples/sender_and_domain_samples.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | 17 | def validate_an_entire_domain(): 18 | """GET https: // api.mailjet.com / v3 / REST / dns""" 19 | _id = "$dns_ID" 20 | return mailjet30.dns.get(id=_id) 21 | 22 | 23 | def do_an_immediate_check_via_a_post(): 24 | """POST https://api.mailjet.com/v3/REST/dns/$dns_ID/check""" 25 | _id = "$dns_ID" 26 | return mailjet30.dns_check.create(id=_id) 27 | 28 | 29 | def host_a_text_file(): 30 | """GET https://api.mailjet.com/v3/REST/sender""" 31 | _id = "$sender_ID" 32 | return mailjet30.sender.get(id=_id) 33 | 34 | 35 | def validation_by_doing_a_post(): 36 | """POST https://api.mailjet.com/v3/REST/sender/$sender_ID/validate""" 37 | _id = "$sender_ID" 38 | return mailjet30.sender_validate.create(id=_id) 39 | 40 | 41 | def spf_and_dkim_validation(): 42 | """ET https://api.mailjet.com/v3/REST/dns""" 43 | _id = "$dns_ID" 44 | return mailjet30.dns.get(id=_id) 45 | 46 | 47 | def use_a_sender_on_all_api_keys(): 48 | """POST https://api.mailjet.com/v3/REST/metasender""" 49 | data = { 50 | "Description": "Metasender 1 - used for Promo emails", 51 | "Email": "pilot@mailjet.com", 52 | } 53 | return mailjet30.metasender.create(data=data) 54 | 55 | 56 | if __name__ == "__main__": 57 | result = validate_an_entire_domain() 58 | print(result.status_code) 59 | try: 60 | print(json.dumps(result.json(), indent=4)) 61 | except json.decoder.JSONDecodeError: 62 | print(result.text) 63 | -------------------------------------------------------------------------------- /samples/statistic_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from mailjet_rest import Client 5 | 6 | 7 | mailjet30 = Client( 8 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 9 | ) 10 | 11 | mailjet31 = Client( 12 | auth=(os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"]), 13 | version="v3.1", 14 | ) 15 | 16 | 17 | def event_based_vs_message_based_stats_timing(): 18 | """GET https://api.mailjet.com/v3/REST/statcounters""" 19 | filters = { 20 | "SourceId": "$Campaign_ID", 21 | "CounterSource": "Campaign", 22 | "CounterTiming": "Message", 23 | "CounterResolution": "Lifetime", 24 | } 25 | return mailjet30.statcounters.get(filters=filters) 26 | 27 | 28 | def view_the_spread_of_events_over_time(): 29 | """GET https://api.mailjet.com/v3/REST/statcounters""" 30 | filters = { 31 | "SourceId": "$Campaign_ID", 32 | "CounterSource": "Campaign", 33 | "CounterTiming": "Event", 34 | "CounterResolution": "Day", 35 | "FromTS": "123", 36 | "ToTS": "456", 37 | } 38 | return mailjet30.statcounters.get(filters=filters) 39 | 40 | 41 | def statistics_for_specific_recipient(): 42 | """GET https://api.mailjet.com/v3/REST/contactstatistics""" 43 | return mailjet30.contactstatistics.get() 44 | 45 | 46 | def stats_for_clicked_links(): 47 | """GET https://api.mailjet.com/v3/REST/statistics/link-click""" 48 | filters = {"CampaignId": "$Campaign_ID"} 49 | return mailjet30.statistics_linkClick.get(filters=filters) 50 | 51 | 52 | def mailbox_provider_statistics(): 53 | """GET https://api.mailjet.com/v3/REST/statistics/recipient-esp""" 54 | filters = {"CampaignId": "$Campaign_ID"} 55 | return mailjet30.statistics_recipientEsp.get(filters=filters) 56 | 57 | 58 | def geographical_statistics(): 59 | """GET https://api.mailjet.com/v3/REST/geostatistics""" 60 | return mailjet30.geostatistics.get() 61 | 62 | 63 | if __name__ == "__main__": 64 | result = geographical_statistics() 65 | print(result.status_code) 66 | try: 67 | print(json.dumps(result.json(), indent=4)) 68 | except json.decoder.JSONDecodeError: 69 | print(result.text) 70 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | """A suite of tests for Mailjet API client functionality.""" 2 | 3 | import os 4 | import random 5 | import string 6 | import unittest 7 | from typing import Any 8 | 9 | from mailjet_rest import Client 10 | 11 | 12 | class TestSuite(unittest.TestCase): 13 | """A suite of tests for Mailjet API client functionality. 14 | 15 | This class provides setup and teardown functionality for tests involving the 16 | Mailjet API client, with authentication and client initialization handled 17 | in `setUp`. Each test in this suite operates with the configured Mailjet client 18 | instance to simulate API interactions. 19 | """ 20 | 21 | def setUp(self) -> None: 22 | """Set up the test environment by initializing authentication credentials and the Mailjet client. 23 | 24 | This method is called before each test to ensure a consistent testing 25 | environment. It retrieves the API keys from environment variables and 26 | uses them to create an instance of the Mailjet `Client` for authenticated 27 | API interactions. 28 | 29 | Attributes: 30 | - 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. 31 | - self.client (Client): An instance of the Mailjet Client class, initialized with the provided authentication credentials. 32 | """ 33 | self.auth: tuple[str, str] = ( 34 | os.environ["MJ_APIKEY_PUBLIC"], 35 | os.environ["MJ_APIKEY_PRIVATE"], 36 | ) 37 | self.client: Client = Client(auth=self.auth) 38 | 39 | def test_get_no_param(self) -> None: 40 | """This test function sends a GET request to the Mailjet API endpoint for contacts without any parameters. 41 | 42 | It verifies that the response contains 'Data' and 'Count' fields. 43 | 44 | Parameters: 45 | None 46 | """ 47 | result: Any = self.client.contact.get().json() 48 | self.assertTrue("Data" in result and "Count" in result) 49 | 50 | def test_get_valid_params(self) -> None: 51 | """This test function sends a GET request to the Mailjet API endpoint for contacts with a valid parameter 'limit'. 52 | 53 | It verifies that the response contains a count of contacts that is within the range of 0 to 2. 54 | 55 | Parameters: 56 | None 57 | """ 58 | result: Any = self.client.contact.get(filters={"limit": 2}).json() 59 | self.assertTrue(result["Count"] >= 0 or result["Count"] <= 2) 60 | 61 | def test_get_invalid_parameters(self) -> None: 62 | """This test function sends a GET request to the Mailjet API endpoint for contacts with an invalid parameter. 63 | 64 | It verifies that the response contains 'Count' field, demonstrating that invalid parameters are ignored. 65 | 66 | Parameters: 67 | None 68 | """ 69 | # invalid parameters are ignored 70 | result: Any = self.client.contact.get(filters={"invalid": "false"}).json() 71 | self.assertTrue("Count" in result) 72 | 73 | def test_get_with_data(self) -> None: 74 | """This test function sends a GET request to the Mailjet API endpoint for contacts with 'data' parameter. 75 | 76 | It verifies that the request is successful (status code 200) and does not use the 'data' parameter. 77 | 78 | Parameters: 79 | None 80 | """ 81 | # it shouldn't use data 82 | result = self.client.contact.get(data={"Email": "api@mailjet.com"}) 83 | self.assertTrue(result.status_code == 200) 84 | 85 | def test_get_with_action(self) -> None: 86 | """This function tests the functionality of adding a contact to a contact list using the Mailjet API client. 87 | 88 | It first retrieves a contact and a contact list from the API, then adds the contact to the list. 89 | Finally, it verifies that the contact has been successfully added to the list. 90 | 91 | Parameters: 92 | None 93 | 94 | Attributes: 95 | - get_contact (Any): The result of the initial contact retrieval, containing a single contact. 96 | - contact_id (str): The ID of the retrieved contact. 97 | - post_contact (Response): The response from creating a new contact if no contact was found. 98 | - get_contact_list (Any): The result of the contact list retrieval, containing a single contact list. 99 | - list_id (str): The ID of the retrieved contact list. 100 | - post_contact_list (Response): The response from creating a new contact list if no contact list was found. 101 | - data (dict[str, list[dict[str, str]]]): The data for managing contact lists, containing the list ID and action to add the contact. 102 | - result_add_list (Response): The response from adding the contact to the contact list. 103 | - result (Any): The result of retrieving the contact's contact lists, containing the count of contact lists. 104 | """ 105 | get_contact: Any = self.client.contact.get(filters={"limit": 1}).json() 106 | if get_contact["Count"] != 0: 107 | contact_id: str = get_contact["Data"][0]["ID"] 108 | else: 109 | contact_random_email: str = ( 110 | "".join( 111 | random.choice(string.ascii_uppercase + string.digits) 112 | for _ in range(10) 113 | ) 114 | + "@mailjet.com" 115 | ) 116 | post_contact = self.client.contact.create( 117 | data={"Email": contact_random_email}, 118 | ) 119 | self.assertTrue(post_contact.status_code == 201) 120 | contact_id = post_contact.json()["Data"][0]["ID"] 121 | 122 | get_contact_list: Any = self.client.contactslist.get( 123 | filters={"limit": 1}, 124 | ).json() 125 | if get_contact_list["Count"] != 0: 126 | list_id: str = get_contact_list["Data"][0]["ID"] 127 | else: 128 | contact_list_random_name: str = ( 129 | "".join( 130 | random.choice(string.ascii_uppercase + string.digits) 131 | for _ in range(10) 132 | ) 133 | + "@mailjet.com" 134 | ) 135 | post_contact_list = self.client.contactslist.create( 136 | data={"Name": contact_list_random_name}, 137 | ) 138 | self.assertTrue(post_contact_list.status_code == 201) 139 | list_id = post_contact_list.json()["Data"][0]["ID"] 140 | 141 | data: dict[str, list[dict[str, str]]] = { 142 | "ContactsLists": [{"ListID": list_id, "Action": "addnoforce"}], 143 | } 144 | result_add_list = self.client.contact_managecontactslists.create( 145 | id=contact_id, 146 | data=data, 147 | ) 148 | self.assertTrue(result_add_list.status_code == 201) 149 | 150 | result = self.client.contact_getcontactslists.get(contact_id).json() 151 | self.assertTrue("Count" in result) 152 | 153 | def test_get_with_id_filter(self) -> None: 154 | """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. 155 | 156 | It verifies that the response contains a contact with the same email address as the one used in the filter. 157 | 158 | Parameters: 159 | None 160 | 161 | Attributes: 162 | - result_contact (Any): The result of the initial contact retrieval, containing a single contact. 163 | - result_contact_with_id (Any): The result of the contact retrieval using the email address from the initial contact as a filter. 164 | """ 165 | result_contact: Any = self.client.contact.get(filters={"limit": 1}).json() 166 | result_contact_with_id: Any = self.client.contact.get( 167 | filter={"Email": result_contact["Data"][0]["Email"]}, 168 | ).json() 169 | self.assertTrue( 170 | result_contact_with_id["Data"][0]["Email"] 171 | == result_contact["Data"][0]["Email"], 172 | ) 173 | 174 | def test_post_with_no_param(self) -> None: 175 | """This function tests the behavior of the Mailjet API client when attempting to create a sender with no parameters. 176 | 177 | The function sends a POST request to the Mailjet API endpoint for creating a sender with an empty 178 | data dictionary. It then verifies that the response contains a 'StatusCode' field with a value of 400, 179 | indicating a bad request. This test ensures that the client handles missing required parameters 180 | appropriately. 181 | 182 | Parameters: 183 | None 184 | """ 185 | result: Any = self.client.sender.create(data={}).json() 186 | self.assertTrue("StatusCode" in result and result["StatusCode"] == 400) 187 | 188 | def test_client_custom_version(self) -> None: 189 | """This test function verifies the functionality of setting a custom version for the Mailjet API client. 190 | 191 | The function initializes a new instance of the Mailjet Client with custom version "v3.1". 192 | It then asserts that the client's configuration version is correctly set to "v3.1". 193 | Additionally, it verifies that the send endpoint URL in the client's configuration is updated to the correct version. 194 | 195 | Parameters: 196 | None 197 | """ 198 | self.client = Client(auth=self.auth, version="v3.1") 199 | self.assertEqual(self.client.config.version, "v3.1") 200 | self.assertEqual( 201 | self.client.config["send"][0], 202 | "https://api.mailjet.com/v3.1/send", 203 | ) 204 | 205 | def test_user_agent(self) -> None: 206 | """This function tests the user agent configuration of the Mailjet API client. 207 | 208 | The function initializes a new instance of the Mailjet Client with a custom version "v3.1". 209 | It then asserts that the client's user agent is correctly set to "mailjet-apiv3-python/v1.3.5". 210 | This test ensures that the client's user agent is properly configured and includes the correct version information. 211 | 212 | Parameters: 213 | None 214 | """ 215 | self.client = Client(auth=self.auth, version="v3.1") 216 | self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.4.0") 217 | 218 | 219 | if __name__ == "__main__": 220 | unittest.main() 221 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """The `tests` package contains unit and integration tests for the Mailjet REST API client. 2 | 3 | This package ensures that all core functionalities of the Mailjet client, including 4 | authentication, API requests, error handling, and response parsing, work as expected. 5 | Each module within this package tests a specific aspect or component of the client. 6 | """ 7 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from functools import partial 3 | 4 | import glob 5 | import json 6 | import os 7 | import re 8 | from datetime import datetime 9 | from pathlib import Path 10 | from typing import Any 11 | 12 | import pytest 13 | from _pytest.logging import LogCaptureFixture 14 | 15 | from mailjet_rest.utils.version import get_version 16 | from mailjet_rest import Client 17 | from mailjet_rest.client import prepare_url, parse_response, logging_handler, Config 18 | 19 | 20 | def debug_entries() -> tuple[str, str, str, str, str, str, str]: 21 | """Provide a simple tuples with debug entries for testing purposes. 22 | 23 | Parameters: 24 | None 25 | 26 | Returns: 27 | tuple: A tuple containing seven debug entries 28 | """ 29 | entries = ( 30 | "DEBUG", 31 | "REQUEST:", 32 | "REQUEST_HEADERS:", 33 | "REQUEST_CONTENT:", 34 | "RESPONSE:", 35 | "RESP_HEADERS:", 36 | "RESP_CODE:", 37 | ) 38 | return entries 39 | 40 | 41 | def validate_datetime_format(date_text: str, datetime_format: str) -> None: 42 | """Validate the format of a given date string against a specified datetime format. 43 | 44 | Parameters: 45 | date_text (str): The date string to be validated. 46 | datetime_format (str): The datetime format to which the date string should be validated. 47 | 48 | Raises: 49 | ValueError: If the date string does not match the specified datetime format. 50 | """ 51 | try: 52 | datetime.strptime(date_text, datetime_format) 53 | except ValueError: 54 | raise ValueError("Incorrect data format, should be %Y%m%d_%H%M%S") 55 | 56 | 57 | @pytest.fixture 58 | def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: 59 | """Provide a simple data structure and its encoding for testing purposes. 60 | 61 | Parameters: 62 | None 63 | 64 | Returns: 65 | tuple: A tuple containing two elements: 66 | - A dictionary representing structured data with a list of dictionaries. 67 | - A string representing the encoding of the data. 68 | """ 69 | data: dict[str, list[dict[str, str]]] = { 70 | "Data": [{"Name": "first_name", "Value": "John"}] 71 | } 72 | data_encoding: str = "utf-8" 73 | return data, data_encoding 74 | 75 | 76 | @pytest.fixture 77 | def client_mj30() -> Client: 78 | """Create and return a Mailjet API client instance for version 3.0. 79 | 80 | Parameters: 81 | None 82 | 83 | Returns: 84 | 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. 85 | """ 86 | auth: tuple[str, str] = ( 87 | os.environ["MJ_APIKEY_PUBLIC"], 88 | os.environ["MJ_APIKEY_PRIVATE"], 89 | ) 90 | return Client(auth=auth) 91 | 92 | 93 | @pytest.fixture 94 | def client_mj30_invalid_auth() -> Client: 95 | """Create and return a Mailjet API client instance for version 3.0, but with invalid authentication credentials. 96 | 97 | Parameters: 98 | None 99 | 100 | Returns: 101 | Client: An instance of the Mailjet API client configured for version 3.0. 102 | The client is authenticated using invalid public and private API keys. 103 | If the client is used to make requests, it will raise a ValueError. 104 | """ 105 | auth: tuple[str, str] = ( 106 | "invalid_public_key", 107 | "invalid_private_key", 108 | ) 109 | return Client(auth=auth) 110 | 111 | 112 | @pytest.fixture 113 | def client_mj31() -> Client: 114 | """Create and return a Mailjet API client instance for version 3.1. 115 | 116 | Parameters: 117 | None 118 | 119 | Returns: 120 | Client: An instance of the Mailjet API client configured for version 3.1. 121 | The client is authenticated using the public and private API keys provided as environment variables. 122 | 123 | Note: 124 | - The function retrieves the public and private API keys from the environment variables 'MJ_APIKEY_PUBLIC' and 'MJ_APIKEY_PRIVATE' respectively. 125 | - The client is initialized with the provided authentication credentials and the version set to 'v3.1'. 126 | """ 127 | auth: tuple[str, str] = ( 128 | os.environ["MJ_APIKEY_PUBLIC"], 129 | os.environ["MJ_APIKEY_PRIVATE"], 130 | ) 131 | return Client( 132 | auth=auth, 133 | version="v3.1", 134 | ) 135 | 136 | 137 | def test_json_data_str_or_bytes_with_ensure_ascii( 138 | simple_data: tuple[dict[str, list[dict[str, str]]], str] 139 | ) -> None: 140 | """ 141 | This function tests the conversion of structured data into JSON format with the specified encoding settings. 142 | 143 | Parameters: 144 | simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: 145 | - A dictionary representing structured data with a list of dictionaries. 146 | - A string representing the encoding of the data. 147 | 148 | Returns: 149 | None: The function does not return any value. It performs assertions to validate the JSON conversion. 150 | """ 151 | data, data_encoding = simple_data 152 | ensure_ascii: bool = True 153 | 154 | if "application/json" and data is not None: 155 | json_data: str | bytes | None = None 156 | json_data = json.dumps(data, ensure_ascii=ensure_ascii) 157 | 158 | assert isinstance(json_data, str) 159 | if not ensure_ascii: 160 | json_data = json_data.encode(data_encoding) 161 | assert isinstance(json_data, bytes) 162 | 163 | 164 | def test_json_data_str_or_bytes_with_ensure_ascii_false( 165 | simple_data: tuple[dict[str, list[dict[str, str]]], str] 166 | ) -> None: 167 | """This function tests the conversion of structured data into JSON format with the specified encoding settings. 168 | 169 | It specifically tests the case where the 'ensure_ascii' parameter is set to False. 170 | 171 | Parameters: 172 | simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: 173 | - A dictionary representing structured data with a list of dictionaries. 174 | - A string representing the encoding of the data. 175 | 176 | Returns: 177 | None: The function does not return any value. It performs assertions to validate the JSON conversion. 178 | """ 179 | data, data_encoding = simple_data 180 | ensure_ascii: bool = False 181 | 182 | if "application/json" and data is not None: 183 | json_data: str | bytes | None = None 184 | json_data = json.dumps(data, ensure_ascii=ensure_ascii) 185 | 186 | assert isinstance(json_data, str) 187 | if not ensure_ascii: 188 | json_data = json_data.encode(data_encoding) 189 | assert isinstance(json_data, bytes) 190 | 191 | 192 | def test_json_data_is_none( 193 | simple_data: tuple[dict[str, list[dict[str, str]]], str] 194 | ) -> None: 195 | """ 196 | This function tests the conversion of structured data into JSON format when the data is None. 197 | 198 | Parameters: 199 | simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: 200 | - A dictionary representing structured data with a list of dictionaries. 201 | - A string representing the encoding of the data. 202 | 203 | Returns: 204 | None: The function does not return any value. It performs assertions to validate the JSON conversion. 205 | """ 206 | data, data_encoding = simple_data 207 | ensure_ascii: bool = True 208 | data: dict[str, list[dict[str, str]]] | None = None # type: ignore 209 | 210 | if "application/json" and data is not None: 211 | json_data: str | bytes | None = None 212 | json_data = json.dumps(data, ensure_ascii=ensure_ascii) 213 | 214 | assert isinstance(json_data, str) 215 | if not ensure_ascii: 216 | json_data = json_data.encode(data_encoding) 217 | assert isinstance(json_data, bytes) 218 | 219 | 220 | def test_prepare_url_list_splitting() -> None: 221 | """This function tests the prepare_url function by splitting a string containing underscores and converting the first letter of each word to uppercase. 222 | 223 | The function then compares the resulting list with an expected list. 224 | 225 | Parameters: 226 | None 227 | 228 | Note: 229 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 230 | - It splits the resulting string into a list using the underscore as the delimiter. 231 | - It asserts that the resulting list is equal to the expected list ["contact", "managecontactslists"]. 232 | """ 233 | name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") 234 | split: list[str] = name.split("_") # noqa: FURB184 235 | assert split == ["contact", "managecontactslists"] 236 | 237 | 238 | def test_prepare_url_first_list_element() -> None: 239 | """This function tests the prepare_url function by splitting a string containing underscores, and retrieving the first element of the resulting list. 240 | 241 | Parameters: 242 | None 243 | 244 | Note: 245 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 246 | - It splits the resulting string into a list using the underscore as the delimiter. 247 | - It asserts that the first element of the split list is equal to "contact". 248 | """ 249 | name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") 250 | fname: str = name.split("_")[0] 251 | assert fname == "contact" 252 | 253 | 254 | def test_prepare_url_headers_and_url() -> None: 255 | """Test the prepare_url function by splitting a string containing underscores, and retrieving the first element of the resulting list. 256 | 257 | Additionally, this test verifies the URL and headers generated by the prepare_url function. 258 | 259 | Parameters: 260 | None 261 | 262 | Note: 263 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 264 | - It creates a Config object with the specified version and API URL. 265 | - It retrieves the URL and headers from the Config object using the modified string as the key. 266 | - It asserts that the URL is equal to "https://api.mailjet.com/v3/REST/contact" and that the headers match the expected headers. 267 | """ 268 | name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") 269 | config: Config = Config(version="v3", api_url="https://api.mailjet.com/") 270 | url, headers = config[name] 271 | assert url == "https://api.mailjet.com/v3/REST/contact" 272 | assert headers == { 273 | "Content-type": "application/json", 274 | "User-agent": f"mailjet-apiv3-python/v{get_version()}", 275 | } 276 | 277 | 278 | # ======= TEST CLIENT ======== 279 | 280 | 281 | def test_post_with_no_param(client_mj30: Client) -> None: 282 | """Tests a POST request with an empty data payload. 283 | 284 | This test sends a POST request to the 'create' endpoint using an empty dictionary 285 | as the data payload. It checks that the API responds with a 400 status code, 286 | indicating a bad request due to missing required parameters. 287 | 288 | Parameters: 289 | client_mj30 (Client): An instance of the Mailjet API client. 290 | 291 | Raises: 292 | AssertionError: If "StatusCode" is not in the result or if its value 293 | is not 400. 294 | """ 295 | result = client_mj30.sender.create(data={}).json() 296 | assert "StatusCode" in result and result["StatusCode"] == 400 297 | 298 | 299 | def test_get_no_param(client_mj30: Client) -> None: 300 | """Tests a GET request to retrieve contact data without any parameters. 301 | 302 | This test sends a GET request to the 'contact' endpoint without filters or 303 | additional parameters. It verifies that the response includes both "Data" 304 | and "Count" fields, confirming the endpoint returns a valid structure. 305 | 306 | Parameters: 307 | client_mj30 (Client): An instance of the Mailjet API client. 308 | 309 | Raises: 310 | AssertionError: If "Data" or "Count" are not present in the response. 311 | """ 312 | result: Any = client_mj30.contact.get().json() 313 | assert "Data" in result and "Count" in result 314 | 315 | 316 | def test_client_initialization_with_invalid_api_key( 317 | client_mj30_invalid_auth: Client, 318 | ) -> None: 319 | """This function tests the initialization of a Mailjet API client with invalid authentication credentials. 320 | 321 | Parameters: 322 | client_mj30_invalid_auth (Client): An instance of the Mailjet API client configured for version 3.0. 323 | The client is authenticated using invalid public and private API keys. 324 | 325 | Returns: 326 | None: The function does not return any value. It is expected to raise a ValueError when the client is used to make requests. 327 | 328 | Note: 329 | - The function uses the pytest.raises context manager to assert that a ValueError is raised when the client's contact.get() method is called. 330 | """ 331 | with pytest.raises(ValueError): 332 | client_mj30_invalid_auth.contact.get().json() 333 | 334 | 335 | def test_prepare_url_mixed_case_input() -> None: 336 | """Test prepare_url function with mixed case input. 337 | 338 | This function tests the prepare_url function by providing a string with mixed case characters. 339 | It then compares the resulting URL with the expected URL. 340 | 341 | Parameters: 342 | None 343 | 344 | Note: 345 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 346 | - It creates a Config object with the specified version and API URL. 347 | - It retrieves the URL and headers from the Config object using the modified string as the key. 348 | - It asserts that the URL is equal to the expected URL and that the headers match the expected headers. 349 | """ 350 | name: str = re.sub(r"[A-Z]", prepare_url, "contact") 351 | config: Config = Config(version="v3", api_url="https://api.mailjet.com/") 352 | url, headers = config[name] 353 | assert url == "https://api.mailjet.com/v3/REST/contact" 354 | assert headers == { 355 | "Content-type": "application/json", 356 | "User-agent": f"mailjet-apiv3-python/v{get_version()}", 357 | } 358 | 359 | 360 | def test_prepare_url_empty_input() -> None: 361 | """Test prepare_url function with empty input. 362 | 363 | This function tests the prepare_url function by providing an empty string as input. 364 | It then compares the resulting URL with the expected URL. 365 | 366 | Parameters: 367 | None 368 | 369 | Note: 370 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 371 | - It creates a Config object with the specified version and API URL. 372 | - It retrieves the URL and headers from the Config object using the modified string as the key. 373 | - It asserts that the URL is equal to the expected URL and that the headers match the expected headers. 374 | """ 375 | name = re.sub(r"[A-Z]", prepare_url, "") 376 | config = Config(version="v3", api_url="https://api.mailjet.com/") 377 | url, headers = config[name] 378 | assert url == "https://api.mailjet.com/v3/REST/" 379 | assert headers == { 380 | "Content-type": "application/json", 381 | "User-agent": f"mailjet-apiv3-python/v{get_version()}", 382 | } 383 | 384 | 385 | def test_prepare_url_with_numbers_input_bad() -> None: 386 | """Test the prepare_url function with input containing numbers. 387 | 388 | This function tests the prepare_url function by providing a string with numbers. 389 | It then compares the resulting URL with the expected URL. 390 | 391 | Parameters: 392 | None 393 | 394 | Note: 395 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 396 | - It creates a Config object with the specified version and API URL. 397 | - It retrieves the URL and headers from the Config object using the modified string as the key. 398 | - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. 399 | """ 400 | name = re.sub(r"[A-Z]", prepare_url, "contact1_managecontactslists1") 401 | config = Config(version="v3", api_url="https://api.mailjet.com/") 402 | url, headers = config[name] 403 | assert url != "https://api.mailjet.com/v3/REST/contact" 404 | assert headers == { 405 | "Content-type": "application/json", 406 | "User-agent": f"mailjet-apiv3-python/v{get_version()}", 407 | } 408 | 409 | 410 | def test_prepare_url_leading_trailing_underscores_input_bad() -> None: 411 | """Test prepare_url function with input containing leading and trailing underscores. 412 | 413 | This function tests the prepare_url function by providing a string with leading and trailing underscores. 414 | It then compares the resulting URL with the expected URL. 415 | 416 | Parameters: 417 | None 418 | 419 | Note: 420 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 421 | - It creates a Config object with the specified version and API URL. 422 | - It retrieves the URL and headers from the Config object using the modified string as the key. 423 | - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. 424 | """ 425 | name: str = re.sub(r"[A-Z]", prepare_url, "_contact_managecontactslists_") 426 | config: Config = Config(version="v3", api_url="https://api.mailjet.com/") 427 | url, headers = config[name] 428 | assert url != "https://api.mailjet.com/v3/REST/contact" 429 | assert headers == { 430 | "Content-type": "application/json", 431 | "User-agent": f"mailjet-apiv3-python/v{get_version()}", 432 | } 433 | 434 | 435 | def test_prepare_url_mixed_case_input_bad() -> None: 436 | """Test prepare_url function with mixed case input. 437 | 438 | This function tests the prepare_url function by providing a string with mixed case characters. 439 | It then compares the resulting URL with the expected URL. 440 | 441 | Parameters: 442 | None 443 | 444 | Note: 445 | - The function uses the re.sub method to replace uppercase letters with the prepare_url function. 446 | - It creates a Config object with the specified version and API URL. 447 | - It retrieves the URL and headers from the Config object using the modified string as the key. 448 | - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. 449 | """ 450 | name: str = re.sub(r"[A-Z]", prepare_url, "cOntact") 451 | config: Config = Config(version="v3", api_url="https://api.mailjet.com/") 452 | url, headers = config[name] 453 | assert url != "https://api.mailjet.com/v3/REST/contact" 454 | assert headers == { 455 | "Content-type": "application/json", 456 | "User-agent": f"mailjet-apiv3-python/v{get_version()}", 457 | } 458 | 459 | 460 | def test_debug_logging_to_stdout_has_all_debug_entries( 461 | client_mj30: Client, 462 | caplog: LogCaptureFixture, 463 | ) -> None: 464 | """This function tests the debug logging to stdout, ensuring that all debug entries are present. 465 | 466 | Parameters: 467 | client_mj30 (Client): An instance of the Mailjet API client. 468 | caplog (LogCaptureFixture): A fixture for capturing log entries. 469 | """ 470 | result = client_mj30.contact.get() 471 | parse_response(result, lambda: logging_handler(to_file=False), debug=True) 472 | 473 | assert result.status_code == 200 474 | assert len(caplog.records) == 6 475 | assert all(x in caplog.text for x in debug_entries()) 476 | 477 | 478 | def test_debug_logging_to_stdout_has_all_debug_entries_when_unknown_or_not_found( 479 | client_mj30: Client, 480 | caplog: LogCaptureFixture, 481 | ) -> None: 482 | """This function tests the debug logging to stdout, ensuring that all debug entries are present. 483 | 484 | Parameters: 485 | client_mj30 (Client): An instance of the Mailjet API client. 486 | caplog (LogCaptureFixture): A fixture for capturing log entries. 487 | """ 488 | # A wrong "cntact" endpoint to get 400 "Unknown resource" error message 489 | result = client_mj30.cntact.get() 490 | parse_response(result, lambda: logging_handler(to_file=False), debug=True) 491 | 492 | assert 400 <= result.status_code <= 404 493 | assert len(caplog.records) == 8 494 | assert all(x in caplog.text for x in debug_entries()) 495 | 496 | 497 | def test_debug_logging_to_stdout_when_retrieve_message_with_id_type_mismatch( 498 | client_mj30: Client, 499 | caplog: LogCaptureFixture, 500 | ) -> None: 501 | """This function tests the debug logging to stdout by retrieving message if id type mismatch, ensuring that all debug entries are present. 502 | 503 | GET https://api.mailjet.com/v3/REST/message/$MESSAGE_ID 504 | 505 | Parameters: 506 | client_mj30 (Client): An instance of the Mailjet API client. 507 | caplog (LogCaptureFixture): A fixture for capturing log entries. 508 | """ 509 | _id = "*************" # $MESSAGE_ID with all "*" will cause "Incorrect ID provided - ID type mismatch" (Error 400). 510 | result = client_mj30.message.get(_id) 511 | parse_response(result, lambda: logging_handler(to_file=False), debug=True) 512 | 513 | assert result.status_code == 400 514 | assert len(caplog.records) == 8 515 | assert all(x in caplog.text for x in debug_entries()) 516 | 517 | 518 | def test_debug_logging_to_stdout_when_retrieve_message_with_object_not_found( 519 | client_mj30: Client, 520 | caplog: LogCaptureFixture, 521 | ) -> None: 522 | """This function tests the debug logging to stdout by retrieving message if object not found, ensuring that all debug entries are present. 523 | 524 | GET https://api.mailjet.com/v3/REST/message/$MESSAGE_ID 525 | 526 | Parameters: 527 | client_mj30 (Client): An instance of the Mailjet API client. 528 | caplog (LogCaptureFixture): A fixture for capturing log entries. 529 | """ 530 | _id = "0000000000000" # $MESSAGE_ID with all zeros "0" will cause "Object not found" (Error 404). 531 | result = client_mj30.message.get(_id) 532 | parse_response(result, lambda: logging_handler(to_file=False), debug=True) 533 | 534 | assert result.status_code == 404 535 | assert len(caplog.records) == 8 536 | assert all(x in caplog.text for x in debug_entries()) 537 | 538 | 539 | def test_debug_logging_to_log_file( 540 | client_mj30: Client, caplog: LogCaptureFixture 541 | ) -> None: 542 | """This function tests the debug logging to a log file. 543 | 544 | It sends a GET request to the 'contact' endpoint of the Mailjet API client, parses the response, 545 | logs the debug information to a log file, validates that the log filename has the correct datetime format provided, 546 | and then verifies the existence and removal of the log file. 547 | 548 | Parameters: 549 | client_mj30 (Client): An instance of the Mailjet API client. 550 | caplog (LogCaptureFixture): A fixture for capturing log entries. 551 | """ 552 | result = client_mj30.contact.get() 553 | parse_response(result, logging_handler, debug=True) 554 | partial(logging_handler, to_file=True) 555 | cwd = Path.cwd() 556 | log_files = glob.glob("*.log") 557 | for log_file in log_files: 558 | log_file_name = Path(log_file).stem 559 | validate_datetime_format(log_file_name, "%Y%m%d_%H%M%S") 560 | log_file_path = os.path.join(cwd, log_file) 561 | 562 | assert result.status_code == 200 563 | assert Path(log_file_path).exists() 564 | 565 | print(f"Removing log file {log_file}...") 566 | Path(log_file_path).unlink() 567 | print(f"The log file {log_file} has been removed.") 568 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from mailjet_rest.utils.version import get_version, VERSION 6 | 7 | 8 | def test_version_length_equal_three() -> None: 9 | """Verify that the tuple contains 3 items.""" 10 | assert len(VERSION) == 3 11 | 12 | 13 | def test_get_version_is_none() -> None: 14 | """Test that package version is none.""" 15 | version: None = None 16 | result: str | tuple[int, ...] 17 | result = get_version(version) 18 | assert isinstance(result, str) 19 | result = tuple(map(int, result.split("."))) 20 | assert result == VERSION 21 | assert isinstance(result, tuple) 22 | 23 | 24 | def test_get_version() -> None: 25 | """Test that package version is string. 26 | 27 | Verify that if it's equal to tuple after splitting and mapped to tuple. 28 | """ 29 | result: str | tuple[int, ...] 30 | result = get_version(VERSION) 31 | assert isinstance(result, str) 32 | result = tuple(map(int, result.split("."))) 33 | assert result == VERSION 34 | assert isinstance(result, tuple) 35 | 36 | 37 | def test_get_version_raises_exception() -> None: 38 | """Test that package version raise ValueError if its length is not equal 3.""" 39 | version: tuple[int, int] = ( 40 | 1, 41 | 2, 42 | ) 43 | with pytest.raises(ValueError): 44 | get_version(version) 45 | --------------------------------------------------------------------------------