mailgun 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. mailgun/__init__.py +10 -0
  2. mailgun/_version.py +1 -0
  3. mailgun/client.py +510 -0
  4. mailgun/doc_tests/files/data.csv +5 -0
  5. mailgun/doc_tests/files/email_previews.csv +18 -0
  6. mailgun/doc_tests/files/email_validation.csv +5 -0
  7. mailgun/doc_tests/files/mailgun_bounces_test.csv +5 -0
  8. mailgun/doc_tests/files/mailgun_complaints.csv +4 -0
  9. mailgun/doc_tests/files/mailgun_unsubscribes.csv +5 -0
  10. mailgun/doc_tests/files/mailgun_whitelists.csv +4 -0
  11. mailgun/doc_tests/files/test1.txt +1 -0
  12. mailgun/doc_tests/files/test2.txt +1 -0
  13. mailgun/doc_tests/files/test_mime.mime +7 -0
  14. mailgun/examples/__init__.py +0 -0
  15. mailgun/examples/domain_examples.py +243 -0
  16. mailgun/examples/email_validation_examples.py +113 -0
  17. mailgun/examples/events_examples.py +62 -0
  18. mailgun/examples/inbox_placement_examples.py +92 -0
  19. mailgun/examples/ip_pools_examples.py +77 -0
  20. mailgun/examples/ips_examples.py +59 -0
  21. mailgun/examples/mailing_lists_examples.py +217 -0
  22. mailgun/examples/messages_examples.py +135 -0
  23. mailgun/examples/routes_examples.py +73 -0
  24. mailgun/examples/suppressions_examples.py +355 -0
  25. mailgun/examples/tags_examples.py +94 -0
  26. mailgun/examples/templates_examples.py +149 -0
  27. mailgun/examples/webhooks_examples.py +64 -0
  28. mailgun/handlers/__init__.py +1 -0
  29. mailgun/handlers/default_handler.py +38 -0
  30. mailgun/handlers/domains_handler.py +98 -0
  31. mailgun/handlers/email_validation_handler.py +35 -0
  32. mailgun/handlers/error_handler.py +14 -0
  33. mailgun/handlers/inbox_placement_handler.py +71 -0
  34. mailgun/handlers/ip_pools_handler.py +43 -0
  35. mailgun/handlers/ips_handler.py +35 -0
  36. mailgun/handlers/mailinglists_handler.py +54 -0
  37. mailgun/handlers/messages_handler.py +33 -0
  38. mailgun/handlers/routes_handler.py +35 -0
  39. mailgun/handlers/suppressions_handler.py +110 -0
  40. mailgun/handlers/tags_handler.py +41 -0
  41. mailgun/handlers/templates_handler.py +62 -0
  42. mailgun-1.0.0.dist-info/METADATA +1083 -0
  43. mailgun-1.0.0.dist-info/RECORD +48 -0
  44. mailgun-1.0.0.dist-info/WHEEL +5 -0
  45. mailgun-1.0.0.dist-info/licenses/LICENSE +202 -0
  46. mailgun-1.0.0.dist-info/top_level.txt +2 -0
  47. tests/__init__.py +6 -0
  48. tests/tests.py +1425 -0
mailgun/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """The `mailgun` package provides a Python SDK for interacting with the Mailgun API.
2
+
3
+ Packages:
4
+ - examples: basic examples.
5
+ - handlers: predefined handlers.
6
+
7
+ Modules:
8
+ - client: Defines the main API client.
9
+
10
+ """
mailgun/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
mailgun/client.py ADDED
@@ -0,0 +1,510 @@
1
+ """This module provides the main client and helper classes for interacting with the Mailgun API.
2
+
3
+ The `mailgun.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
+ Classes:
7
+ - Config: Manages configuration settings for the Mailgun API.
8
+ - Endpoint: Represents specific API endpoints and provides methods for
9
+ common HTTP operations like GET, POST, PUT, and DELETE.
10
+ - Client: The main API client for authenticating and making requests.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from typing import TYPE_CHECKING
17
+ from typing import Any
18
+ from typing import Callable
19
+ from urllib.parse import urljoin
20
+
21
+ import requests # type: ignore[import-untyped]
22
+
23
+ from mailgun.handlers.default_handler import handle_default
24
+ from mailgun.handlers.domains_handler import handle_domainlist
25
+ from mailgun.handlers.domains_handler import handle_domains
26
+ from mailgun.handlers.domains_handler import handle_sending_queues
27
+ from mailgun.handlers.email_validation_handler import handle_address_validate
28
+ from mailgun.handlers.error_handler import ApiError
29
+ from mailgun.handlers.inbox_placement_handler import handle_inbox
30
+ from mailgun.handlers.ip_pools_handler import handle_ippools
31
+ from mailgun.handlers.ips_handler import handle_ips
32
+ from mailgun.handlers.mailinglists_handler import handle_lists
33
+ from mailgun.handlers.messages_handler import handle_resend_message
34
+ from mailgun.handlers.routes_handler import handle_routes
35
+ from mailgun.handlers.suppressions_handler import handle_bounces
36
+ from mailgun.handlers.suppressions_handler import handle_complaints
37
+ from mailgun.handlers.suppressions_handler import handle_unsubscribes
38
+ from mailgun.handlers.suppressions_handler import handle_whitelists
39
+ from mailgun.handlers.tags_handler import handle_tags
40
+ from mailgun.handlers.templates_handler import handle_templates
41
+
42
+
43
+ if TYPE_CHECKING:
44
+ from collections.abc import Mapping
45
+
46
+ from requests.models import Response # type: ignore[import-untyped]
47
+
48
+
49
+ HANDLERS: dict[str, Callable] = { # type: ignore[type-arg]
50
+ "resendmessage": handle_resend_message,
51
+ "domains": handle_domains,
52
+ "domainlist": handle_domainlist,
53
+ "dkim_authority": handle_domains,
54
+ "dkim_selector": handle_domains,
55
+ "web_prefix": handle_domains,
56
+ "sending_queues": handle_sending_queues,
57
+ "ips": handle_ips,
58
+ "ip_pools": handle_ippools,
59
+ "tags": handle_tags,
60
+ "bounces": handle_bounces,
61
+ "unsubscribes": handle_unsubscribes,
62
+ "whitelists": handle_whitelists,
63
+ "complaints": handle_complaints,
64
+ "routes": handle_routes,
65
+ "lists": handle_lists,
66
+ "templates": handle_templates,
67
+ "addressvalidate": handle_address_validate,
68
+ "inbox": handle_inbox,
69
+ "messages": handle_default,
70
+ "messages.mime": handle_default,
71
+ "events": handle_default,
72
+ "stats": handle_default,
73
+ }
74
+
75
+
76
+ class Config:
77
+ """Config class. Configure client with basic (urls, version, headers)."""
78
+
79
+ DEFAULT_API_URL: str = "https://api.mailgun.net/"
80
+ API_REF: str = "https://documentation.mailgun.com/en/latest/api_reference.html"
81
+ user_agent: str = "mailgun-api-python/"
82
+
83
+ def __init__(self, api_url: str | None = None) -> None:
84
+ """Initialize a new Config instance with specified or default API settings.
85
+
86
+ This initializer sets the API version and base URL. If no version or URL
87
+ is provided, it defaults to the predefined class values.
88
+
89
+ :param version: API version (default: v3)
90
+ :type version: str | None
91
+ :param api_url: API base url
92
+ :type api_url: str | None
93
+ """
94
+ self.ex_handler: bool = True
95
+ self.api_url = api_url or self.DEFAULT_API_URL
96
+
97
+ def __getitem__(self, key: str) -> tuple[Any, dict[str, str]]:
98
+ """Parse incoming split attr name, check it and prepare endpoint url.
99
+
100
+ Most urls generated here can't be generated dynamically as we are doing this
101
+ in build_url() method under Endpoint class.
102
+ :param key: incoming attr name
103
+ :type key: str
104
+ :return: url, headers
105
+ """
106
+ key = key.lower()
107
+ headers = {"User-agent": self.user_agent}
108
+ v1_base = urljoin(self.api_url, "v1/")
109
+ v3_base = urljoin(self.api_url, "v3/")
110
+ v4_base = urljoin(self.api_url, "v4/")
111
+ v5_base = urljoin(self.api_url, "v5/")
112
+
113
+ special_cases = {
114
+ "messages": {"base": v3_base, "keys": ["messages"]},
115
+ "mimemessage": {"base": v3_base, "keys": ["messages.mime"]},
116
+ "resendmessage": {"base": v3_base, "keys": ["resendmessage"]},
117
+ "ippools": {"base": v3_base, "keys": ["ip_pools"]},
118
+ "dkimkeys": {"base": v1_base, "keys": ["dkim", "keys"]},
119
+ "domainlist": {"base": v4_base, "keys": ["domainlist"]},
120
+ }
121
+
122
+ if key in special_cases:
123
+ return special_cases[key], headers
124
+
125
+ # Handle DIPP endpoints
126
+ if "subaccount" in key:
127
+ if "ip_pools" in key:
128
+ return {
129
+ "base": v5_base,
130
+ "keys": ["accounts", "subaccounts", "ip_pools"],
131
+ }, headers
132
+ if "ip_pool" in key:
133
+ return {
134
+ "base": v5_base,
135
+ "keys": ["accounts", "subaccounts", "{subaccountId}", "ip_pool"],
136
+ }, headers
137
+
138
+ # Handle DKIM management endpoints
139
+ if "dkim_management" in key:
140
+ if "rotation" in key:
141
+ return {
142
+ "base": v1_base,
143
+ "keys": ["dkim_management", "domains", "{name}", "rotation"],
144
+ }, headers
145
+ if "rotate" in key:
146
+ return {
147
+ "base": v1_base,
148
+ "keys": ["dkim_management", "domains", "{name}", "rotate"],
149
+ }, headers
150
+
151
+ if "domains" in key:
152
+ split = key.split("_") if "_" in key else [key]
153
+ final_keys = split
154
+
155
+ if any(x in key for x in ("activate", "deactivate")):
156
+ action = "activate" if "activate" in key else "deactivate"
157
+ final_keys = [
158
+ "domains",
159
+ "{authority_name}",
160
+ "keys",
161
+ "{selector}",
162
+ action,
163
+ ]
164
+ return {"base": v4_base, "keys": final_keys}, headers
165
+
166
+ if "dkimauthority" in split:
167
+ final_keys = ["dkim_authority"]
168
+ elif "dkimselector" in split:
169
+ final_keys = ["dkim_selector"]
170
+ elif "webprefix" in split:
171
+ final_keys = ["web_prefix"]
172
+ elif "sendingqueues" in split:
173
+ final_keys = ["sending_queues"]
174
+
175
+ v3_domain_endpoints = {
176
+ "credentials",
177
+ "connection",
178
+ "tracking",
179
+ "dkimauthority",
180
+ "dkimselector",
181
+ "webprefix",
182
+ "webhooks",
183
+ "sendingqueues",
184
+ }
185
+ base = v3_base if any(x in key for x in v3_domain_endpoints) else v4_base
186
+ return {"base": f"{base}domains/", "keys": final_keys}, headers
187
+
188
+ if "addressvalidate" in key:
189
+ return {
190
+ "base": f"{v4_base}address/validate",
191
+ "keys": key.split("_"),
192
+ }, headers
193
+
194
+ return {"base": v3_base, "keys": key.split("_")}, headers
195
+
196
+
197
+ class Endpoint:
198
+ """Generate request and return response."""
199
+
200
+ def __init__(
201
+ self,
202
+ url: dict[str, Any],
203
+ headers: dict[str, str],
204
+ auth: tuple[str, str] | None,
205
+ ) -> None:
206
+ """Initialize a new Endpoint instance.
207
+
208
+ :param url: URL dict with pairs {"base": "keys"}
209
+ :type url: dict[str, Any]
210
+ :param headers: Headers dict
211
+ :type headers: dict[str, str]
212
+ :param auth: requests auth tuple
213
+ :type auth: tuple[str, str] | None
214
+ """
215
+ self._url = url
216
+ self.headers = headers
217
+ self._auth = auth
218
+
219
+ def api_call(
220
+ self,
221
+ auth: tuple[str, str] | None,
222
+ method: str,
223
+ url: dict[str, Any],
224
+ headers: dict[str, str],
225
+ data: Any | None = None,
226
+ filters: Mapping[str, str | Any] | None = None,
227
+ timeout: int = 60,
228
+ files: dict[str, bytes] | None = None,
229
+ domain: str | None = None,
230
+ **kwargs: Any,
231
+ ) -> Response | Any:
232
+ """Build URL and make a request.
233
+
234
+ :param auth: auth data
235
+ :type auth: tuple[str, str] | None
236
+ :param method: request method
237
+ :type method: str
238
+ :param url: incoming url (base+keys)
239
+ :type url: dict[str, Any]
240
+ :param headers: incoming headers
241
+ :type headers: dict[str, str]
242
+ :param data: incoming post/put data
243
+ :type data: Any | None
244
+ :param filters: incoming params
245
+ :type filters: dict | None
246
+ :param timeout: requested timeout (60-default)
247
+ :type timeout: int
248
+ :param files: incoming files
249
+ :type files: dict[str, Any] | None
250
+ :param domain: incoming domain
251
+ :type domain: str | None
252
+ :param kwargs: kwargs
253
+ :type kwargs: Any
254
+ :return: server response from API
255
+ :rtype: requests.models.Response
256
+ :raises: TimeoutError, ApiError
257
+ """
258
+ url = self.build_url(url, domain=domain, method=method, **kwargs)
259
+ req_method = getattr(requests, method)
260
+
261
+ try:
262
+ return req_method(
263
+ url,
264
+ data=data,
265
+ params=filters,
266
+ headers=headers,
267
+ auth=auth,
268
+ timeout=timeout,
269
+ files=files,
270
+ verify=True,
271
+ stream=False,
272
+ )
273
+
274
+ except requests.exceptions.Timeout:
275
+ raise TimeoutError
276
+ except requests.RequestException as e:
277
+ raise ApiError(e)
278
+ except Exception as e:
279
+ raise e
280
+
281
+ @staticmethod
282
+ def build_url(
283
+ url: dict[str, Any],
284
+ domain: str | None = None,
285
+ method: str | None = None,
286
+ **kwargs: Any,
287
+ ) -> Any:
288
+ """Build final request url using predefined handlers.
289
+
290
+ Note: Some urls are being built in Config class, as they can't be generated dynamically.
291
+ :param url: incoming url (base+keys)
292
+ :type url: dict[str, Any]
293
+ :param domain: incoming domain
294
+ :type domain: str
295
+ :param method: requested method
296
+ :type method: str
297
+ :param kwargs: kwargs
298
+ :type kwargs: Any
299
+ :return: built URL
300
+ """
301
+ return HANDLERS[url["keys"][0]](url, domain, method, **kwargs)
302
+
303
+ def get(
304
+ self,
305
+ filters: Mapping[str, str | Any] | None = None,
306
+ domain: str | None = None,
307
+ **kwargs: Any,
308
+ ) -> Response:
309
+ """GET method for API calls.
310
+
311
+ :param filters: incoming params
312
+ :type filters: Mapping[str, str | Any] | None
313
+ :param domain: incoming domain
314
+ :type domain: str | None
315
+ :param kwargs: kwargs
316
+ :type kwargs: Any
317
+ :return: api_call GET request
318
+ :rtype: requests.models.Response
319
+ """
320
+ return self.api_call(
321
+ self._auth,
322
+ "get",
323
+ self._url,
324
+ domain=domain,
325
+ headers=self.headers,
326
+ filters=filters,
327
+ **kwargs,
328
+ )
329
+
330
+ def create(
331
+ self,
332
+ data: Any | None = None,
333
+ filters: Mapping[str, str | Any] | None = None,
334
+ domain: str | None = None,
335
+ headers: str | None = None,
336
+ files: dict[str, bytes] | None = None,
337
+ **kwargs: Any,
338
+ ) -> Response:
339
+ """POST method for API calls.
340
+
341
+ :param data: incoming post data
342
+ :type data: Any | None
343
+ :param filters: incoming params
344
+ :type filters: dict
345
+ :param domain: incoming domain
346
+ :type domain: str
347
+ :param headers: incoming headers
348
+ :type headers: str | None
349
+ :param files: incoming files
350
+ :type files: dict[str, Any] | None
351
+ :param kwargs: kwargs
352
+ :type kwargs: Any
353
+ :return: api_call POST request
354
+ :rtype: requests.models.Response
355
+ """
356
+ if "Content-type" in self.headers:
357
+ if self.headers["Content-type"] == "application/json":
358
+ data = json.dumps(data)
359
+ elif headers:
360
+ if headers == "application/json":
361
+ data = json.dumps(data)
362
+ self.headers["Content-type"] = "application/json"
363
+ elif headers == "multipart/form-data":
364
+ self.headers["Content-type"] = "multipart/form-data"
365
+
366
+ return self.api_call(
367
+ self._auth,
368
+ "post",
369
+ self._url,
370
+ files=files,
371
+ domain=domain,
372
+ headers=self.headers,
373
+ data=data,
374
+ filters=filters,
375
+ **kwargs,
376
+ )
377
+
378
+ def put(
379
+ self,
380
+ data: Any | None = None,
381
+ filters: Mapping[str, str | Any] | None = None,
382
+ **kwargs: Any,
383
+ ) -> Response:
384
+ """PUT method for API calls.
385
+
386
+ :param data: incoming data
387
+ :type data: Any | None
388
+ :param filters: incoming params
389
+ :type filters: dict
390
+ :param kwargs: kwargs
391
+ :type kwargs: Any
392
+ :return: api_call POST request
393
+ :rtype: requests.models.Response
394
+ """
395
+ return self.api_call(
396
+ self._auth,
397
+ "put",
398
+ self._url,
399
+ headers=self.headers,
400
+ data=data,
401
+ filters=filters,
402
+ **kwargs,
403
+ )
404
+
405
+ def patch(
406
+ self,
407
+ data: Any | None = None,
408
+ filters: Mapping[str, str | Any] | None = None,
409
+ **kwargs: Any,
410
+ ) -> Response:
411
+ """PATCH method for API calls.
412
+
413
+ :param data: incoming data
414
+ :type data: Any | None
415
+ :param filters: incoming params
416
+ :type filters: dict
417
+ :param kwargs: kwargs
418
+ :type kwargs: Any
419
+ :return: api_call PATCH request
420
+ :rtype: requests.models.Response
421
+ """
422
+ return self.api_call(
423
+ self._auth,
424
+ "patch",
425
+ self._url,
426
+ headers=self.headers,
427
+ data=data,
428
+ filters=filters,
429
+ **kwargs,
430
+ )
431
+
432
+ def update(
433
+ self,
434
+ data: Any | None,
435
+ filters: Mapping[str, str | Any] | None = None,
436
+ **kwargs: Any,
437
+ ) -> Response:
438
+ """PUT method for API calls.
439
+
440
+ :param data: incoming data
441
+ :type data: dict[str, Any] | None
442
+ :param filters: incoming params
443
+ :type filters: dict
444
+ :param kwargs: kwargs
445
+ :type kwargs: Any
446
+ :return: api_call PUT request
447
+ :rtype: requests.models.Response
448
+ """
449
+ if self.headers["Content-type"] == "application/json":
450
+ data = json.dumps(data)
451
+ return self.api_call(
452
+ self._auth,
453
+ "put",
454
+ self._url,
455
+ headers=self.headers,
456
+ data=data,
457
+ filters=filters,
458
+ **kwargs,
459
+ )
460
+
461
+ def delete(self, domain: str | None = None, **kwargs: Any) -> Response:
462
+ """DELETE method for API calls.
463
+
464
+ :param domain: incoming domain
465
+ :type domain: str
466
+ :param kwargs: kwargs
467
+ :type kwargs: Any
468
+ :return: api_call DELETE request
469
+ :rtype: requests.models.Response
470
+ """
471
+ return self.api_call(
472
+ self._auth,
473
+ "delete",
474
+ self._url,
475
+ headers=self.headers,
476
+ domain=domain,
477
+ **kwargs,
478
+ )
479
+
480
+
481
+ class Client:
482
+ """Client class."""
483
+
484
+ def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None:
485
+ """Initialize a new Client instance for API interaction.
486
+
487
+ This method sets up API authentication and configuration. The `auth` parameter
488
+ provides a tuple with the API key and secret. Additional keyword arguments can
489
+ specify configuration options like API version and URL.
490
+
491
+ :param auth: auth set ("username", "APIKEY")
492
+ :type auth: set
493
+ :param kwargs: kwargs
494
+ """
495
+ self.auth = auth
496
+ api_url = kwargs.get("api_url")
497
+ self.config = Config(api_url=api_url)
498
+
499
+ def __getattr__(self, name: str) -> Any:
500
+ """Get named attribute of an object, split it and execute.
501
+
502
+ :param name: attribute name (Example: client.domains_ips. names: ["domains", "ips"])
503
+ :type name: str
504
+ :return: type object (executes existing handler)
505
+ """
506
+ split = name.split("_")
507
+ # identify the resource
508
+ fname = split[0]
509
+ url, headers = self.config[name]
510
+ return type(fname, (Endpoint,), {})(url=url, headers=headers, auth=self.auth)
@@ -0,0 +1,5 @@
1
+ address,name,subscribed,vars
2
+ bob@mywebsite.com,Bob3,TRUE,34
3
+ jane@example.com,Janen,TRUE,21
4
+ pete@example.com,Pete,TRUE,44
5
+ foo@example.com,Foo,TRUE,20
@@ -0,0 +1,18 @@
1
+ email
2
+ 2@test.com
3
+ 3@test.com
4
+ 4@test.com
5
+ 5@test.com
6
+ 6@test.com
7
+ 7@test.com
8
+ 8@test.com
9
+ 9@test.com
10
+ 10@test.com
11
+ 11@test.com
12
+ 12@test.com
13
+ 13@test.com
14
+ 14@test.com
15
+ 15@test.com
16
+ 16@test.com
17
+ 17@test.com
18
+ 18@test.com
@@ -0,0 +1,5 @@
1
+ email
2
+ 2@test.com
3
+ 3@test.com
4
+ 4@test.com
5
+ 5@test.com
@@ -0,0 +1,5 @@
1
+ address,code,error,created_at
2
+ foo@example.com,,,
3
+ bar@example.org,,,
4
+ new@example.net,,,
5
+ address@example.com,,,
@@ -0,0 +1,4 @@
1
+ address,created_at
2
+ adc@gmail.com,"Thu, 13 Oct 2011 18:02:00 GMT"
3
+ vdb@gmail.com,"Thu, 13 Oct 2011 18:02:00 GMT"
4
+ zxc@gmail.com,"Thu, 13 Oct 2011 18:02:00 GMT"
@@ -0,0 +1,5 @@
1
+ address,tags,created_at
2
+ foo@example.com,,
3
+ bar@example.org,,
4
+ new@example.net,,
5
+ address@example.com,,
@@ -0,0 +1,4 @@
1
+ address, domain
2
+ adc@gmail.com, "2048.zeefarmer.com",
3
+ vdb@gmail.com, "2048.zeefarmer.com",
4
+ zxc@gmail.com, "2048.zeefarmer.com"
@@ -0,0 +1 @@
1
+ First files!!!!!
@@ -0,0 +1 @@
1
+ This is a test file
@@ -0,0 +1,7 @@
1
+ Content-Type: text/plain; charset="ascii"
2
+ Subject: Joe's Example Subject
3
+ From: Joe Example <joe@example.com>
4
+ To: John Doe <john.doe@example.com>
5
+ Content-Transfer-Encoding: 7bit
6
+ Date: Thu, 6 Mar 2014 00:37:52 +0000
7
+ Testing some Mailgun MIME awesomeness!
File without changes