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 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__"]
@@ -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: ...
@@ -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()