sending-sdk 0.1.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.
- sending/__init__.py +9 -0
- sending/_async_client.py +60 -0
- sending/_client.py +69 -0
- sending/_errors.py +22 -0
- sending/_protocols.py +20 -0
- sending/async_client.py +89 -0
- sending/client.py +81 -0
- sending/models.py +1314 -0
- sending/py.typed +0 -0
- sending/resources/__init__.py +28 -0
- sending/resources/analytics.py +94 -0
- sending/resources/audience.py +127 -0
- sending/resources/automations.py +20 -0
- sending/resources/campaigns.py +32 -0
- sending/resources/domains.py +28 -0
- sending/resources/inboxes.py +68 -0
- sending/resources/messaging.py +43 -0
- sending/resources/webhooks.py +32 -0
- sending_sdk-0.1.0.dist-info/METADATA +132 -0
- sending_sdk-0.1.0.dist-info/RECORD +21 -0
- sending_sdk-0.1.0.dist-info/WHEEL +4 -0
sending/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""SDK Python ufficiale per l'API di Sending.dev."""
|
|
2
|
+
|
|
3
|
+
from ._client import DEFAULT_BASE_URL
|
|
4
|
+
from ._errors import SendingError
|
|
5
|
+
from .async_client import AsyncSending
|
|
6
|
+
from .client import Sending
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__all__ = ["Sending", "AsyncSending", "SendingError", "DEFAULT_BASE_URL", "__version__"]
|
sending/_async_client.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ._client import DEFAULT_BASE_URL, _to_jsonable
|
|
8
|
+
from ._errors import SendingError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncHttpClient:
|
|
12
|
+
"""Variante asincrona di HttpClient su httpx.AsyncClient."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
18
|
+
*,
|
|
19
|
+
timeout: float = 30.0,
|
|
20
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
if not api_key:
|
|
23
|
+
raise ValueError("Sending: api_key richiesta")
|
|
24
|
+
self._client = httpx.AsyncClient(
|
|
25
|
+
base_url=base_url.rstrip("/"),
|
|
26
|
+
headers={
|
|
27
|
+
"Authorization": f"Bearer {api_key}",
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
},
|
|
30
|
+
timeout=timeout,
|
|
31
|
+
transport=transport,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async def request(
|
|
35
|
+
self,
|
|
36
|
+
method: str,
|
|
37
|
+
path: str,
|
|
38
|
+
*,
|
|
39
|
+
query: Optional[Mapping[str, Any]] = None,
|
|
40
|
+
body: Any = None,
|
|
41
|
+
) -> Any:
|
|
42
|
+
params = (
|
|
43
|
+
{k: v for k, v in query.items() if v is not None} if query else None
|
|
44
|
+
)
|
|
45
|
+
json_body = _to_jsonable(body) if body is not None else None
|
|
46
|
+
|
|
47
|
+
res = await self._client.request(method, path, params=params, json=json_body)
|
|
48
|
+
|
|
49
|
+
content_type = res.headers.get("content-type", "")
|
|
50
|
+
if "application/json" in content_type and res.content:
|
|
51
|
+
data: Any = res.json()
|
|
52
|
+
else:
|
|
53
|
+
data = res.text or None
|
|
54
|
+
|
|
55
|
+
if res.is_error:
|
|
56
|
+
raise SendingError(res.status_code, data)
|
|
57
|
+
return data
|
|
58
|
+
|
|
59
|
+
async def aclose(self) -> None:
|
|
60
|
+
await self._client.aclose()
|
sending/_client.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ._errors import SendingError
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_URL = "https://sending.dev/api/v1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_jsonable(value: Any) -> Any:
|
|
14
|
+
"""Converte i modelli Pydantic in dict; lascia il resto invariato."""
|
|
15
|
+
if isinstance(value, BaseModel):
|
|
16
|
+
return value.model_dump(exclude_none=True, by_alias=True)
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpClient:
|
|
21
|
+
"""Client HTTP minimale: auth bearer, JSON, gestione errori."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: str,
|
|
26
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
27
|
+
*,
|
|
28
|
+
timeout: float = 30.0,
|
|
29
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
if not api_key:
|
|
32
|
+
raise ValueError("Sending: api_key richiesta")
|
|
33
|
+
self._client = httpx.Client(
|
|
34
|
+
base_url=base_url.rstrip("/"),
|
|
35
|
+
headers={
|
|
36
|
+
"Authorization": f"Bearer {api_key}",
|
|
37
|
+
"Accept": "application/json",
|
|
38
|
+
},
|
|
39
|
+
timeout=timeout,
|
|
40
|
+
transport=transport,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def request(
|
|
44
|
+
self,
|
|
45
|
+
method: str,
|
|
46
|
+
path: str,
|
|
47
|
+
*,
|
|
48
|
+
query: Optional[Mapping[str, Any]] = None,
|
|
49
|
+
body: Any = None,
|
|
50
|
+
) -> Any:
|
|
51
|
+
params = (
|
|
52
|
+
{k: v for k, v in query.items() if v is not None} if query else None
|
|
53
|
+
)
|
|
54
|
+
json_body = _to_jsonable(body) if body is not None else None
|
|
55
|
+
|
|
56
|
+
res = self._client.request(method, path, params=params, json=json_body)
|
|
57
|
+
|
|
58
|
+
content_type = res.headers.get("content-type", "")
|
|
59
|
+
if "application/json" in content_type and res.content:
|
|
60
|
+
data: Any = res.json()
|
|
61
|
+
else:
|
|
62
|
+
data = res.text or None
|
|
63
|
+
|
|
64
|
+
if res.is_error:
|
|
65
|
+
raise SendingError(res.status_code, data)
|
|
66
|
+
return data
|
|
67
|
+
|
|
68
|
+
def close(self) -> None:
|
|
69
|
+
self._client.close()
|
sending/_errors.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SendingError(Exception):
|
|
7
|
+
"""Errore sollevato quando l'API risponde con uno stato non 2xx."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, status: int, body: Any) -> None:
|
|
10
|
+
self.status = status
|
|
11
|
+
self.body = body
|
|
12
|
+
self.code = _extract_code(body)
|
|
13
|
+
suffix = f" ({self.code})" if self.code else ""
|
|
14
|
+
super().__init__(f"Sending API error {status}{suffix}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _extract_code(body: Any) -> Optional[str]:
|
|
18
|
+
if isinstance(body, dict):
|
|
19
|
+
err = body.get("error")
|
|
20
|
+
if isinstance(err, str):
|
|
21
|
+
return err
|
|
22
|
+
return None
|
sending/_protocols.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping, Optional, Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RequestClient(Protocol):
|
|
7
|
+
"""Interfaccia comune ai backend HTTP sync e async.
|
|
8
|
+
|
|
9
|
+
Il backend sync restituisce direttamente il valore; quello async restituisce
|
|
10
|
+
una coroutine da attendere. In entrambi i casi il tipo statico e' `Any`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def request(
|
|
14
|
+
self,
|
|
15
|
+
method: str,
|
|
16
|
+
path: str,
|
|
17
|
+
*,
|
|
18
|
+
query: Optional[Mapping[str, Any]] = None,
|
|
19
|
+
body: Any = None,
|
|
20
|
+
) -> Any: ...
|
sending/async_client.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ._async_client import AsyncHttpClient
|
|
8
|
+
from ._client import DEFAULT_BASE_URL
|
|
9
|
+
from .resources import (
|
|
10
|
+
Automations,
|
|
11
|
+
Campaigns,
|
|
12
|
+
Contacts,
|
|
13
|
+
CustomFields,
|
|
14
|
+
Domains,
|
|
15
|
+
EmailRules,
|
|
16
|
+
Emails,
|
|
17
|
+
Events,
|
|
18
|
+
Inboxes,
|
|
19
|
+
Integrations,
|
|
20
|
+
Lists,
|
|
21
|
+
Messages,
|
|
22
|
+
Segments,
|
|
23
|
+
Tags,
|
|
24
|
+
Usage,
|
|
25
|
+
Utm,
|
|
26
|
+
Webhooks,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AsyncSending:
|
|
31
|
+
"""Client asincrono dell'API di Sending.dev.
|
|
32
|
+
|
|
33
|
+
Le stesse classi risorsa del client sync: con il backend async ogni metodo
|
|
34
|
+
restituisce una coroutine da attendere.
|
|
35
|
+
|
|
36
|
+
Esempio::
|
|
37
|
+
|
|
38
|
+
import asyncio
|
|
39
|
+
from sending import AsyncSending
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
async with AsyncSending(api_key="sk_...") as client:
|
|
43
|
+
await client.emails.send({
|
|
44
|
+
"to": "a@b.com",
|
|
45
|
+
"from": "you@dominio.com",
|
|
46
|
+
"subject": "Ciao",
|
|
47
|
+
"html": "<p>Hi</p>",
|
|
48
|
+
"idempotencyKey": "welcome-001",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
api_key: str,
|
|
57
|
+
*,
|
|
58
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
59
|
+
timeout: float = 30.0,
|
|
60
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
self._http = AsyncHttpClient(api_key, base_url, timeout=timeout, transport=transport)
|
|
63
|
+
|
|
64
|
+
self.emails = Emails(self._http)
|
|
65
|
+
self.messages = Messages(self._http)
|
|
66
|
+
self.events = Events(self._http)
|
|
67
|
+
self.campaigns = Campaigns(self._http)
|
|
68
|
+
self.contacts = Contacts(self._http)
|
|
69
|
+
self.lists = Lists(self._http)
|
|
70
|
+
self.tags = Tags(self._http)
|
|
71
|
+
self.custom_fields = CustomFields(self._http)
|
|
72
|
+
self.segments = Segments(self._http)
|
|
73
|
+
self.domains = Domains(self._http)
|
|
74
|
+
self.inboxes = Inboxes(self._http)
|
|
75
|
+
self.email_rules = EmailRules(self._http)
|
|
76
|
+
self.automations = Automations(self._http)
|
|
77
|
+
self.webhooks = Webhooks(self._http)
|
|
78
|
+
self.utm = Utm(self._http)
|
|
79
|
+
self.integrations = Integrations(self._http)
|
|
80
|
+
self.usage = Usage(self._http)
|
|
81
|
+
|
|
82
|
+
async def aclose(self) -> None:
|
|
83
|
+
await self._http.aclose()
|
|
84
|
+
|
|
85
|
+
async def __aenter__(self) -> "AsyncSending":
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
async def __aexit__(self, *_exc: object) -> None:
|
|
89
|
+
await self.aclose()
|
sending/client.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ._client import DEFAULT_BASE_URL, HttpClient
|
|
8
|
+
from .resources import (
|
|
9
|
+
Automations,
|
|
10
|
+
Campaigns,
|
|
11
|
+
Contacts,
|
|
12
|
+
CustomFields,
|
|
13
|
+
Domains,
|
|
14
|
+
EmailRules,
|
|
15
|
+
Emails,
|
|
16
|
+
Events,
|
|
17
|
+
Inboxes,
|
|
18
|
+
Integrations,
|
|
19
|
+
Lists,
|
|
20
|
+
Messages,
|
|
21
|
+
Segments,
|
|
22
|
+
Tags,
|
|
23
|
+
Usage,
|
|
24
|
+
Utm,
|
|
25
|
+
Webhooks,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Sending:
|
|
30
|
+
"""Client ufficiale dell'API di Sending.dev.
|
|
31
|
+
|
|
32
|
+
Esempio::
|
|
33
|
+
|
|
34
|
+
from sending import Sending
|
|
35
|
+
|
|
36
|
+
client = Sending(api_key="sk_...")
|
|
37
|
+
client.emails.send({
|
|
38
|
+
"to": "a@b.com",
|
|
39
|
+
"from": "you@dominio.com",
|
|
40
|
+
"subject": "Ciao",
|
|
41
|
+
"html": "<p>Hi</p>",
|
|
42
|
+
"idempotencyKey": "welcome-001",
|
|
43
|
+
})
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str,
|
|
49
|
+
*,
|
|
50
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
51
|
+
timeout: float = 30.0,
|
|
52
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._http = HttpClient(api_key, base_url, timeout=timeout, transport=transport)
|
|
55
|
+
|
|
56
|
+
self.emails = Emails(self._http)
|
|
57
|
+
self.messages = Messages(self._http)
|
|
58
|
+
self.events = Events(self._http)
|
|
59
|
+
self.campaigns = Campaigns(self._http)
|
|
60
|
+
self.contacts = Contacts(self._http)
|
|
61
|
+
self.lists = Lists(self._http)
|
|
62
|
+
self.tags = Tags(self._http)
|
|
63
|
+
self.custom_fields = CustomFields(self._http)
|
|
64
|
+
self.segments = Segments(self._http)
|
|
65
|
+
self.domains = Domains(self._http)
|
|
66
|
+
self.inboxes = Inboxes(self._http)
|
|
67
|
+
self.email_rules = EmailRules(self._http)
|
|
68
|
+
self.automations = Automations(self._http)
|
|
69
|
+
self.webhooks = Webhooks(self._http)
|
|
70
|
+
self.utm = Utm(self._http)
|
|
71
|
+
self.integrations = Integrations(self._http)
|
|
72
|
+
self.usage = Usage(self._http)
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
self._http.close()
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> "Sending":
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *_exc: object) -> None:
|
|
81
|
+
self.close()
|