mailglyph 2.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.
- mailglyph/__init__.py +47 -0
- mailglyph/_version.py +1 -0
- mailglyph/client.py +77 -0
- mailglyph/exceptions.py +46 -0
- mailglyph/http_client.py +227 -0
- mailglyph/models.py +163 -0
- mailglyph/py.typed +0 -0
- mailglyph/resources/__init__.py +18 -0
- mailglyph/resources/_utils.py +13 -0
- mailglyph/resources/campaigns.py +251 -0
- mailglyph/resources/contacts.py +129 -0
- mailglyph/resources/emails.py +113 -0
- mailglyph/resources/events.py +73 -0
- mailglyph/resources/segments.py +222 -0
- mailglyph-2.0.0.dist-info/METADATA +218 -0
- mailglyph-2.0.0.dist-info/RECORD +18 -0
- mailglyph-2.0.0.dist-info/WHEEL +4 -0
- mailglyph-2.0.0.dist-info/licenses/LICENSE +21 -0
mailglyph/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .client import AsyncMailGlyph, MailGlyph
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
ApiError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
MailGlyphError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
)
|
|
10
|
+
from .models import (
|
|
11
|
+
Campaign,
|
|
12
|
+
Contact,
|
|
13
|
+
FilterCondition,
|
|
14
|
+
FilterGroup,
|
|
15
|
+
Segment,
|
|
16
|
+
SegmentFilter,
|
|
17
|
+
SendEmailResult,
|
|
18
|
+
StaticSegmentMembersAddResult,
|
|
19
|
+
StaticSegmentMembersRemoveResult,
|
|
20
|
+
TrackEventResult,
|
|
21
|
+
VerifyEmailResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "2.0.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"ApiError",
|
|
28
|
+
"AsyncMailGlyph",
|
|
29
|
+
"AuthenticationError",
|
|
30
|
+
"Campaign",
|
|
31
|
+
"Contact",
|
|
32
|
+
"FilterCondition",
|
|
33
|
+
"FilterGroup",
|
|
34
|
+
"MailGlyph",
|
|
35
|
+
"MailGlyphError",
|
|
36
|
+
"NotFoundError",
|
|
37
|
+
"RateLimitError",
|
|
38
|
+
"Segment",
|
|
39
|
+
"SegmentFilter",
|
|
40
|
+
"SendEmailResult",
|
|
41
|
+
"StaticSegmentMembersAddResult",
|
|
42
|
+
"StaticSegmentMembersRemoveResult",
|
|
43
|
+
"TrackEventResult",
|
|
44
|
+
"ValidationError",
|
|
45
|
+
"VerifyEmailResult",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
mailglyph/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
mailglyph/client.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .http_client import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, HttpClient
|
|
4
|
+
from .resources import (
|
|
5
|
+
AsyncCampaignsResource,
|
|
6
|
+
AsyncContactsResource,
|
|
7
|
+
AsyncEmailsResource,
|
|
8
|
+
AsyncEventsResource,
|
|
9
|
+
AsyncSegmentsResource,
|
|
10
|
+
CampaignsResource,
|
|
11
|
+
ContactsResource,
|
|
12
|
+
EmailsResource,
|
|
13
|
+
EventsResource,
|
|
14
|
+
SegmentsResource,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MailGlyph:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str,
|
|
22
|
+
*,
|
|
23
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
24
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
25
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._http_client = HttpClient(
|
|
28
|
+
api_key,
|
|
29
|
+
base_url=base_url,
|
|
30
|
+
timeout=timeout,
|
|
31
|
+
max_retries=max_retries,
|
|
32
|
+
)
|
|
33
|
+
self.emails = EmailsResource(self._http_client)
|
|
34
|
+
self.events = EventsResource(self._http_client)
|
|
35
|
+
self.contacts = ContactsResource(self._http_client)
|
|
36
|
+
self.campaigns = CampaignsResource(self._http_client)
|
|
37
|
+
self.segments = SegmentsResource(self._http_client)
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
self._http_client.close()
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> MailGlyph:
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def __exit__(self, exc_type: object, exc: object, exc_tb: object) -> None:
|
|
46
|
+
self.close()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AsyncMailGlyph:
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
api_key: str,
|
|
53
|
+
*,
|
|
54
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
55
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
56
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._http_client = HttpClient(
|
|
59
|
+
api_key,
|
|
60
|
+
base_url=base_url,
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
max_retries=max_retries,
|
|
63
|
+
)
|
|
64
|
+
self.emails = AsyncEmailsResource(self._http_client)
|
|
65
|
+
self.events = AsyncEventsResource(self._http_client)
|
|
66
|
+
self.contacts = AsyncContactsResource(self._http_client)
|
|
67
|
+
self.campaigns = AsyncCampaignsResource(self._http_client)
|
|
68
|
+
self.segments = AsyncSegmentsResource(self._http_client)
|
|
69
|
+
|
|
70
|
+
async def close(self) -> None:
|
|
71
|
+
await self._http_client.aclose()
|
|
72
|
+
|
|
73
|
+
async def __aenter__(self) -> AsyncMailGlyph:
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
async def __aexit__(self, exc_type: object, exc: object, exc_tb: object) -> None:
|
|
77
|
+
await self.close()
|
mailglyph/exceptions.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MailGlyphError(Exception):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
*,
|
|
11
|
+
status_code: int | None = None,
|
|
12
|
+
payload: Any = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.message = message
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.payload = payload
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthenticationError(MailGlyphError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationError(MailGlyphError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NotFoundError(MailGlyphError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RateLimitError(MailGlyphError):
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
message: str,
|
|
36
|
+
*,
|
|
37
|
+
status_code: int | None = None,
|
|
38
|
+
payload: Any = None,
|
|
39
|
+
retry_after: float | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__(message, status_code=status_code, payload=payload)
|
|
42
|
+
self.retry_after = retry_after
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ApiError(MailGlyphError):
|
|
46
|
+
pass
|
mailglyph/http_client.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._version import __version__
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
ApiError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
MailGlyphError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
DEFAULT_BASE_URL = "https://api.mailglyph.com"
|
|
21
|
+
DEFAULT_TIMEOUT = 30.0
|
|
22
|
+
DEFAULT_MAX_RETRIES = 3
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HttpClient:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key: str,
|
|
29
|
+
*,
|
|
30
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
31
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
32
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._api_key = api_key
|
|
35
|
+
self._base_url = base_url.rstrip("/")
|
|
36
|
+
self._timeout = timeout
|
|
37
|
+
self._max_retries = max_retries
|
|
38
|
+
self._key_type = self._detect_key_type(api_key)
|
|
39
|
+
headers = {
|
|
40
|
+
"Authorization": f"Bearer {api_key}",
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"User-Agent": f"mailglyph-python/{__version__}",
|
|
43
|
+
}
|
|
44
|
+
self._sync_client = httpx.Client(
|
|
45
|
+
base_url=self._base_url, timeout=self._timeout, headers=headers
|
|
46
|
+
)
|
|
47
|
+
self._async_client = httpx.AsyncClient(
|
|
48
|
+
base_url=self._base_url,
|
|
49
|
+
timeout=self._timeout,
|
|
50
|
+
headers=headers,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def key_type(self) -> str:
|
|
55
|
+
return self._key_type
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _detect_key_type(api_key: str) -> str:
|
|
59
|
+
if api_key.startswith("sk_"):
|
|
60
|
+
return "sk"
|
|
61
|
+
if api_key.startswith("pk_"):
|
|
62
|
+
return "pk"
|
|
63
|
+
raise AuthenticationError("Invalid API key format. Expected key prefix 'sk_' or 'pk_'.")
|
|
64
|
+
|
|
65
|
+
def _enforce_key_restrictions(self, path: str) -> None:
|
|
66
|
+
if self._key_type == "pk" and path != "/v1/track":
|
|
67
|
+
raise AuthenticationError("Public API keys (pk_*) can only be used with /v1/track.")
|
|
68
|
+
if self._key_type == "sk" and path == "/v1/track":
|
|
69
|
+
raise AuthenticationError("Secret API keys (sk_*) cannot be used with /v1/track.")
|
|
70
|
+
|
|
71
|
+
def request(
|
|
72
|
+
self,
|
|
73
|
+
method: str,
|
|
74
|
+
path: str,
|
|
75
|
+
*,
|
|
76
|
+
params: dict[str, Any] | None = None,
|
|
77
|
+
json_body: dict[str, Any] | None = None,
|
|
78
|
+
) -> Any:
|
|
79
|
+
self._enforce_key_restrictions(path)
|
|
80
|
+
return self._request_with_retries(method, path, params=params, json_body=json_body)
|
|
81
|
+
|
|
82
|
+
async def arequest(
|
|
83
|
+
self,
|
|
84
|
+
method: str,
|
|
85
|
+
path: str,
|
|
86
|
+
*,
|
|
87
|
+
params: dict[str, Any] | None = None,
|
|
88
|
+
json_body: dict[str, Any] | None = None,
|
|
89
|
+
) -> Any:
|
|
90
|
+
self._enforce_key_restrictions(path)
|
|
91
|
+
return await self._arequest_with_retries(method, path, params=params, json_body=json_body)
|
|
92
|
+
|
|
93
|
+
def _request_with_retries(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
path: str,
|
|
97
|
+
*,
|
|
98
|
+
params: dict[str, Any] | None,
|
|
99
|
+
json_body: dict[str, Any] | None,
|
|
100
|
+
) -> Any:
|
|
101
|
+
for attempt in range(self._max_retries + 1):
|
|
102
|
+
try:
|
|
103
|
+
response = self._sync_client.request(method, path, params=params, json=json_body)
|
|
104
|
+
except httpx.TransportError as exc:
|
|
105
|
+
if attempt >= self._max_retries:
|
|
106
|
+
raise ApiError(f"Request failed: {exc}") from exc
|
|
107
|
+
time.sleep(self._retry_delay(attempt, None))
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if self._should_retry(response.status_code) and attempt < self._max_retries:
|
|
111
|
+
time.sleep(self._retry_delay(attempt, response.headers.get("Retry-After")))
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
return self._parse_response(response)
|
|
115
|
+
|
|
116
|
+
raise ApiError("Request retries exhausted.")
|
|
117
|
+
|
|
118
|
+
async def _arequest_with_retries(
|
|
119
|
+
self,
|
|
120
|
+
method: str,
|
|
121
|
+
path: str,
|
|
122
|
+
*,
|
|
123
|
+
params: dict[str, Any] | None,
|
|
124
|
+
json_body: dict[str, Any] | None,
|
|
125
|
+
) -> Any:
|
|
126
|
+
for attempt in range(self._max_retries + 1):
|
|
127
|
+
try:
|
|
128
|
+
response = await self._async_client.request(
|
|
129
|
+
method, path, params=params, json=json_body
|
|
130
|
+
)
|
|
131
|
+
except httpx.TransportError as exc:
|
|
132
|
+
if attempt >= self._max_retries:
|
|
133
|
+
raise ApiError(f"Request failed: {exc}") from exc
|
|
134
|
+
await asyncio.sleep(self._retry_delay(attempt, None))
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
if self._should_retry(response.status_code) and attempt < self._max_retries:
|
|
138
|
+
await asyncio.sleep(self._retry_delay(attempt, response.headers.get("Retry-After")))
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
return self._parse_response(response)
|
|
142
|
+
|
|
143
|
+
raise ApiError("Request retries exhausted.")
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _should_retry(status_code: int) -> bool:
|
|
147
|
+
return status_code == 429 or 500 <= status_code <= 599
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _retry_delay(attempt: int, retry_after: str | None) -> float:
|
|
151
|
+
if retry_after is not None:
|
|
152
|
+
try:
|
|
153
|
+
return max(float(retry_after), 0.0)
|
|
154
|
+
except ValueError:
|
|
155
|
+
pass
|
|
156
|
+
base = float(2**attempt)
|
|
157
|
+
jitter = random.uniform(0.0, 0.2)
|
|
158
|
+
return base + jitter
|
|
159
|
+
|
|
160
|
+
def _parse_response(self, response: httpx.Response) -> Any:
|
|
161
|
+
if response.status_code >= 400:
|
|
162
|
+
self._raise_for_status(response)
|
|
163
|
+
|
|
164
|
+
if response.status_code == 204:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
if not response.content:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
return response.json()
|
|
172
|
+
except ValueError:
|
|
173
|
+
return response.text
|
|
174
|
+
|
|
175
|
+
def _raise_for_status(self, response: httpx.Response) -> None:
|
|
176
|
+
payload: Any
|
|
177
|
+
try:
|
|
178
|
+
payload = response.json()
|
|
179
|
+
except ValueError:
|
|
180
|
+
payload = None
|
|
181
|
+
|
|
182
|
+
message = self._extract_error_message(response.status_code, payload, response.text)
|
|
183
|
+
status_code = response.status_code
|
|
184
|
+
|
|
185
|
+
if status_code == 400:
|
|
186
|
+
raise ValidationError(message, status_code=status_code, payload=payload)
|
|
187
|
+
if status_code == 401:
|
|
188
|
+
raise AuthenticationError(message, status_code=status_code, payload=payload)
|
|
189
|
+
if status_code == 404:
|
|
190
|
+
raise NotFoundError(message, status_code=status_code, payload=payload)
|
|
191
|
+
if status_code == 429:
|
|
192
|
+
retry_after = response.headers.get("Retry-After")
|
|
193
|
+
retry_after_seconds: float | None = None
|
|
194
|
+
if retry_after is not None:
|
|
195
|
+
try:
|
|
196
|
+
retry_after_seconds = float(retry_after)
|
|
197
|
+
except ValueError:
|
|
198
|
+
retry_after_seconds = None
|
|
199
|
+
raise RateLimitError(
|
|
200
|
+
message,
|
|
201
|
+
status_code=status_code,
|
|
202
|
+
payload=payload,
|
|
203
|
+
retry_after=retry_after_seconds,
|
|
204
|
+
)
|
|
205
|
+
if 500 <= status_code <= 599:
|
|
206
|
+
raise ApiError(message, status_code=status_code, payload=payload)
|
|
207
|
+
|
|
208
|
+
raise MailGlyphError(message, status_code=status_code, payload=payload)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _extract_error_message(status_code: int, payload: Any, fallback: str) -> str:
|
|
212
|
+
if isinstance(payload, dict):
|
|
213
|
+
message = payload.get("message")
|
|
214
|
+
if isinstance(message, str) and message:
|
|
215
|
+
return message
|
|
216
|
+
error = payload.get("error")
|
|
217
|
+
if isinstance(error, str) and error:
|
|
218
|
+
return error
|
|
219
|
+
if fallback:
|
|
220
|
+
return fallback
|
|
221
|
+
return f"Request failed with status code {status_code}."
|
|
222
|
+
|
|
223
|
+
def close(self) -> None:
|
|
224
|
+
self._sync_client.close()
|
|
225
|
+
|
|
226
|
+
async def aclose(self) -> None:
|
|
227
|
+
await self._async_client.aclose()
|
mailglyph/models.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MailGlyphModel(BaseModel):
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContactRef(MailGlyphModel):
|
|
13
|
+
id: str
|
|
14
|
+
email: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SendEmailItem(MailGlyphModel):
|
|
18
|
+
contact: ContactRef
|
|
19
|
+
email: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SendEmailResult(MailGlyphModel):
|
|
23
|
+
emails: list[SendEmailItem] = Field(default_factory=list)
|
|
24
|
+
timestamp: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VerifyEmailResult(MailGlyphModel):
|
|
28
|
+
email: str
|
|
29
|
+
valid: bool
|
|
30
|
+
is_disposable: bool = Field(alias="isDisposable")
|
|
31
|
+
is_alias: bool = Field(alias="isAlias")
|
|
32
|
+
is_typo: bool = Field(alias="isTypo")
|
|
33
|
+
is_plus_addressed: bool = Field(alias="isPlusAddressed")
|
|
34
|
+
is_random_input: bool = Field(alias="isRandomInput")
|
|
35
|
+
is_personal_email: bool = Field(alias="isPersonalEmail")
|
|
36
|
+
domain_exists: bool = Field(alias="domainExists")
|
|
37
|
+
has_website: bool = Field(alias="hasWebsite")
|
|
38
|
+
has_mx_records: bool = Field(alias="hasMxRecords")
|
|
39
|
+
suggested_email: str | None = Field(default=None, alias="suggestedEmail")
|
|
40
|
+
reasons: list[str] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TrackEventResult(MailGlyphModel):
|
|
44
|
+
contact: str
|
|
45
|
+
event: str
|
|
46
|
+
timestamp: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ContactMeta(MailGlyphModel):
|
|
50
|
+
is_new: bool | None = Field(default=None, alias="isNew")
|
|
51
|
+
is_update: bool | None = Field(default=None, alias="isUpdate")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Contact(MailGlyphModel):
|
|
55
|
+
id: str
|
|
56
|
+
email: str
|
|
57
|
+
subscribed: bool
|
|
58
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
59
|
+
created_at: str | None = Field(default=None, alias="createdAt")
|
|
60
|
+
updated_at: str | None = Field(default=None, alias="updatedAt")
|
|
61
|
+
meta: ContactMeta | None = Field(default=None, alias="_meta")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Segment(MailGlyphModel):
|
|
65
|
+
id: str
|
|
66
|
+
name: str | None = None
|
|
67
|
+
description: str | None = None
|
|
68
|
+
condition: FilterCondition | None = None
|
|
69
|
+
track_membership: bool | None = Field(default=None, alias="trackMembership")
|
|
70
|
+
member_count: int | None = Field(default=None, alias="memberCount")
|
|
71
|
+
project_id: str | None = Field(default=None, alias="projectId")
|
|
72
|
+
created_at: str | None = Field(default=None, alias="createdAt")
|
|
73
|
+
updated_at: str | None = Field(default=None, alias="updatedAt")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SegmentFilter(MailGlyphModel):
|
|
77
|
+
field: str
|
|
78
|
+
operator: str
|
|
79
|
+
value: Any | None = None
|
|
80
|
+
unit: str | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FilterGroup(MailGlyphModel):
|
|
84
|
+
filters: list[SegmentFilter] = Field(default_factory=list)
|
|
85
|
+
conditions: FilterCondition | None = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FilterCondition(MailGlyphModel):
|
|
89
|
+
logic: str
|
|
90
|
+
groups: list[FilterGroup] = Field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Campaign(MailGlyphModel):
|
|
94
|
+
id: str
|
|
95
|
+
name: str | None = None
|
|
96
|
+
description: str | None = None
|
|
97
|
+
subject: str | None = None
|
|
98
|
+
body: str | None = None
|
|
99
|
+
from_email: str | None = Field(default=None, alias="from")
|
|
100
|
+
from_name: str | None = Field(default=None, alias="fromName")
|
|
101
|
+
reply_to: str | None = Field(default=None, alias="replyTo")
|
|
102
|
+
audience_type: str | None = Field(
|
|
103
|
+
default=None,
|
|
104
|
+
validation_alias=AliasChoices("audienceType", "type"),
|
|
105
|
+
serialization_alias="audienceType",
|
|
106
|
+
)
|
|
107
|
+
audience_condition: FilterCondition | None = Field(default=None, alias="audienceCondition")
|
|
108
|
+
segment_id: str | None = Field(default=None, alias="segmentId")
|
|
109
|
+
status: str | None = None
|
|
110
|
+
total_recipients: int | None = Field(default=None, alias="totalRecipients")
|
|
111
|
+
sent_count: int | None = Field(default=None, alias="sentCount")
|
|
112
|
+
delivered_count: int | None = Field(default=None, alias="deliveredCount")
|
|
113
|
+
opened_count: int | None = Field(default=None, alias="openedCount")
|
|
114
|
+
clicked_count: int | None = Field(default=None, alias="clickedCount")
|
|
115
|
+
bounced_count: int | None = Field(default=None, alias="bouncedCount")
|
|
116
|
+
scheduled_for: str | None = Field(
|
|
117
|
+
default=None,
|
|
118
|
+
validation_alias=AliasChoices("scheduledFor", "scheduledAt"),
|
|
119
|
+
serialization_alias="scheduledFor",
|
|
120
|
+
)
|
|
121
|
+
sent_at: str | None = Field(default=None, alias="sentAt")
|
|
122
|
+
created_at: str | None = Field(default=None, alias="createdAt")
|
|
123
|
+
updated_at: str | None = Field(default=None, alias="updatedAt")
|
|
124
|
+
segment: Segment | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ContactsPage(MailGlyphModel):
|
|
128
|
+
contacts: list[Contact] = Field(default_factory=list)
|
|
129
|
+
cursor: str | None = None
|
|
130
|
+
has_more: bool = Field(default=False, alias="hasMore")
|
|
131
|
+
total: int | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class CampaignsPage(MailGlyphModel):
|
|
135
|
+
data: list[Campaign] = Field(default_factory=list)
|
|
136
|
+
page: int | None = None
|
|
137
|
+
page_size: int | None = Field(default=None, alias="pageSize")
|
|
138
|
+
total: int | None = None
|
|
139
|
+
total_pages: int | None = Field(default=None, alias="totalPages")
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def campaigns(self) -> list[Campaign]:
|
|
143
|
+
return self.data
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SegmentContactsPage(MailGlyphModel):
|
|
147
|
+
data: list[Contact] = Field(default_factory=list)
|
|
148
|
+
total: int | None = None
|
|
149
|
+
page: int | None = None
|
|
150
|
+
page_size: int | None = Field(default=None, alias="pageSize")
|
|
151
|
+
total_pages: int | None = Field(default=None, alias="totalPages")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class StaticSegmentMembersAddResult(MailGlyphModel):
|
|
155
|
+
added: int
|
|
156
|
+
not_found: list[str] = Field(default_factory=list, alias="notFound")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class StaticSegmentMembersRemoveResult(MailGlyphModel):
|
|
160
|
+
removed: int
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
FilterCondition.model_rebuild()
|
mailglyph/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .campaigns import AsyncCampaignsResource, CampaignsResource
|
|
2
|
+
from .contacts import AsyncContactsResource, ContactsResource
|
|
3
|
+
from .emails import AsyncEmailsResource, EmailsResource
|
|
4
|
+
from .events import AsyncEventsResource, EventsResource
|
|
5
|
+
from .segments import AsyncSegmentsResource, SegmentsResource
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AsyncCampaignsResource",
|
|
9
|
+
"AsyncContactsResource",
|
|
10
|
+
"AsyncEmailsResource",
|
|
11
|
+
"AsyncEventsResource",
|
|
12
|
+
"AsyncSegmentsResource",
|
|
13
|
+
"CampaignsResource",
|
|
14
|
+
"ContactsResource",
|
|
15
|
+
"EmailsResource",
|
|
16
|
+
"EventsResource",
|
|
17
|
+
"SegmentsResource",
|
|
18
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compact_dict(payload: dict[str, Any]) -> dict[str, Any]:
|
|
7
|
+
return {key: value for key, value in payload.items() if value is not None}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def unwrap_data(payload: Any) -> Any:
|
|
11
|
+
if isinstance(payload, dict) and "data" in payload:
|
|
12
|
+
return payload["data"]
|
|
13
|
+
return payload
|