vaemail 1.0.0__tar.gz

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.
vaemail-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaemail
3
+ Version: 1.0.0
4
+ Summary: Email infrastructure for AI agents and applications: send transactional email, authenticate sending domains, track delivery, diagnose deliverability.
5
+ Author: VaEmail (Agence SW)
6
+ License: MIT
7
+ Project-URL: Homepage, https://vaemail.fr/agents
8
+ Project-URL: Documentation, https://vaemail.fr/docs/api
9
+ Project-URL: Source, https://github.com/vaemail/vaemail-python
10
+ Keywords: email,email-api,transactional-email,ai-agent,agent,deliverability,sdk,vaemail,eu
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ # vaemail — Python SDK
15
+
16
+ Email infrastructure for AI agents and applications. Send transactional email,
17
+ authenticate sending domains, track delivery, and diagnose deliverability.
18
+
19
+ No dependencies: the SDK uses only the standard library, so it installs without
20
+ pulling anything behind it.
21
+
22
+ ```bash
23
+ pip install vaemail
24
+ ```
25
+
26
+ ## Send an email
27
+
28
+ ```python
29
+ from vaemail import VaEmail
30
+
31
+ client = VaEmail(api_key="your-api-key")
32
+
33
+ client.send({
34
+ "to": "customer@example.com",
35
+ "subject": "Your order is on its way",
36
+ "html": "<p>Tracking number: 1Z999</p>",
37
+ }, idempotency_key="order-4711")
38
+ ```
39
+
40
+ The idempotency key makes the call safe to replay for 24 hours: retrying after a
41
+ timeout will not send a second email.
42
+
43
+ ## Try before sending
44
+
45
+ ```python
46
+ client.validate({"to": "customer@example.com", "subject": "Hi", "html": "<p>Hi</p>"})
47
+ ```
48
+
49
+ Same checks as a real send — key scope, quotas, sending domain, suppression
50
+ list — but nothing leaves. Useful when an agent builds a message on its own.
51
+
52
+ ## Authenticate a sending domain
53
+
54
+ ```python
55
+ result = client.add_domain("news.example.com", dkim_tokens=["aaa111", "bbb222", "ccc333"])
56
+
57
+ for record in result["data"]["dns_records"]:
58
+ if record["publishable"]:
59
+ print(record["type"], record["host"], "->", record["value"])
60
+ ```
61
+
62
+ The three DKIM tokens come from Amazon SES when the domain identity is created.
63
+ Every record says whether it is `publishable` as-is: a value that still depends
64
+ on something unknown is flagged instead of being handed over, so an agent never
65
+ publishes a record that would break the domain's authentication.
66
+
67
+ ## Errors say what to do next
68
+
69
+ ```python
70
+ from vaemail import VaEmailError
71
+
72
+ try:
73
+ client.send({"to": "customer@example.com"})
74
+ except VaEmailError as error:
75
+ print(error.code) # DOMAIN_NOT_VERIFIED
76
+ print(error.retryable) # False
77
+ print(error.for_agent()) # reason, corrective action, whether to retry
78
+ ```
79
+
80
+ ## Everything else
81
+
82
+ `capabilities()`, `health()`, `get_message()`, `list_messages()`,
83
+ `list_domains()`, `verify_domain()`, `dns_records()`,
84
+ `diagnose_deliverability()`, `list_suppressions()`, `usage()`, `audit_logs()`.
85
+
86
+ Full API reference: <https://vaemail.fr/docs/api>
87
+
88
+ ## Tests
89
+
90
+ ```bash
91
+ python -m unittest discover -s tests
92
+ ```
93
+
94
+ MIT licensed.
@@ -0,0 +1,81 @@
1
+ # vaemail — Python SDK
2
+
3
+ Email infrastructure for AI agents and applications. Send transactional email,
4
+ authenticate sending domains, track delivery, and diagnose deliverability.
5
+
6
+ No dependencies: the SDK uses only the standard library, so it installs without
7
+ pulling anything behind it.
8
+
9
+ ```bash
10
+ pip install vaemail
11
+ ```
12
+
13
+ ## Send an email
14
+
15
+ ```python
16
+ from vaemail import VaEmail
17
+
18
+ client = VaEmail(api_key="your-api-key")
19
+
20
+ client.send({
21
+ "to": "customer@example.com",
22
+ "subject": "Your order is on its way",
23
+ "html": "<p>Tracking number: 1Z999</p>",
24
+ }, idempotency_key="order-4711")
25
+ ```
26
+
27
+ The idempotency key makes the call safe to replay for 24 hours: retrying after a
28
+ timeout will not send a second email.
29
+
30
+ ## Try before sending
31
+
32
+ ```python
33
+ client.validate({"to": "customer@example.com", "subject": "Hi", "html": "<p>Hi</p>"})
34
+ ```
35
+
36
+ Same checks as a real send — key scope, quotas, sending domain, suppression
37
+ list — but nothing leaves. Useful when an agent builds a message on its own.
38
+
39
+ ## Authenticate a sending domain
40
+
41
+ ```python
42
+ result = client.add_domain("news.example.com", dkim_tokens=["aaa111", "bbb222", "ccc333"])
43
+
44
+ for record in result["data"]["dns_records"]:
45
+ if record["publishable"]:
46
+ print(record["type"], record["host"], "->", record["value"])
47
+ ```
48
+
49
+ The three DKIM tokens come from Amazon SES when the domain identity is created.
50
+ Every record says whether it is `publishable` as-is: a value that still depends
51
+ on something unknown is flagged instead of being handed over, so an agent never
52
+ publishes a record that would break the domain's authentication.
53
+
54
+ ## Errors say what to do next
55
+
56
+ ```python
57
+ from vaemail import VaEmailError
58
+
59
+ try:
60
+ client.send({"to": "customer@example.com"})
61
+ except VaEmailError as error:
62
+ print(error.code) # DOMAIN_NOT_VERIFIED
63
+ print(error.retryable) # False
64
+ print(error.for_agent()) # reason, corrective action, whether to retry
65
+ ```
66
+
67
+ ## Everything else
68
+
69
+ `capabilities()`, `health()`, `get_message()`, `list_messages()`,
70
+ `list_domains()`, `verify_domain()`, `dns_records()`,
71
+ `diagnose_deliverability()`, `list_suppressions()`, `usage()`, `audit_logs()`.
72
+
73
+ Full API reference: <https://vaemail.fr/docs/api>
74
+
75
+ ## Tests
76
+
77
+ ```bash
78
+ python -m unittest discover -s tests
79
+ ```
80
+
81
+ MIT licensed.
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vaemail"
7
+ version = "1.0.0"
8
+ description = "Email infrastructure for AI agents and applications: send transactional email, authenticate sending domains, track delivery, diagnose deliverability."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "VaEmail (Agence SW)" }]
12
+ requires-python = ">=3.9"
13
+ dependencies = []
14
+ keywords = ["email", "email-api", "transactional-email", "ai-agent", "agent", "deliverability", "sdk", "vaemail", "eu"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://vaemail.fr/agents"
18
+ Documentation = "https://vaemail.fr/docs/api"
19
+ Source = "https://github.com/vaemail/vaemail-python"
20
+
21
+ [tool.setuptools]
22
+ packages = ["vaemail"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,107 @@
1
+ """Ce qui compte est ce qui part sur le réseau : on l'observe avec un faux opener.
2
+
3
+ Pas de dépendance de test non plus (unittest de la bibliothèque standard), pour
4
+ que `python -m unittest` suffise partout.
5
+ """
6
+
7
+ import io
8
+ import json
9
+ import sys
10
+ import unittest
11
+ import urllib.error
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
15
+
16
+ from vaemail import DEFAULT_BASE_URL, VaEmail, VaEmailError # noqa: E402
17
+
18
+
19
+ class FausseReponse(io.BytesIO):
20
+ def __enter__(self):
21
+ return self
22
+
23
+ def __exit__(self, *_):
24
+ self.close()
25
+ return False
26
+
27
+
28
+ def espion(charge=None, statut=200):
29
+ appels = []
30
+
31
+ def opener(requete, timeout=None):
32
+ appels.append(requete)
33
+ corps = json.dumps(charge if charge is not None else {"success": True, "data": {}})
34
+ if statut >= 400:
35
+ raise urllib.error.HTTPError(
36
+ requete.full_url, statut, "erreur", {}, io.BytesIO(corps.encode())
37
+ )
38
+ return FausseReponse(corps.encode())
39
+
40
+ return appels, opener
41
+
42
+
43
+ class TestSdk(unittest.TestCase):
44
+ def test_la_cle_part_en_entete_et_la_base_est_la_production(self):
45
+ appels, opener = espion()
46
+ VaEmail(api_key="cle-test", opener=opener).capabilities()
47
+
48
+ self.assertEqual(appels[0].full_url, f"{DEFAULT_BASE_URL}/api/v1/capabilities")
49
+ self.assertEqual(appels[0].get_header("Api-key"), "cle-test")
50
+
51
+ def test_les_jetons_dkim_sont_transmis(self):
52
+ appels, opener = espion()
53
+ VaEmail(api_key="k", opener=opener).add_domain(
54
+ "news.exemple.fr", dkim_tokens=["aaa111", "bbb222", "ccc333"]
55
+ )
56
+
57
+ corps = json.loads(appels[0].data)
58
+ self.assertEqual(corps["dkim_tokens"], ["aaa111", "bbb222", "ccc333"])
59
+ self.assertNotIn("dkim_selector", corps)
60
+
61
+ def test_la_cle_d_idempotence_part_en_entete(self):
62
+ appels, opener = espion()
63
+ VaEmail(api_key="k", opener=opener).send({"to": "x@exemple.fr"}, "commande-42")
64
+
65
+ self.assertEqual(appels[0].get_header("Idempotency-key"), "commande-42")
66
+
67
+ def test_les_filtres_vides_ne_polluent_pas_l_url(self):
68
+ appels, opener = espion()
69
+ VaEmail(api_key="k", opener=opener).list_messages(status="sent", limit=None)
70
+
71
+ self.assertIn("status=sent", appels[0].full_url)
72
+ self.assertNotIn("limit", appels[0].full_url)
73
+
74
+ def test_une_erreur_dit_quoi_faire_et_si_l_appel_peut_etre_rejoue(self):
75
+ _, opener = espion(
76
+ {
77
+ "error": {
78
+ "code": "DOMAIN_NOT_VERIFIED",
79
+ "message": "Domaine non authentifié.",
80
+ "resolution": {"action": "verify_domain", "endpoint": "/api/v1/domains/verify"},
81
+ "retryable": False,
82
+ }
83
+ },
84
+ 422,
85
+ )
86
+
87
+ with self.assertRaises(VaEmailError) as capture:
88
+ VaEmail(api_key="k", opener=opener).send({"to": "x@exemple.fr"})
89
+
90
+ erreur = capture.exception
91
+ self.assertEqual(erreur.code, "DOMAIN_NOT_VERIFIED")
92
+ self.assertEqual(erreur.status, 422)
93
+ self.assertFalse(erreur.retryable)
94
+ self.assertIn("verify_domain", erreur.for_agent())
95
+
96
+ def test_une_erreur_applicative_renvoyee_en_200_est_levee_quand_meme(self):
97
+ _, opener = espion({"error": {"code": "QUOTA_EXCEEDED", "retryable": True}}, 200)
98
+
99
+ with self.assertRaises(VaEmailError) as capture:
100
+ VaEmail(api_key="k", opener=opener).usage()
101
+
102
+ self.assertEqual(capture.exception.code, "QUOTA_EXCEEDED")
103
+ self.assertTrue(capture.exception.retryable)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main()
@@ -0,0 +1,220 @@
1
+ """SDK Python de VaEmail.
2
+
3
+ Aucune dépendance : `urllib` suffit, et un paquet destiné à être installé par un
4
+ agent doit pouvoir s'installer sans rien tirer derrière lui. C'est le même parti
5
+ pris que le paquet npm.
6
+
7
+ from vaemail import VaEmail
8
+
9
+ client = VaEmail(api_key="...")
10
+ client.send({"to": "x@exemple.fr", "subject": "Bonjour", "html": "<p>Bonjour</p>"})
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import urllib.error
17
+ import urllib.parse
18
+ import urllib.request
19
+ from typing import Any
20
+
21
+ __all__ = ["VaEmail", "VaEmailError", "DEFAULT_BASE_URL"]
22
+ __version__ = "1.0.0"
23
+
24
+ DEFAULT_BASE_URL = "https://app.vaemail.fr"
25
+
26
+
27
+ class VaEmailError(Exception):
28
+ """Erreur rendue par l'API.
29
+
30
+ Porte ce qu'un appelant doit savoir pour décider : le code, l'action
31
+ corrective proposée, et si le même appel peut aboutir plus tard. Une
32
+ exception qui ne dit que « erreur 422 » oblige à lire la documentation ;
33
+ celle-ci porte la suite.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ message: str,
39
+ *,
40
+ code: str = "UNKNOWN",
41
+ status: int | None = None,
42
+ resolution: dict[str, Any] | None = None,
43
+ retryable: bool = False,
44
+ body: Any = None,
45
+ ) -> None:
46
+ super().__init__(message)
47
+ self.code = code
48
+ self.status = status
49
+ self.resolution = resolution or {}
50
+ self.retryable = retryable
51
+ self.body = body
52
+
53
+ def for_agent(self) -> str:
54
+ """Le motif, l'action corrective et s'il faut réessayer, en clair."""
55
+ lignes = [f"{self.code}: {self}"]
56
+
57
+ action = self.resolution.get("action")
58
+ if action:
59
+ endpoint = self.resolution.get("endpoint")
60
+ lignes.append(
61
+ f"Action corrective : {action}" + (f" ({endpoint})" if endpoint else "")
62
+ )
63
+
64
+ lignes.append(
65
+ "Le même appel peut aboutir plus tard."
66
+ if self.retryable
67
+ else "Réessayer à l'identique ne changera rien."
68
+ )
69
+
70
+ return "\n".join(lignes)
71
+
72
+
73
+ class VaEmail:
74
+ def __init__(
75
+ self,
76
+ api_key: str | None = None,
77
+ base_url: str = DEFAULT_BASE_URL,
78
+ *,
79
+ timeout: float = 30.0,
80
+ opener: Any = None,
81
+ ) -> None:
82
+ self.api_key = api_key
83
+ self.base_url = base_url.rstrip("/")
84
+ self.timeout = timeout
85
+ # Injectable pour les tests : on ne veut pas d'appel réseau dans une
86
+ # suite de tests, et on ne veut pas non plus d'une dépendance de test.
87
+ self._opener = opener or urllib.request.urlopen
88
+
89
+ # -- transport ---------------------------------------------------------
90
+
91
+ def _call(
92
+ self,
93
+ method: str,
94
+ path: str,
95
+ *,
96
+ body: dict[str, Any] | None = None,
97
+ query: dict[str, Any] | None = None,
98
+ idempotency_key: str | None = None,
99
+ ) -> Any:
100
+ url = self.base_url + path
101
+
102
+ propres = {k: v for k, v in (query or {}).items() if v not in (None, "")}
103
+ if propres:
104
+ url += "?" + urllib.parse.urlencode(propres)
105
+
106
+ entetes = {"Accept": "application/json"}
107
+ if self.api_key:
108
+ entetes["api-key"] = self.api_key
109
+ if idempotency_key:
110
+ entetes["Idempotency-Key"] = idempotency_key
111
+
112
+ donnees = None
113
+ if body is not None:
114
+ donnees = json.dumps(body).encode("utf-8")
115
+ entetes["Content-Type"] = "application/json"
116
+
117
+ requete = urllib.request.Request(url, data=donnees, headers=entetes, method=method)
118
+
119
+ try:
120
+ with self._opener(requete, timeout=self.timeout) as reponse:
121
+ brut = reponse.read().decode("utf-8") or "{}"
122
+ charge = json.loads(brut)
123
+ except urllib.error.HTTPError as erreur:
124
+ brut = erreur.read().decode("utf-8") or "{}"
125
+ try:
126
+ charge = json.loads(brut)
127
+ except json.JSONDecodeError:
128
+ charge = {}
129
+ raise self._erreur(charge, erreur.code) from None
130
+
131
+ # Une réponse 200 peut porter une erreur applicative : on la lève aussi,
132
+ # sinon l'appelant croit avoir réussi.
133
+ if isinstance(charge, dict) and charge.get("error"):
134
+ raise self._erreur(charge, 200)
135
+
136
+ return charge
137
+
138
+ @staticmethod
139
+ def _erreur(charge: Any, status: int) -> VaEmailError:
140
+ details = charge.get("error", {}) if isinstance(charge, dict) else {}
141
+
142
+ return VaEmailError(
143
+ details.get("message") or f"Erreur HTTP {status}.",
144
+ code=details.get("code", "UNKNOWN"),
145
+ status=status,
146
+ resolution=details.get("resolution"),
147
+ retryable=bool(details.get("retryable")),
148
+ body=charge,
149
+ )
150
+
151
+ # -- API ---------------------------------------------------------------
152
+
153
+ def capabilities(self) -> Any:
154
+ return self._call("GET", "/api/v1/capabilities")
155
+
156
+ def health(self) -> Any:
157
+ return self._call("GET", "/api/v1/health")
158
+
159
+ def send(self, message: dict[str, Any], idempotency_key: str | None = None) -> Any:
160
+ """Envoie un message. `idempotency_key` rend l'appel rejouable sans doublon 24 h."""
161
+ return self._call(
162
+ "POST", "/api/v1/transactional/send", body=message, idempotency_key=idempotency_key
163
+ )
164
+
165
+ def validate(self, message: dict[str, Any]) -> Any:
166
+ """Essai à blanc : mêmes contrôles que l'envoi, rien ne part."""
167
+ return self._call("POST", "/api/v1/messages/validate", body=message)
168
+
169
+ def get_message(self, message_id: str | int) -> Any:
170
+ return self._call("GET", f"/api/v1/messages/{urllib.parse.quote(str(message_id))}")
171
+
172
+ def list_messages(self, **filters: Any) -> Any:
173
+ return self._call("GET", "/api/v1/messages", query=filters)
174
+
175
+ def list_domains(self) -> Any:
176
+ return self._call("GET", "/api/v1/domains")
177
+
178
+ def add_domain(
179
+ self,
180
+ domain: str,
181
+ *,
182
+ selector: str | None = None,
183
+ dkim_tokens: list[str] | None = None,
184
+ idempotency_key: str | None = None,
185
+ ) -> Any:
186
+ """Déclare un domaine d'envoi.
187
+
188
+ `dkim_tokens` sont les trois valeurs données par SES à la création de
189
+ l'identité de domaine. Sans elles, la partie DKIM du guide DNS rendu est
190
+ un marqueur et le domaine ne peut pas être terminé sans intervention.
191
+ """
192
+ corps: dict[str, Any] = {"domain": domain}
193
+ if selector:
194
+ corps["dkim_selector"] = selector
195
+ if dkim_tokens:
196
+ corps["dkim_tokens"] = list(dkim_tokens)
197
+
198
+ return self._call("POST", "/api/v1/domains", body=corps, idempotency_key=idempotency_key)
199
+
200
+ def verify_domain(self, domain: str, selector: str | None = None) -> Any:
201
+ corps: dict[str, Any] = {"domain": domain}
202
+ if selector:
203
+ corps["dkim_selector"] = selector
204
+
205
+ return self._call("POST", "/api/v1/domains/verify", body=corps)
206
+
207
+ def dns_records(self, domain: str) -> Any:
208
+ return self._call("GET", f"/api/v1/domains/{urllib.parse.quote(domain)}/dns")
209
+
210
+ def diagnose_deliverability(self, domain: str) -> Any:
211
+ return self._call("GET", "/api/v1/deliverability/diagnose", query={"domain": domain})
212
+
213
+ def list_suppressions(self, **filters: Any) -> Any:
214
+ return self._call("GET", "/api/v1/suppressions", query=filters)
215
+
216
+ def usage(self) -> Any:
217
+ return self._call("GET", "/api/v1/usage")
218
+
219
+ def audit_logs(self, **filters: Any) -> Any:
220
+ return self._call("GET", "/api/v1/audit-logs", query=filters)
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaemail
3
+ Version: 1.0.0
4
+ Summary: Email infrastructure for AI agents and applications: send transactional email, authenticate sending domains, track delivery, diagnose deliverability.
5
+ Author: VaEmail (Agence SW)
6
+ License: MIT
7
+ Project-URL: Homepage, https://vaemail.fr/agents
8
+ Project-URL: Documentation, https://vaemail.fr/docs/api
9
+ Project-URL: Source, https://github.com/vaemail/vaemail-python
10
+ Keywords: email,email-api,transactional-email,ai-agent,agent,deliverability,sdk,vaemail,eu
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ # vaemail — Python SDK
15
+
16
+ Email infrastructure for AI agents and applications. Send transactional email,
17
+ authenticate sending domains, track delivery, and diagnose deliverability.
18
+
19
+ No dependencies: the SDK uses only the standard library, so it installs without
20
+ pulling anything behind it.
21
+
22
+ ```bash
23
+ pip install vaemail
24
+ ```
25
+
26
+ ## Send an email
27
+
28
+ ```python
29
+ from vaemail import VaEmail
30
+
31
+ client = VaEmail(api_key="your-api-key")
32
+
33
+ client.send({
34
+ "to": "customer@example.com",
35
+ "subject": "Your order is on its way",
36
+ "html": "<p>Tracking number: 1Z999</p>",
37
+ }, idempotency_key="order-4711")
38
+ ```
39
+
40
+ The idempotency key makes the call safe to replay for 24 hours: retrying after a
41
+ timeout will not send a second email.
42
+
43
+ ## Try before sending
44
+
45
+ ```python
46
+ client.validate({"to": "customer@example.com", "subject": "Hi", "html": "<p>Hi</p>"})
47
+ ```
48
+
49
+ Same checks as a real send — key scope, quotas, sending domain, suppression
50
+ list — but nothing leaves. Useful when an agent builds a message on its own.
51
+
52
+ ## Authenticate a sending domain
53
+
54
+ ```python
55
+ result = client.add_domain("news.example.com", dkim_tokens=["aaa111", "bbb222", "ccc333"])
56
+
57
+ for record in result["data"]["dns_records"]:
58
+ if record["publishable"]:
59
+ print(record["type"], record["host"], "->", record["value"])
60
+ ```
61
+
62
+ The three DKIM tokens come from Amazon SES when the domain identity is created.
63
+ Every record says whether it is `publishable` as-is: a value that still depends
64
+ on something unknown is flagged instead of being handed over, so an agent never
65
+ publishes a record that would break the domain's authentication.
66
+
67
+ ## Errors say what to do next
68
+
69
+ ```python
70
+ from vaemail import VaEmailError
71
+
72
+ try:
73
+ client.send({"to": "customer@example.com"})
74
+ except VaEmailError as error:
75
+ print(error.code) # DOMAIN_NOT_VERIFIED
76
+ print(error.retryable) # False
77
+ print(error.for_agent()) # reason, corrective action, whether to retry
78
+ ```
79
+
80
+ ## Everything else
81
+
82
+ `capabilities()`, `health()`, `get_message()`, `list_messages()`,
83
+ `list_domains()`, `verify_domain()`, `dns_records()`,
84
+ `diagnose_deliverability()`, `list_suppressions()`, `usage()`, `audit_logs()`.
85
+
86
+ Full API reference: <https://vaemail.fr/docs/api>
87
+
88
+ ## Tests
89
+
90
+ ```bash
91
+ python -m unittest discover -s tests
92
+ ```
93
+
94
+ MIT licensed.
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_sdk.py
4
+ vaemail/__init__.py
5
+ vaemail.egg-info/PKG-INFO
6
+ vaemail.egg-info/SOURCES.txt
7
+ vaemail.egg-info/dependency_links.txt
8
+ vaemail.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ vaemail