reloop-email 0.2.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.
- reloop_email/__init__.py +12 -0
- reloop_email/_http_client.py +74 -0
- reloop_email/_parameters.py +86 -0
- reloop_email/_resource.py +98 -0
- reloop_email/_resource_factory.py +127 -0
- reloop_email/client.py +6 -0
- reloop_email/errors.py +27 -0
- reloop_email/reloop.py +41 -0
- reloop_email/services/__init__.py +4 -0
- reloop_email/services/api_keys.py +62 -0
- reloop_email/services/contacts/__init__.py +133 -0
- reloop_email/services/contacts/channels.py +65 -0
- reloop_email/services/contacts/groups.py +38 -0
- reloop_email/services/domain.py +71 -0
- reloop_email-0.2.0.dist-info/METADATA +159 -0
- reloop_email-0.2.0.dist-info/RECORD +17 -0
- reloop_email-0.2.0.dist-info/WHEEL +4 -0
reloop_email/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .errors import ReloopApiError, ReloopNetworkError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HTTPClient:
|
|
11
|
+
"""Internal HTTP transport for the Reloop SDK."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, api_key: str, base_url: str = "https://reloop.sh") -> None:
|
|
14
|
+
if not api_key:
|
|
15
|
+
raise ValueError("Reloop SDK requires an api_key.")
|
|
16
|
+
|
|
17
|
+
self.api_key = api_key
|
|
18
|
+
self.base_url = base_url.rstrip("/")
|
|
19
|
+
self._http = httpx.Client(
|
|
20
|
+
base_url=self.base_url,
|
|
21
|
+
headers={
|
|
22
|
+
"x-api-key": self.api_key,
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"Accept": "application/json",
|
|
25
|
+
},
|
|
26
|
+
timeout=30.0,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def request(
|
|
30
|
+
self,
|
|
31
|
+
method: str,
|
|
32
|
+
path: str,
|
|
33
|
+
*,
|
|
34
|
+
params: Optional[dict[str, Any]] = None,
|
|
35
|
+
json: Optional[dict[str, Any]] = None,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
try:
|
|
38
|
+
response = self._http.request(method, path, params=params, json=json)
|
|
39
|
+
except httpx.RequestError as exc:
|
|
40
|
+
raise ReloopNetworkError(f"Reloop network error: {exc}") from exc
|
|
41
|
+
|
|
42
|
+
if response.status_code >= 400:
|
|
43
|
+
body: Any = None
|
|
44
|
+
try:
|
|
45
|
+
body = response.json()
|
|
46
|
+
except ValueError:
|
|
47
|
+
body = response.text
|
|
48
|
+
|
|
49
|
+
message = response.reason_phrase
|
|
50
|
+
if isinstance(body, dict) and body.get("message"):
|
|
51
|
+
message = str(body["message"])
|
|
52
|
+
|
|
53
|
+
raise ReloopApiError(
|
|
54
|
+
f"Reloop API error ({response.status_code}): {message}",
|
|
55
|
+
status_code=response.status_code,
|
|
56
|
+
body=body,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if response.status_code == 204 or not response.content:
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
data = response.json()
|
|
63
|
+
if not isinstance(data, dict):
|
|
64
|
+
return {"data": data}
|
|
65
|
+
return data
|
|
66
|
+
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
self._http.close()
|
|
69
|
+
|
|
70
|
+
def __enter__(self) -> "HTTPClient":
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(self, *args: object) -> None:
|
|
74
|
+
self.close()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
REQUEST_KEY_MAP = {
|
|
7
|
+
"first_name": "firstName",
|
|
8
|
+
"last_name": "lastName",
|
|
9
|
+
"group_ids": "groupIds",
|
|
10
|
+
"group_id": "groupId",
|
|
11
|
+
"fallback_value": "fallbackValue",
|
|
12
|
+
"default_subscription": "defaultSubscription",
|
|
13
|
+
"channel_id": "channelId",
|
|
14
|
+
"property_name": "propertyName",
|
|
15
|
+
"property_type": "propertyType",
|
|
16
|
+
"contact_id": "contactId",
|
|
17
|
+
"rate_limit_enabled": "rateLimitEnabled",
|
|
18
|
+
"user_id": "userId",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def for_request(parameters: dict[str, Any]) -> dict[str, Any]:
|
|
23
|
+
normalized: dict[str, Any] = {}
|
|
24
|
+
|
|
25
|
+
for key, value in parameters.items():
|
|
26
|
+
if key == "unsubscribed":
|
|
27
|
+
if "status" not in parameters:
|
|
28
|
+
normalized["status"] = "unsubscribed" if value else "subscribed"
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
api_key = REQUEST_KEY_MAP.get(key, _to_camel_case(key))
|
|
32
|
+
normalized[api_key] = _normalize_value(value, is_request=True)
|
|
33
|
+
|
|
34
|
+
return {key: value for key, value in normalized.items() if value is not None}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def for_query(options: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
return for_request(options)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def for_snake_request(parameters: dict[str, Any]) -> dict[str, Any]:
|
|
42
|
+
"""Pass parameters through without camelCase conversion (domain API)."""
|
|
43
|
+
return {key: value for key, value in parameters.items() if value is not None}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def for_response(data: dict[str, Any]) -> dict[str, Any]:
|
|
47
|
+
normalized: dict[str, Any] = {}
|
|
48
|
+
|
|
49
|
+
for key, value in data.items():
|
|
50
|
+
normalized[_to_snake_case(key)] = _normalize_value(value, is_request=False)
|
|
51
|
+
|
|
52
|
+
return normalized
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _normalize_value(value: Any, *, is_request: bool) -> Any:
|
|
56
|
+
if not isinstance(value, dict):
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
if _is_list(value):
|
|
60
|
+
return [
|
|
61
|
+
_normalize_value(item, is_request=is_request) if isinstance(item, dict) else item
|
|
62
|
+
for item in value
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
return for_request(value) if is_request else for_response(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_list(value: dict[str, Any]) -> bool:
|
|
69
|
+
if not value:
|
|
70
|
+
return True
|
|
71
|
+
return list(value.keys()) == list(range(len(value)))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _to_camel_case(key: str) -> str:
|
|
75
|
+
if key in REQUEST_KEY_MAP:
|
|
76
|
+
return REQUEST_KEY_MAP[key]
|
|
77
|
+
if "_" not in key:
|
|
78
|
+
return key
|
|
79
|
+
parts = key.split("_")
|
|
80
|
+
return parts[0] + "".join(part.capitalize() for part in parts[1:])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _to_snake_case(key: str) -> str:
|
|
84
|
+
if "_" in key:
|
|
85
|
+
return key
|
|
86
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", key).lower()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Iterator, Mapping
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Resource(Mapping[str, Any]):
|
|
7
|
+
"""StripeObject-style resource with snake_case attribute access."""
|
|
8
|
+
|
|
9
|
+
_repr_attr = "id"
|
|
10
|
+
|
|
11
|
+
def __init__(self, **attributes: Any) -> None:
|
|
12
|
+
self._values = dict(attributes)
|
|
13
|
+
for key, value in attributes.items():
|
|
14
|
+
object.__setattr__(self, key, value)
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dict(cls, data: dict[str, Any]) -> Resource:
|
|
18
|
+
return cls(**data)
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, key: str) -> Any:
|
|
21
|
+
return self._values[key]
|
|
22
|
+
|
|
23
|
+
def __iter__(self) -> Iterator[str]:
|
|
24
|
+
return iter(self._values)
|
|
25
|
+
|
|
26
|
+
def __len__(self) -> int:
|
|
27
|
+
return len(self._values)
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
ident = getattr(self, self._repr_attr, None)
|
|
31
|
+
class_name = self.__class__.__name__
|
|
32
|
+
if ident is not None:
|
|
33
|
+
return f"<{class_name} {self._repr_attr}={ident!r}>"
|
|
34
|
+
return f"<{class_name}>"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ApiKey(Resource):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ApiKeyList(Resource):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Contact(Resource):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ContactList(Resource):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ContactProperty(Resource):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PropertyList(Resource):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ContactGroup(Resource):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GroupList(Resource):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ContactChannel(Resource):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ChannelList(Resource):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DnsRecord(Resource):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Domain(Resource):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DomainList(Resource):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DomainStatus(Resource):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DomainNameservers(Resource):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ForwardDnsResponse(Resource):
|
|
98
|
+
pass
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
from ._parameters import for_response
|
|
6
|
+
from ._resource import (
|
|
7
|
+
ApiKey,
|
|
8
|
+
ApiKeyList,
|
|
9
|
+
ChannelList,
|
|
10
|
+
Contact,
|
|
11
|
+
ContactChannel,
|
|
12
|
+
ContactGroup,
|
|
13
|
+
ContactList,
|
|
14
|
+
ContactProperty,
|
|
15
|
+
DnsRecord,
|
|
16
|
+
Domain,
|
|
17
|
+
DomainList,
|
|
18
|
+
DomainNameservers,
|
|
19
|
+
DomainStatus,
|
|
20
|
+
ForwardDnsResponse,
|
|
21
|
+
GroupList,
|
|
22
|
+
PropertyList,
|
|
23
|
+
Resource,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T", bound=Resource)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build(resource_cls: type[T], data: dict[str, Any]) -> T:
|
|
30
|
+
return resource_cls.from_dict(for_response(data))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def api_key(data: dict[str, Any]) -> ApiKey:
|
|
34
|
+
return _build(ApiKey, data)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def api_key_list(data: dict[str, Any]) -> ApiKeyList:
|
|
38
|
+
normalized = for_response(data)
|
|
39
|
+
if isinstance(normalized.get("api_keys"), list):
|
|
40
|
+
normalized["api_keys"] = [api_key(item) for item in normalized["api_keys"]]
|
|
41
|
+
return ApiKeyList.from_dict(normalized)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def contact(data: dict[str, Any]) -> Contact:
|
|
45
|
+
return _build(Contact, data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def contact_list(data: dict[str, Any]) -> ContactList:
|
|
49
|
+
normalized = for_response(data)
|
|
50
|
+
if isinstance(normalized.get("contacts"), list):
|
|
51
|
+
normalized["contacts"] = [contact(item) for item in normalized["contacts"]]
|
|
52
|
+
return ContactList.from_dict(normalized)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def contact_property(data: dict[str, Any]) -> ContactProperty:
|
|
56
|
+
return _build(ContactProperty, data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def property_list(data: dict[str, Any]) -> PropertyList:
|
|
60
|
+
normalized = for_response(data)
|
|
61
|
+
if isinstance(normalized.get("properties"), list):
|
|
62
|
+
normalized["properties"] = [
|
|
63
|
+
contact_property(item) for item in normalized["properties"]
|
|
64
|
+
]
|
|
65
|
+
return PropertyList.from_dict(normalized)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def contact_group(data: dict[str, Any]) -> ContactGroup:
|
|
69
|
+
normalized = for_response(data)
|
|
70
|
+
if isinstance(normalized.get("contacts"), list):
|
|
71
|
+
normalized["contacts"] = [contact(item) for item in normalized["contacts"]]
|
|
72
|
+
return ContactGroup.from_dict(normalized)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def group_list(data: dict[str, Any]) -> GroupList:
|
|
76
|
+
normalized = for_response(data)
|
|
77
|
+
if isinstance(normalized.get("groups"), list):
|
|
78
|
+
normalized["groups"] = [contact_group(item) for item in normalized["groups"]]
|
|
79
|
+
return GroupList.from_dict(normalized)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def contact_channel(data: dict[str, Any]) -> ContactChannel:
|
|
83
|
+
normalized = for_response(data)
|
|
84
|
+
if isinstance(normalized.get("contact"), dict):
|
|
85
|
+
normalized["contact"] = contact(normalized["contact"])
|
|
86
|
+
return ContactChannel.from_dict(normalized)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def channel_list(data: dict[str, Any]) -> ChannelList:
|
|
90
|
+
normalized = for_response(data)
|
|
91
|
+
if isinstance(normalized.get("channels"), list):
|
|
92
|
+
normalized["channels"] = [
|
|
93
|
+
contact_channel(item) for item in normalized["channels"]
|
|
94
|
+
]
|
|
95
|
+
return ChannelList.from_dict(normalized)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def dns_record(data: dict[str, Any]) -> DnsRecord:
|
|
99
|
+
return _build(DnsRecord, data)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def domain(data: dict[str, Any]) -> Domain:
|
|
103
|
+
normalized = for_response(data)
|
|
104
|
+
if isinstance(normalized.get("dns_records"), list):
|
|
105
|
+
normalized["dns_records"] = [
|
|
106
|
+
dns_record(item) for item in normalized["dns_records"]
|
|
107
|
+
]
|
|
108
|
+
return Domain.from_dict(normalized)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def domain_list(data: dict[str, Any]) -> DomainList:
|
|
112
|
+
normalized = for_response(data)
|
|
113
|
+
if isinstance(normalized.get("domains"), list):
|
|
114
|
+
normalized["domains"] = [domain(item) for item in normalized["domains"]]
|
|
115
|
+
return DomainList.from_dict(normalized)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def domain_status(data: dict[str, Any]) -> DomainStatus:
|
|
119
|
+
return _build(DomainStatus, data)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def domain_nameservers(data: dict[str, Any]) -> DomainNameservers:
|
|
123
|
+
return _build(DomainNameservers, data)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def forward_dns_response(data: dict[str, Any]) -> ForwardDnsResponse:
|
|
127
|
+
return _build(ForwardDnsResponse, data)
|
reloop_email/client.py
ADDED
reloop_email/errors.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ReloopError(Exception):
|
|
5
|
+
"""Base exception for all Reloop SDK errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReloopApiError(ReloopError):
|
|
9
|
+
"""Raised when the Reloop API returns an error response."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status_code: Optional[int] = None,
|
|
16
|
+
body: Optional[Any] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.body = body
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str:
|
|
23
|
+
return f"ReloopApiError(message={self.args[0]!r}, status_code={self.status_code!r})"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReloopNetworkError(ReloopError):
|
|
27
|
+
"""Raised when a network error occurs while communicating with Reloop."""
|
reloop_email/reloop.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ._http_client import HTTPClient
|
|
6
|
+
from .services.api_keys import ApiKeysService
|
|
7
|
+
from .services.contacts import ContactsService
|
|
8
|
+
from .services.domain import DomainService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Reloop:
|
|
12
|
+
"""Reloop Python SDK client."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
*,
|
|
18
|
+
base_url: str = "https://reloop.sh",
|
|
19
|
+
) -> None:
|
|
20
|
+
self._http = HTTPClient(api_key, base_url)
|
|
21
|
+
self.api_keys = ApiKeysService(self._http)
|
|
22
|
+
self.contacts = ContactsService(self._http)
|
|
23
|
+
self.domain = DomainService(self._http)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def client(
|
|
27
|
+
cls,
|
|
28
|
+
api_key: str,
|
|
29
|
+
base_url: str = "https://reloop.sh",
|
|
30
|
+
) -> "Reloop":
|
|
31
|
+
"""Create a new Reloop client with the given API key."""
|
|
32
|
+
return cls(api_key, base_url=base_url)
|
|
33
|
+
|
|
34
|
+
def close(self) -> None:
|
|
35
|
+
self._http.close()
|
|
36
|
+
|
|
37
|
+
def __enter__(self) -> "Reloop":
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def __exit__(self, *args: object) -> None:
|
|
41
|
+
self.close()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .._http_client import HTTPClient
|
|
6
|
+
from .._parameters import for_query, for_request
|
|
7
|
+
from .._resource_factory import api_key, api_key_list
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiKeysService:
|
|
11
|
+
"""Manage API keys."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
14
|
+
self._client = client
|
|
15
|
+
|
|
16
|
+
def create(self, **parameters: Any) -> Any:
|
|
17
|
+
data = self._client.request(
|
|
18
|
+
"POST",
|
|
19
|
+
"/api/api-key/v1/",
|
|
20
|
+
json=for_request(parameters),
|
|
21
|
+
)
|
|
22
|
+
return api_key(data)
|
|
23
|
+
|
|
24
|
+
def list(self, **options: Any) -> Any:
|
|
25
|
+
data = self._client.request(
|
|
26
|
+
"GET",
|
|
27
|
+
"/api/api-key/v1/",
|
|
28
|
+
params=for_query(options) or None,
|
|
29
|
+
)
|
|
30
|
+
return api_key_list(data)
|
|
31
|
+
|
|
32
|
+
def get(self, api_key_id: str) -> Any:
|
|
33
|
+
data = self._client.request("GET", f"/api/api-key/v1/{api_key_id}")
|
|
34
|
+
return api_key(data)
|
|
35
|
+
|
|
36
|
+
def update(self, api_key_id: str, **parameters: Any) -> Any:
|
|
37
|
+
data = self._client.request(
|
|
38
|
+
"PATCH",
|
|
39
|
+
f"/api/api-key/v1/{api_key_id}",
|
|
40
|
+
json=for_request(parameters),
|
|
41
|
+
)
|
|
42
|
+
return api_key(data)
|
|
43
|
+
|
|
44
|
+
def delete(self, api_key_id: str) -> Any:
|
|
45
|
+
data = self._client.request("DELETE", f"/api/api-key/v1/{api_key_id}")
|
|
46
|
+
return api_key(data)
|
|
47
|
+
|
|
48
|
+
def rotate(self, api_key_id: str) -> Any:
|
|
49
|
+
data = self._client.request("POST", f"/api/api-key/v1/rotate/{api_key_id}")
|
|
50
|
+
return api_key(data)
|
|
51
|
+
|
|
52
|
+
def enable(self, api_key_id: str) -> Any:
|
|
53
|
+
data = self._client.request("POST", f"/api/api-key/v1/enable/{api_key_id}")
|
|
54
|
+
return api_key(data)
|
|
55
|
+
|
|
56
|
+
def disable(self, api_key_id: str) -> Any:
|
|
57
|
+
data = self._client.request("POST", f"/api/api-key/v1/disable/{api_key_id}")
|
|
58
|
+
return api_key(data)
|
|
59
|
+
|
|
60
|
+
def pause(self, api_key_id: str) -> Any:
|
|
61
|
+
"""Pause an API key (alias for :meth:`disable`)."""
|
|
62
|
+
return self.disable(api_key_id)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..._http_client import HTTPClient
|
|
6
|
+
from ..._parameters import for_query, for_request
|
|
7
|
+
from ..._resource_factory import (
|
|
8
|
+
contact,
|
|
9
|
+
contact_group,
|
|
10
|
+
contact_list,
|
|
11
|
+
contact_property,
|
|
12
|
+
group_list,
|
|
13
|
+
property_list,
|
|
14
|
+
)
|
|
15
|
+
from .channels import ContactChannelsService
|
|
16
|
+
from .groups import ContactGroupsService
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContactsService:
|
|
20
|
+
"""Manage contacts, properties, and groups."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
23
|
+
self._client = client
|
|
24
|
+
self.groups = ContactGroupsService(client)
|
|
25
|
+
self.channels = ContactChannelsService(client)
|
|
26
|
+
|
|
27
|
+
def create(self, **parameters: Any) -> Any:
|
|
28
|
+
data = self._client.request(
|
|
29
|
+
"POST",
|
|
30
|
+
"/api/contacts/create",
|
|
31
|
+
json=for_request(parameters),
|
|
32
|
+
)
|
|
33
|
+
return contact(data)
|
|
34
|
+
|
|
35
|
+
def get(self, contact_id: str) -> Any:
|
|
36
|
+
data = self._client.request("GET", f"/api/contacts/retrieve/{contact_id}")
|
|
37
|
+
return contact(data)
|
|
38
|
+
|
|
39
|
+
def list(self, **options: Any) -> Any:
|
|
40
|
+
group_id = options.get("group_id") or options.get("groupId")
|
|
41
|
+
|
|
42
|
+
if group_id:
|
|
43
|
+
filtered = {
|
|
44
|
+
key: value
|
|
45
|
+
for key, value in options.items()
|
|
46
|
+
if key not in ("group_id", "groupId")
|
|
47
|
+
}
|
|
48
|
+
return self.groups.list_contacts(group_id, **filtered)
|
|
49
|
+
|
|
50
|
+
data = self._client.request(
|
|
51
|
+
"GET",
|
|
52
|
+
"/api/contacts/list",
|
|
53
|
+
params=for_query(options) or None,
|
|
54
|
+
)
|
|
55
|
+
return contact_list(data)
|
|
56
|
+
|
|
57
|
+
def update(self, contact_id: str, **parameters: Any) -> Any:
|
|
58
|
+
data = self._client.request(
|
|
59
|
+
"PATCH",
|
|
60
|
+
f"/api/contacts/{contact_id}",
|
|
61
|
+
json=for_request(parameters),
|
|
62
|
+
)
|
|
63
|
+
return contact(data)
|
|
64
|
+
|
|
65
|
+
def delete(self, contact_id: str) -> Any:
|
|
66
|
+
data = self._client.request("DELETE", f"/api/contacts/{contact_id}")
|
|
67
|
+
return contact(data)
|
|
68
|
+
|
|
69
|
+
def create_property(self, **parameters: Any) -> Any:
|
|
70
|
+
data = self._client.request(
|
|
71
|
+
"POST",
|
|
72
|
+
"/api/contacts/v1/properties/create",
|
|
73
|
+
json=for_request(parameters),
|
|
74
|
+
)
|
|
75
|
+
return contact_property(data)
|
|
76
|
+
|
|
77
|
+
def list_properties(self, **options: Any) -> Any:
|
|
78
|
+
data = self._client.request(
|
|
79
|
+
"GET",
|
|
80
|
+
"/api/contacts/v1/properties/list",
|
|
81
|
+
params=for_query(options) or None,
|
|
82
|
+
)
|
|
83
|
+
return property_list(data)
|
|
84
|
+
|
|
85
|
+
def update_property(self, property_id: str, **parameters: Any) -> Any:
|
|
86
|
+
data = self._client.request(
|
|
87
|
+
"PATCH",
|
|
88
|
+
f"/api/contacts/v1/properties/{property_id}",
|
|
89
|
+
json=for_request(parameters),
|
|
90
|
+
)
|
|
91
|
+
return contact_property(data)
|
|
92
|
+
|
|
93
|
+
def delete_property(self, property_id: str) -> Any:
|
|
94
|
+
data = self._client.request(
|
|
95
|
+
"DELETE",
|
|
96
|
+
f"/api/contacts/v1/properties/{property_id}",
|
|
97
|
+
)
|
|
98
|
+
return contact_property(data)
|
|
99
|
+
|
|
100
|
+
def create_group(self, **parameters: Any) -> Any:
|
|
101
|
+
data = self._client.request(
|
|
102
|
+
"POST",
|
|
103
|
+
"/api/contacts/v1/groups/create",
|
|
104
|
+
json=for_request(parameters),
|
|
105
|
+
)
|
|
106
|
+
return contact_group(data)
|
|
107
|
+
|
|
108
|
+
def list_groups(self, **options: Any) -> Any:
|
|
109
|
+
data = self._client.request(
|
|
110
|
+
"GET",
|
|
111
|
+
"/api/contacts/v1/groups/list",
|
|
112
|
+
params=for_query(options) or None,
|
|
113
|
+
)
|
|
114
|
+
return group_list(data)
|
|
115
|
+
|
|
116
|
+
def get_group(self, group_id: str) -> Any:
|
|
117
|
+
data = self._client.request("GET", f"/api/contacts/v1/groups/{group_id}")
|
|
118
|
+
return contact_group(data)
|
|
119
|
+
|
|
120
|
+
def update_group(self, group_id: str, **parameters: Any) -> Any:
|
|
121
|
+
data = self._client.request(
|
|
122
|
+
"PATCH",
|
|
123
|
+
f"/api/contacts/v1/groups/{group_id}",
|
|
124
|
+
json=for_request(parameters),
|
|
125
|
+
)
|
|
126
|
+
return contact_group(data)
|
|
127
|
+
|
|
128
|
+
def delete_group(self, group_id: str) -> Any:
|
|
129
|
+
data = self._client.request(
|
|
130
|
+
"DELETE",
|
|
131
|
+
f"/api/contacts/v1/groups/{group_id}",
|
|
132
|
+
)
|
|
133
|
+
return contact_group(data)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..._http_client import HTTPClient
|
|
6
|
+
from ..._parameters import for_query, for_request
|
|
7
|
+
from ..._resource_factory import channel_list, contact_channel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContactChannelsService:
|
|
11
|
+
"""Manage contact channels."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
14
|
+
self._client = client
|
|
15
|
+
|
|
16
|
+
def create(self, **parameters: Any) -> Any:
|
|
17
|
+
data = self._client.request(
|
|
18
|
+
"POST",
|
|
19
|
+
"/api/contacts/v1/channels/create",
|
|
20
|
+
json=for_request(parameters),
|
|
21
|
+
)
|
|
22
|
+
return contact_channel(data)
|
|
23
|
+
|
|
24
|
+
def list(self, **options: Any) -> Any:
|
|
25
|
+
data = self._client.request(
|
|
26
|
+
"GET",
|
|
27
|
+
"/api/contacts/v1/channels/list",
|
|
28
|
+
params=for_query(options) or None,
|
|
29
|
+
)
|
|
30
|
+
return channel_list(data)
|
|
31
|
+
|
|
32
|
+
def get(self, channel_id: str) -> Any:
|
|
33
|
+
data = self._client.request("GET", f"/api/contacts/v1/channels/{channel_id}")
|
|
34
|
+
return contact_channel(data)
|
|
35
|
+
|
|
36
|
+
def update(self, channel_id: str, **parameters: Any) -> Any:
|
|
37
|
+
data = self._client.request(
|
|
38
|
+
"PATCH",
|
|
39
|
+
f"/api/contacts/v1/channels/{channel_id}",
|
|
40
|
+
json=for_request(parameters),
|
|
41
|
+
)
|
|
42
|
+
return contact_channel(data)
|
|
43
|
+
|
|
44
|
+
def delete(self, channel_id: str) -> Any:
|
|
45
|
+
data = self._client.request(
|
|
46
|
+
"DELETE",
|
|
47
|
+
f"/api/contacts/v1/channels/{channel_id}",
|
|
48
|
+
)
|
|
49
|
+
return contact_channel(data)
|
|
50
|
+
|
|
51
|
+
def add_contact(self, channel_id: str, **parameters: Any) -> Any:
|
|
52
|
+
data = self._client.request(
|
|
53
|
+
"POST",
|
|
54
|
+
f"/api/contacts/channel/{channel_id}",
|
|
55
|
+
json=for_request(parameters),
|
|
56
|
+
)
|
|
57
|
+
return contact_channel(data)
|
|
58
|
+
|
|
59
|
+
def update_subscription(self, channel_id: str, **parameters: Any) -> Any:
|
|
60
|
+
data = self._client.request(
|
|
61
|
+
"PATCH",
|
|
62
|
+
f"/api/contacts/channel/{channel_id}",
|
|
63
|
+
json=for_request(parameters),
|
|
64
|
+
)
|
|
65
|
+
return contact_channel(data)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..._http_client import HTTPClient
|
|
6
|
+
from ..._parameters import for_query, for_request
|
|
7
|
+
from ..._resource_factory import contact, contact_group
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContactGroupsService:
|
|
11
|
+
"""Manage contact group membership."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
14
|
+
self._client = client
|
|
15
|
+
|
|
16
|
+
def add_contact(self, group_id: str, **parameters: Any) -> Any:
|
|
17
|
+
data = self._client.request(
|
|
18
|
+
"POST",
|
|
19
|
+
f"/api/contacts/group/{group_id}",
|
|
20
|
+
json=for_request(parameters),
|
|
21
|
+
)
|
|
22
|
+
return contact(data)
|
|
23
|
+
|
|
24
|
+
def remove_contact(self, group_id: str, **parameters: Any) -> Any:
|
|
25
|
+
data = self._client.request(
|
|
26
|
+
"DELETE",
|
|
27
|
+
f"/api/contacts/group/{group_id}",
|
|
28
|
+
json=for_request(parameters),
|
|
29
|
+
)
|
|
30
|
+
return contact(data)
|
|
31
|
+
|
|
32
|
+
def list_contacts(self, group_id: str, **options: Any) -> Any:
|
|
33
|
+
data = self._client.request(
|
|
34
|
+
"GET",
|
|
35
|
+
f"/api/contacts/v1/groups/{group_id}/contacts",
|
|
36
|
+
params=for_query(options) or None,
|
|
37
|
+
)
|
|
38
|
+
return contact_group(data)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .._http_client import HTTPClient
|
|
6
|
+
from .._parameters import for_query, for_snake_request
|
|
7
|
+
from .._resource_factory import (
|
|
8
|
+
domain,
|
|
9
|
+
domain_list,
|
|
10
|
+
domain_nameservers,
|
|
11
|
+
domain_status,
|
|
12
|
+
forward_dns_response,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DomainService:
|
|
17
|
+
"""Manage sending and receiving domains."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def create(self, **parameters: Any) -> Any:
|
|
23
|
+
data = self._client.request(
|
|
24
|
+
"POST",
|
|
25
|
+
"/api/domain/v1/create",
|
|
26
|
+
json=for_snake_request(parameters),
|
|
27
|
+
)
|
|
28
|
+
return domain(data)
|
|
29
|
+
|
|
30
|
+
def list(self, **options: Any) -> Any:
|
|
31
|
+
data = self._client.request(
|
|
32
|
+
"GET",
|
|
33
|
+
"/api/domain/v1/list",
|
|
34
|
+
params=for_query(options) or None,
|
|
35
|
+
)
|
|
36
|
+
return domain_list(data)
|
|
37
|
+
|
|
38
|
+
def get(self, domain_id: str) -> Any:
|
|
39
|
+
data = self._client.request("GET", f"/api/domain/v1/{domain_id}")
|
|
40
|
+
return domain(data)
|
|
41
|
+
|
|
42
|
+
def get_nameservers(self, domain_id: str) -> Any:
|
|
43
|
+
data = self._client.request(
|
|
44
|
+
"GET",
|
|
45
|
+
f"/api/domain/v1/nameservers/{domain_id}",
|
|
46
|
+
)
|
|
47
|
+
return domain_nameservers(data)
|
|
48
|
+
|
|
49
|
+
def update(self, domain_id: str, **parameters: Any) -> Any:
|
|
50
|
+
data = self._client.request(
|
|
51
|
+
"PATCH",
|
|
52
|
+
f"/api/domain/v1/{domain_id}",
|
|
53
|
+
json=for_snake_request(parameters),
|
|
54
|
+
)
|
|
55
|
+
return domain(data)
|
|
56
|
+
|
|
57
|
+
def delete(self, domain_id: str) -> Any:
|
|
58
|
+
data = self._client.request("DELETE", f"/api/domain/v1/{domain_id}")
|
|
59
|
+
return domain(data)
|
|
60
|
+
|
|
61
|
+
def verify(self, domain_id: str) -> Any:
|
|
62
|
+
data = self._client.request("POST", f"/api/domain/v1/verify/{domain_id}")
|
|
63
|
+
return domain_status(data)
|
|
64
|
+
|
|
65
|
+
def forward_dns(self, domain_id: str, *, email: str) -> Any:
|
|
66
|
+
data = self._client.request(
|
|
67
|
+
"POST",
|
|
68
|
+
f"/api/domain/v1/verify/{domain_id}/forward-dns",
|
|
69
|
+
json={"email": email},
|
|
70
|
+
)
|
|
71
|
+
return forward_dns_response(data)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reloop-email
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Reloop Python SDK
|
|
5
|
+
Project-URL: Homepage, https://reloop.sh
|
|
6
|
+
Project-URL: Repository, https://github.com/reloop-labs/reloop-python
|
|
7
|
+
Author: Reloop Labs
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx>=0.24.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Reloop Python SDK
|
|
13
|
+
|
|
14
|
+
The official Python SDK for [Reloop](https://reloop.sh), modeled after the Stripe Python SDK with snake_case parameters and typed resource responses.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Python 3.9 or higher
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install reloop-email
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Getting Started
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from reloop_email import Reloop
|
|
30
|
+
|
|
31
|
+
reloop = Reloop(api_key="re_123456789")
|
|
32
|
+
# or
|
|
33
|
+
reloop = Reloop.client("re_123456789")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API Keys
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
reloop = Reloop(api_key="rl_123456789")
|
|
40
|
+
|
|
41
|
+
reloop.api_keys.list(page=1, limit=10)
|
|
42
|
+
|
|
43
|
+
reloop.api_keys.create(
|
|
44
|
+
name="Production key",
|
|
45
|
+
enabled=True,
|
|
46
|
+
rate_limit_enabled=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
reloop.api_keys.get("key_123456789")
|
|
50
|
+
reloop.api_keys.update("key_123456789", name="Renamed key")
|
|
51
|
+
reloop.api_keys.rotate("key_123456789")
|
|
52
|
+
reloop.api_keys.disable("key_123456789")
|
|
53
|
+
reloop.api_keys.pause("key_123456789")
|
|
54
|
+
reloop.api_keys.enable("key_123456789")
|
|
55
|
+
reloop.api_keys.delete("key_123456789")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Domains
|
|
59
|
+
|
|
60
|
+
Add, verify, and manage sending domains. Request parameters use snake_case; responses expose snake_case attributes.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
reloop = Reloop(api_key="rl_123456789")
|
|
64
|
+
|
|
65
|
+
created = reloop.domain.create(
|
|
66
|
+
domain="send.example.com",
|
|
67
|
+
custom_return_path="inbound",
|
|
68
|
+
click_tracking=True,
|
|
69
|
+
open_tracking=True,
|
|
70
|
+
tls="opportunistic",
|
|
71
|
+
sending_email=True,
|
|
72
|
+
receiving_email=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
domains = reloop.domain.list(page=1, limit=10, status="active")
|
|
76
|
+
one = reloop.domain.get("domain_123456789")
|
|
77
|
+
|
|
78
|
+
reloop.domain.update(
|
|
79
|
+
"domain_123456789",
|
|
80
|
+
click_tracking=False,
|
|
81
|
+
sending_email=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
reloop.domain.verify("domain_123456789")
|
|
85
|
+
|
|
86
|
+
reloop.domain.forward_dns("domain_123456789", email="admin@example.com")
|
|
87
|
+
|
|
88
|
+
nameservers = reloop.domain.get_nameservers("domain_123456789")
|
|
89
|
+
print(nameservers.dns_provider, nameservers.nameservers)
|
|
90
|
+
|
|
91
|
+
reloop.domain.delete("domain_123456789")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Contacts
|
|
95
|
+
|
|
96
|
+
Manage contacts, custom properties, groups, and channels. Methods accept snake_case keyword arguments and return resource objects with snake_case attributes.
|
|
97
|
+
|
|
98
|
+
### Create a contact
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
reloop = Reloop(api_key="re_123456789")
|
|
102
|
+
|
|
103
|
+
contact = reloop.contacts.create(
|
|
104
|
+
email="steve.wozniak@gmail.com",
|
|
105
|
+
first_name="Steve",
|
|
106
|
+
last_name="Wozniak",
|
|
107
|
+
unsubscribed=False,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
print(contact.email)
|
|
111
|
+
print(contact.first_name)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### List and update contacts
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
contacts = reloop.contacts.list(page=1, limit=10)
|
|
118
|
+
print(contacts.contacts, contacts.total)
|
|
119
|
+
|
|
120
|
+
reloop.contacts.update(
|
|
121
|
+
"cont_123456789",
|
|
122
|
+
first_name="Steve",
|
|
123
|
+
unsubscribed=False,
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Groups and channels
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
reloop.contacts.groups.add_contact(
|
|
131
|
+
"grp_123456789",
|
|
132
|
+
contact_id="cont_123456789",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
reloop.contacts.channels.create(
|
|
136
|
+
name="Product Updates",
|
|
137
|
+
default_subscription="opt_in",
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Error Handling
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from reloop_email import Reloop, ReloopApiError
|
|
145
|
+
|
|
146
|
+
reloop = Reloop(api_key="re_123456789")
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
reloop.contacts.get("cont_invalid")
|
|
150
|
+
except ReloopApiError as error:
|
|
151
|
+
print(error.status_code, error.body)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Context Manager
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
with Reloop(api_key="re_123456789") as reloop:
|
|
158
|
+
reloop.contacts.list(limit=10)
|
|
159
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
reloop_email/__init__.py,sha256=tWlolF6kqj_B6zklRDp4GzPzPwLGuqkBqvQgEO56UhQ,234
|
|
2
|
+
reloop_email/_http_client.py,sha256=Y5MnGVwWp3HV0sIyrg2d_-8lDu_XfA0CLqpG-o1qMuE,2115
|
|
3
|
+
reloop_email/_parameters.py,sha256=KubqE0x9rejq7o7KfFY4iDiS75cWrJ6enJ2CwBHLY_I,2504
|
|
4
|
+
reloop_email/_resource.py,sha256=yfOKfIZkpb0KUG72tpVyYMOJXmcHBFrh9EeeVU4mt-U,1619
|
|
5
|
+
reloop_email/_resource_factory.py,sha256=vLSrejn0ukyDHUofxS5XfxJdapquC2r_qPSRxapK5V4,3812
|
|
6
|
+
reloop_email/client.py,sha256=D_xa3t1CqmfM0SLFgNfWwdA9zw9fjKLEpJNiBN4YcTM,144
|
|
7
|
+
reloop_email/errors.py,sha256=ajAg90kW_qw1vLeB1ct4eCJzJYjUI8ikxPQ0DWWRvrI,720
|
|
8
|
+
reloop_email/reloop.py,sha256=jDPTjw0_IGb2zK0ZBAL0arw9AdeL-N1AgoInNruTmYE,1047
|
|
9
|
+
reloop_email/services/__init__.py,sha256=xikGDamIqwyPBClEpPxXppdvc7VgxUDvZ-KoiNrFGJI,118
|
|
10
|
+
reloop_email/services/api_keys.py,sha256=TEXIqfM72OkefRoPzB78WFJTci2A_l9375_msJg36Cs,1969
|
|
11
|
+
reloop_email/services/domain.py,sha256=YimwEU-YS8wVE0E75gxPLeEqOVvfZb5MrPjJ1vqHIOg,2094
|
|
12
|
+
reloop_email/services/contacts/__init__.py,sha256=pAsZ4-g5IBQeCtG89D5xbGBsTL0gplSmNPj4-WRFrek,4110
|
|
13
|
+
reloop_email/services/contacts/channels.py,sha256=sNR2ZXG0VkaEZnecG6CluVAkPBbzlRu4wuQo5jFKgBQ,2030
|
|
14
|
+
reloop_email/services/contacts/groups.py,sha256=g9ABqZbAaDo-qDRf_Ws81U7OyYNuXwvYwB9QzRwerZI,1150
|
|
15
|
+
reloop_email-0.2.0.dist-info/METADATA,sha256=pfN0RcXvSXZcvbtfQnClF4FdwTuNNfqe_8Y-nJalfYk,3396
|
|
16
|
+
reloop_email-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
17
|
+
reloop_email-0.2.0.dist-info/RECORD,,
|