reloop-email 0.2.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.
- reloop_email-0.2.0/.github/workflows/publish.yml +35 -0
- reloop_email-0.2.0/.gitignore +24 -0
- reloop_email-0.2.0/PKG-INFO +159 -0
- reloop_email-0.2.0/README.md +148 -0
- reloop_email-0.2.0/pyproject.toml +21 -0
- reloop_email-0.2.0/reloop_email/__init__.py +12 -0
- reloop_email-0.2.0/reloop_email/_http_client.py +74 -0
- reloop_email-0.2.0/reloop_email/_parameters.py +86 -0
- reloop_email-0.2.0/reloop_email/_resource.py +98 -0
- reloop_email-0.2.0/reloop_email/_resource_factory.py +127 -0
- reloop_email-0.2.0/reloop_email/client.py +6 -0
- reloop_email-0.2.0/reloop_email/errors.py +27 -0
- reloop_email-0.2.0/reloop_email/reloop.py +41 -0
- reloop_email-0.2.0/reloop_email/services/__init__.py +4 -0
- reloop_email-0.2.0/reloop_email/services/api_keys.py +62 -0
- reloop_email-0.2.0/reloop_email/services/contacts/__init__.py +133 -0
- reloop_email-0.2.0/reloop_email/services/contacts/channels.py +65 -0
- reloop_email-0.2.0/reloop_email/services/contacts/groups.py +38 -0
- reloop_email-0.2.0/reloop_email/services/domain.py +71 -0
- reloop_email-0.2.0/tests/test_api_keys.py +70 -0
- reloop_email-0.2.0/tests/test_domain.py +153 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
paths:
|
|
7
|
+
- 'reloop_email/**'
|
|
8
|
+
- 'pyproject.toml'
|
|
9
|
+
- '.github/workflows/publish.yml'
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
contents: read
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: '3.10'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install hatch
|
|
30
|
+
|
|
31
|
+
- name: Build package
|
|
32
|
+
run: hatch build
|
|
33
|
+
|
|
34
|
+
- name: Publish to PyPI
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python bytecode and envs
|
|
2
|
+
__pycache__/
|
|
3
|
+
tests/__pycache__/
|
|
4
|
+
reloop_email/__pycache__/
|
|
5
|
+
*.pyc
|
|
6
|
+
*.pyo
|
|
7
|
+
*.pyd
|
|
8
|
+
.Python
|
|
9
|
+
env/
|
|
10
|
+
venv/
|
|
11
|
+
.venv/
|
|
12
|
+
pip-log.txt
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.eggs/
|
|
15
|
+
build/
|
|
16
|
+
dist/
|
|
17
|
+
__pycache__
|
|
18
|
+
|
|
19
|
+
# IDE and OS
|
|
20
|
+
.DS_Store
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
__pycache__
|
|
24
|
+
*.log
|
|
@@ -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,148 @@
|
|
|
1
|
+
# Reloop Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python SDK for [Reloop](https://reloop.sh), modeled after the Stripe Python SDK with snake_case parameters and typed resource responses.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.9 or higher
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install reloop-email
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from reloop_email import Reloop
|
|
19
|
+
|
|
20
|
+
reloop = Reloop(api_key="re_123456789")
|
|
21
|
+
# or
|
|
22
|
+
reloop = Reloop.client("re_123456789")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API Keys
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
reloop = Reloop(api_key="rl_123456789")
|
|
29
|
+
|
|
30
|
+
reloop.api_keys.list(page=1, limit=10)
|
|
31
|
+
|
|
32
|
+
reloop.api_keys.create(
|
|
33
|
+
name="Production key",
|
|
34
|
+
enabled=True,
|
|
35
|
+
rate_limit_enabled=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
reloop.api_keys.get("key_123456789")
|
|
39
|
+
reloop.api_keys.update("key_123456789", name="Renamed key")
|
|
40
|
+
reloop.api_keys.rotate("key_123456789")
|
|
41
|
+
reloop.api_keys.disable("key_123456789")
|
|
42
|
+
reloop.api_keys.pause("key_123456789")
|
|
43
|
+
reloop.api_keys.enable("key_123456789")
|
|
44
|
+
reloop.api_keys.delete("key_123456789")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Domains
|
|
48
|
+
|
|
49
|
+
Add, verify, and manage sending domains. Request parameters use snake_case; responses expose snake_case attributes.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
reloop = Reloop(api_key="rl_123456789")
|
|
53
|
+
|
|
54
|
+
created = reloop.domain.create(
|
|
55
|
+
domain="send.example.com",
|
|
56
|
+
custom_return_path="inbound",
|
|
57
|
+
click_tracking=True,
|
|
58
|
+
open_tracking=True,
|
|
59
|
+
tls="opportunistic",
|
|
60
|
+
sending_email=True,
|
|
61
|
+
receiving_email=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
domains = reloop.domain.list(page=1, limit=10, status="active")
|
|
65
|
+
one = reloop.domain.get("domain_123456789")
|
|
66
|
+
|
|
67
|
+
reloop.domain.update(
|
|
68
|
+
"domain_123456789",
|
|
69
|
+
click_tracking=False,
|
|
70
|
+
sending_email=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
reloop.domain.verify("domain_123456789")
|
|
74
|
+
|
|
75
|
+
reloop.domain.forward_dns("domain_123456789", email="admin@example.com")
|
|
76
|
+
|
|
77
|
+
nameservers = reloop.domain.get_nameservers("domain_123456789")
|
|
78
|
+
print(nameservers.dns_provider, nameservers.nameservers)
|
|
79
|
+
|
|
80
|
+
reloop.domain.delete("domain_123456789")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Contacts
|
|
84
|
+
|
|
85
|
+
Manage contacts, custom properties, groups, and channels. Methods accept snake_case keyword arguments and return resource objects with snake_case attributes.
|
|
86
|
+
|
|
87
|
+
### Create a contact
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
reloop = Reloop(api_key="re_123456789")
|
|
91
|
+
|
|
92
|
+
contact = reloop.contacts.create(
|
|
93
|
+
email="steve.wozniak@gmail.com",
|
|
94
|
+
first_name="Steve",
|
|
95
|
+
last_name="Wozniak",
|
|
96
|
+
unsubscribed=False,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
print(contact.email)
|
|
100
|
+
print(contact.first_name)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### List and update contacts
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
contacts = reloop.contacts.list(page=1, limit=10)
|
|
107
|
+
print(contacts.contacts, contacts.total)
|
|
108
|
+
|
|
109
|
+
reloop.contacts.update(
|
|
110
|
+
"cont_123456789",
|
|
111
|
+
first_name="Steve",
|
|
112
|
+
unsubscribed=False,
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Groups and channels
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
reloop.contacts.groups.add_contact(
|
|
120
|
+
"grp_123456789",
|
|
121
|
+
contact_id="cont_123456789",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
reloop.contacts.channels.create(
|
|
125
|
+
name="Product Updates",
|
|
126
|
+
default_subscription="opt_in",
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Error Handling
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from reloop_email import Reloop, ReloopApiError
|
|
134
|
+
|
|
135
|
+
reloop = Reloop(api_key="re_123456789")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
reloop.contacts.get("cont_invalid")
|
|
139
|
+
except ReloopApiError as error:
|
|
140
|
+
print(error.status_code, error.body)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Context Manager
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
with Reloop(api_key="re_123456789") as reloop:
|
|
147
|
+
reloop.contacts.list(limit=10)
|
|
148
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reloop-email"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Reloop Python SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Reloop Labs" }]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.24.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://reloop.sh"
|
|
18
|
+
Repository = "https://github.com/reloop-labs/reloop-python"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["reloop_email"]
|
|
@@ -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
|