pyloops-so 0.1.2__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.
- pyloops_so-0.1.2/.github/workflows/publish.yml +74 -0
- pyloops_so-0.1.2/.gitignore +8 -0
- pyloops_so-0.1.2/CONRIBUTING.md +38 -0
- pyloops_so-0.1.2/LICENSE.md +21 -0
- pyloops_so-0.1.2/PKG-INFO +250 -0
- pyloops_so-0.1.2/README.md +221 -0
- pyloops_so-0.1.2/docs/API.md +225 -0
- pyloops_so-0.1.2/pyproject.toml +56 -0
- pyloops_so-0.1.2/scripts/release.sh +11 -0
- pyloops_so-0.1.2/src/loops_py/__init__.py +46 -0
- pyloops_so-0.1.2/src/loops_py/account/__init__.py +3 -0
- pyloops_so-0.1.2/src/loops_py/account/service.py +23 -0
- pyloops_so-0.1.2/src/loops_py/client.py +195 -0
- pyloops_so-0.1.2/src/loops_py/contacts/__init__.py +3 -0
- pyloops_so-0.1.2/src/loops_py/contacts/service.py +86 -0
- pyloops_so-0.1.2/src/loops_py/core.py +234 -0
- pyloops_so-0.1.2/src/loops_py/events/__init__.py +3 -0
- pyloops_so-0.1.2/src/loops_py/events/service.py +23 -0
- pyloops_so-0.1.2/src/loops_py/exceptions.py +24 -0
- pyloops_so-0.1.2/src/loops_py/mailing_lists/__init__.py +3 -0
- pyloops_so-0.1.2/src/loops_py/mailing_lists/service.py +19 -0
- pyloops_so-0.1.2/src/loops_py/models.py +155 -0
- pyloops_so-0.1.2/src/loops_py/py.typed +0 -0
- pyloops_so-0.1.2/src/loops_py/transactional/__init__.py +3 -0
- pyloops_so-0.1.2/src/loops_py/transactional/service.py +39 -0
- pyloops_so-0.1.2/src/loops_py/types.py +6 -0
- pyloops_so-0.1.2/tests/test_client.py +174 -0
- pyloops_so-0.1.2/uv.lock +399 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build Distributions
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: uv sync --extra dev
|
|
24
|
+
|
|
25
|
+
- name: Lint
|
|
26
|
+
run: uv run ruff check .
|
|
27
|
+
|
|
28
|
+
- name: Test
|
|
29
|
+
run: uv run pytest
|
|
30
|
+
|
|
31
|
+
- name: Build
|
|
32
|
+
run: uv build
|
|
33
|
+
|
|
34
|
+
- name: Upload distributions
|
|
35
|
+
uses: actions/upload-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
|
|
40
|
+
publish-testpypi:
|
|
41
|
+
name: Publish to TestPyPI
|
|
42
|
+
needs: build
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
environment: testpypi
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write
|
|
47
|
+
steps:
|
|
48
|
+
- name: Download distributions
|
|
49
|
+
uses: actions/download-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
- name: Publish package distributions to TestPyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
with:
|
|
57
|
+
repository-url: https://test.pypi.org/legacy/
|
|
58
|
+
|
|
59
|
+
publish-pypi:
|
|
60
|
+
name: Publish to PyPI
|
|
61
|
+
needs: publish-testpypi
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
environment: pypi
|
|
64
|
+
permissions:
|
|
65
|
+
id-token: write
|
|
66
|
+
steps:
|
|
67
|
+
- name: Download distributions
|
|
68
|
+
uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
- name: Publish package distributions to PyPI
|
|
74
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for contributing to `loops-py`.
|
|
4
|
+
|
|
5
|
+
## Development setup
|
|
6
|
+
|
|
7
|
+
1. Install dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync --extra dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Run checks:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run ruff check .
|
|
17
|
+
uv run pytest
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Pull request guidelines
|
|
21
|
+
|
|
22
|
+
1. Keep changes scoped and focused.
|
|
23
|
+
2. Add or update tests for behavior changes.
|
|
24
|
+
3. Update docs when public behavior or API changes.
|
|
25
|
+
4. Ensure lint and tests pass before opening a PR.
|
|
26
|
+
|
|
27
|
+
## Commit style
|
|
28
|
+
|
|
29
|
+
Use clear commit messages that describe what changed and why.
|
|
30
|
+
|
|
31
|
+
## Reporting issues
|
|
32
|
+
|
|
33
|
+
When opening an issue, include:
|
|
34
|
+
|
|
35
|
+
- What you expected
|
|
36
|
+
- What happened
|
|
37
|
+
- Reproduction steps
|
|
38
|
+
- Environment details (Python version, OS)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 loops-py contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyloops-so
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Lightweight Python SDK (unofficial) for the Loops API
|
|
5
|
+
Project-URL: Homepage, https://github.com/annjawn/loops-py
|
|
6
|
+
Project-URL: Repository, https://github.com/annjawn/loops-py
|
|
7
|
+
Project-URL: Issues, https://github.com/annjawn/loops-py/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/annjawn/loops-py#readme
|
|
9
|
+
Author: Loops Py Contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE.md
|
|
12
|
+
Keywords: api,email,loops,sdk
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Unofficial Loops.so Python Library (`pyloops-so`)
|
|
31
|
+
|
|
32
|
+
`pyloops-so` is a lightweight Python SDK for the [Loops API](https://loops.so/docs/api-reference), designed for production usage with minimal dependencies.
|
|
33
|
+
|
|
34
|
+
## Why this library
|
|
35
|
+
|
|
36
|
+
- Complete support for [Loops.so](https://loops.so) endpoints
|
|
37
|
+
- Typed request/response models via Pydantic
|
|
38
|
+
- Optional raw JSON mode when you want plain dictionaries
|
|
39
|
+
- Single runtime dependency (`pydantic`)
|
|
40
|
+
- Small, composable client structure (`contacts`, `events`, `transactional`, etc.)
|
|
41
|
+
|
|
42
|
+
## Loops API docs
|
|
43
|
+
|
|
44
|
+
Official Loops API reference: [https://loops.so/docs/api-reference](https://loops.so/docs/api-reference)
|
|
45
|
+
|
|
46
|
+
Endpoint docs covered by this SDK:
|
|
47
|
+
|
|
48
|
+
- [Create Contact](https://loops.so/docs/api-reference/create-contact)
|
|
49
|
+
- [Update Contact](https://loops.so/docs/api-reference/update-contact)
|
|
50
|
+
- [Find Contact](https://loops.so/docs/api-reference/find-contact)
|
|
51
|
+
- [Delete Contact](https://loops.so/docs/api-reference/delete-contact)
|
|
52
|
+
- [Create Contact Property](https://loops.so/docs/api-reference/create-contact-property)
|
|
53
|
+
- [List Contact Properties](https://loops.so/docs/api-reference/list-contact-properties)
|
|
54
|
+
- [List Mailing Lists](https://loops.so/docs/api-reference/list-mailing-lists)
|
|
55
|
+
- [Send Event](https://loops.so/docs/api-reference/send-event)
|
|
56
|
+
- [Send Transactional Email](https://loops.so/docs/api-reference/send-transactional-email)
|
|
57
|
+
- [List Transactional Emails](https://loops.so/docs/api-reference/list-transactional-emails)
|
|
58
|
+
- [API Key](https://loops.so/docs/api-reference/api-key)
|
|
59
|
+
- [Dedicated Sending IPs](https://loops.so/docs/api-reference/dedicated-sending-ips)
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
Install from PyPI:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv add pyloops-so
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For local development:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv sync --extra dev
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Authentication
|
|
76
|
+
|
|
77
|
+
Loops uses Bearer auth for all endpoints:
|
|
78
|
+
|
|
79
|
+
```http
|
|
80
|
+
Authorization: Bearer {api_key}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Create a client once and reuse it:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from loops_py import LoopsClient
|
|
87
|
+
|
|
88
|
+
client = LoopsClient(api_key="loops_api_key")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage model
|
|
92
|
+
|
|
93
|
+
The SDK supports two call styles:
|
|
94
|
+
|
|
95
|
+
1. Top-level convenience methods (`client.create_contact(...)`) for compatibility.
|
|
96
|
+
2. Grouped service methods (`client.contacts.create_contact(...)`) for clearer organization.
|
|
97
|
+
|
|
98
|
+
Both call styles use the same underlying implementation.
|
|
99
|
+
|
|
100
|
+
## Typed mode (default)
|
|
101
|
+
|
|
102
|
+
By default, responses are returned as Pydantic models.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from loops_py import ContactRequest, LoopsClient
|
|
106
|
+
|
|
107
|
+
client = LoopsClient(api_key="loops_api_key")
|
|
108
|
+
|
|
109
|
+
created = client.contacts.create_contact(
|
|
110
|
+
ContactRequest(
|
|
111
|
+
email="ada@example.com",
|
|
112
|
+
first_name="Ada",
|
|
113
|
+
user_id="usr_123",
|
|
114
|
+
mailing_lists={"cll2pyfrx0000mm080fwnwdg0": True},
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
print(created.success)
|
|
119
|
+
print(created.id)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## JSON mode
|
|
123
|
+
|
|
124
|
+
If you prefer raw dict/list responses, use `response_mode="json"`.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from loops_py import LoopsClient
|
|
128
|
+
|
|
129
|
+
client = LoopsClient(api_key="loops_api_key", response_mode="json")
|
|
130
|
+
raw = client.account.verify_api_key()
|
|
131
|
+
print(raw["teamName"])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
You can override per call:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
typed = client.account.verify_api_key(as_json=False)
|
|
138
|
+
raw = client.account.verify_api_key(as_json=True)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Error handling
|
|
142
|
+
|
|
143
|
+
HTTP errors from Loops raise `LoopsAPIError` with status code and parsed response payload.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from loops_py import LoopsAPIError
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
client.contacts.find_contact({"email": "missing@example.com"})
|
|
150
|
+
except LoopsAPIError as exc:
|
|
151
|
+
print(exc.status_code)
|
|
152
|
+
print(exc.response)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Rate limit handling and retries
|
|
156
|
+
|
|
157
|
+
Loops applies request rate limits (baseline 10 requests/second/team) and can return `429`.
|
|
158
|
+
This SDK retries `429` responses automatically with exponential backoff.
|
|
159
|
+
|
|
160
|
+
Default retry behavior:
|
|
161
|
+
|
|
162
|
+
- `max_retries=3` (up to 4 total attempts)
|
|
163
|
+
- `retry_backoff_base=0.25` seconds
|
|
164
|
+
- `retry_backoff_max=4.0` seconds
|
|
165
|
+
- `retry_jitter=0.1` (10% random jitter)
|
|
166
|
+
- `Retry-After` header is honored when present
|
|
167
|
+
|
|
168
|
+
Configure it:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from loops_py import LoopsClient
|
|
172
|
+
|
|
173
|
+
client = LoopsClient(
|
|
174
|
+
api_key="loops_api_key",
|
|
175
|
+
max_retries=5,
|
|
176
|
+
retry_backoff_base=0.2,
|
|
177
|
+
retry_backoff_max=6.0,
|
|
178
|
+
retry_jitter=0.2,
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Disable retries by setting `max_retries=0`.
|
|
183
|
+
|
|
184
|
+
## Endpoint mapping
|
|
185
|
+
|
|
186
|
+
- `contacts`
|
|
187
|
+
- `create_contact` -> `POST /contacts/create`
|
|
188
|
+
- `update_contact` -> `PUT /contacts/update`
|
|
189
|
+
- `find_contact` -> `GET /contacts/find`
|
|
190
|
+
- `delete_contact` -> `POST /contacts/delete`
|
|
191
|
+
- `create_contact_property` -> `POST /contacts/properties`
|
|
192
|
+
- `list_contact_properties` -> `GET /contacts/properties`
|
|
193
|
+
- `mailing_lists`
|
|
194
|
+
- `list_mailing_lists` -> `GET /mailing-lists`
|
|
195
|
+
- `events`
|
|
196
|
+
- `send_event` -> `POST /events/send`
|
|
197
|
+
- `transactional`
|
|
198
|
+
- `send_transactional_email` -> `POST /transactional`
|
|
199
|
+
- `list_transactional_emails` -> `GET /transactional`
|
|
200
|
+
- `account`
|
|
201
|
+
- `verify_api_key` -> `GET /api-key`
|
|
202
|
+
- `list_dedicated_sending_ips` -> `GET /dedicated-sending-ips`
|
|
203
|
+
|
|
204
|
+
## Idempotency support
|
|
205
|
+
|
|
206
|
+
For endpoints that support idempotency, pass `idempotency_key`:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
client.events.send_event(
|
|
210
|
+
{"email": "user@example.com", "eventName": "signup"},
|
|
211
|
+
idempotency_key="signup-user@example.com-2026-02-28",
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Build and publish
|
|
216
|
+
|
|
217
|
+
Build sdist + wheel:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
uv build
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Artifacts:
|
|
224
|
+
|
|
225
|
+
- `dist/*.tar.gz`
|
|
226
|
+
- `dist/*.whl`
|
|
227
|
+
|
|
228
|
+
Publish (requires PyPI token):
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
export UV_PUBLISH_TOKEN="pypi-..."
|
|
232
|
+
uv publish
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
TestPyPI:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
uv run ruff check .
|
|
245
|
+
uv run pytest
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Unofficial Loops.so Python Library (`pyloops-so`)
|
|
2
|
+
|
|
3
|
+
`pyloops-so` is a lightweight Python SDK for the [Loops API](https://loops.so/docs/api-reference), designed for production usage with minimal dependencies.
|
|
4
|
+
|
|
5
|
+
## Why this library
|
|
6
|
+
|
|
7
|
+
- Complete support for [Loops.so](https://loops.so) endpoints
|
|
8
|
+
- Typed request/response models via Pydantic
|
|
9
|
+
- Optional raw JSON mode when you want plain dictionaries
|
|
10
|
+
- Single runtime dependency (`pydantic`)
|
|
11
|
+
- Small, composable client structure (`contacts`, `events`, `transactional`, etc.)
|
|
12
|
+
|
|
13
|
+
## Loops API docs
|
|
14
|
+
|
|
15
|
+
Official Loops API reference: [https://loops.so/docs/api-reference](https://loops.so/docs/api-reference)
|
|
16
|
+
|
|
17
|
+
Endpoint docs covered by this SDK:
|
|
18
|
+
|
|
19
|
+
- [Create Contact](https://loops.so/docs/api-reference/create-contact)
|
|
20
|
+
- [Update Contact](https://loops.so/docs/api-reference/update-contact)
|
|
21
|
+
- [Find Contact](https://loops.so/docs/api-reference/find-contact)
|
|
22
|
+
- [Delete Contact](https://loops.so/docs/api-reference/delete-contact)
|
|
23
|
+
- [Create Contact Property](https://loops.so/docs/api-reference/create-contact-property)
|
|
24
|
+
- [List Contact Properties](https://loops.so/docs/api-reference/list-contact-properties)
|
|
25
|
+
- [List Mailing Lists](https://loops.so/docs/api-reference/list-mailing-lists)
|
|
26
|
+
- [Send Event](https://loops.so/docs/api-reference/send-event)
|
|
27
|
+
- [Send Transactional Email](https://loops.so/docs/api-reference/send-transactional-email)
|
|
28
|
+
- [List Transactional Emails](https://loops.so/docs/api-reference/list-transactional-emails)
|
|
29
|
+
- [API Key](https://loops.so/docs/api-reference/api-key)
|
|
30
|
+
- [Dedicated Sending IPs](https://loops.so/docs/api-reference/dedicated-sending-ips)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install from PyPI:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv add pyloops-so
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For local development:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv sync --extra dev
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Authentication
|
|
47
|
+
|
|
48
|
+
Loops uses Bearer auth for all endpoints:
|
|
49
|
+
|
|
50
|
+
```http
|
|
51
|
+
Authorization: Bearer {api_key}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Create a client once and reuse it:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from loops_py import LoopsClient
|
|
58
|
+
|
|
59
|
+
client = LoopsClient(api_key="loops_api_key")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage model
|
|
63
|
+
|
|
64
|
+
The SDK supports two call styles:
|
|
65
|
+
|
|
66
|
+
1. Top-level convenience methods (`client.create_contact(...)`) for compatibility.
|
|
67
|
+
2. Grouped service methods (`client.contacts.create_contact(...)`) for clearer organization.
|
|
68
|
+
|
|
69
|
+
Both call styles use the same underlying implementation.
|
|
70
|
+
|
|
71
|
+
## Typed mode (default)
|
|
72
|
+
|
|
73
|
+
By default, responses are returned as Pydantic models.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from loops_py import ContactRequest, LoopsClient
|
|
77
|
+
|
|
78
|
+
client = LoopsClient(api_key="loops_api_key")
|
|
79
|
+
|
|
80
|
+
created = client.contacts.create_contact(
|
|
81
|
+
ContactRequest(
|
|
82
|
+
email="ada@example.com",
|
|
83
|
+
first_name="Ada",
|
|
84
|
+
user_id="usr_123",
|
|
85
|
+
mailing_lists={"cll2pyfrx0000mm080fwnwdg0": True},
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(created.success)
|
|
90
|
+
print(created.id)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## JSON mode
|
|
94
|
+
|
|
95
|
+
If you prefer raw dict/list responses, use `response_mode="json"`.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from loops_py import LoopsClient
|
|
99
|
+
|
|
100
|
+
client = LoopsClient(api_key="loops_api_key", response_mode="json")
|
|
101
|
+
raw = client.account.verify_api_key()
|
|
102
|
+
print(raw["teamName"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
You can override per call:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
typed = client.account.verify_api_key(as_json=False)
|
|
109
|
+
raw = client.account.verify_api_key(as_json=True)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Error handling
|
|
113
|
+
|
|
114
|
+
HTTP errors from Loops raise `LoopsAPIError` with status code and parsed response payload.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from loops_py import LoopsAPIError
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
client.contacts.find_contact({"email": "missing@example.com"})
|
|
121
|
+
except LoopsAPIError as exc:
|
|
122
|
+
print(exc.status_code)
|
|
123
|
+
print(exc.response)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Rate limit handling and retries
|
|
127
|
+
|
|
128
|
+
Loops applies request rate limits (baseline 10 requests/second/team) and can return `429`.
|
|
129
|
+
This SDK retries `429` responses automatically with exponential backoff.
|
|
130
|
+
|
|
131
|
+
Default retry behavior:
|
|
132
|
+
|
|
133
|
+
- `max_retries=3` (up to 4 total attempts)
|
|
134
|
+
- `retry_backoff_base=0.25` seconds
|
|
135
|
+
- `retry_backoff_max=4.0` seconds
|
|
136
|
+
- `retry_jitter=0.1` (10% random jitter)
|
|
137
|
+
- `Retry-After` header is honored when present
|
|
138
|
+
|
|
139
|
+
Configure it:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from loops_py import LoopsClient
|
|
143
|
+
|
|
144
|
+
client = LoopsClient(
|
|
145
|
+
api_key="loops_api_key",
|
|
146
|
+
max_retries=5,
|
|
147
|
+
retry_backoff_base=0.2,
|
|
148
|
+
retry_backoff_max=6.0,
|
|
149
|
+
retry_jitter=0.2,
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Disable retries by setting `max_retries=0`.
|
|
154
|
+
|
|
155
|
+
## Endpoint mapping
|
|
156
|
+
|
|
157
|
+
- `contacts`
|
|
158
|
+
- `create_contact` -> `POST /contacts/create`
|
|
159
|
+
- `update_contact` -> `PUT /contacts/update`
|
|
160
|
+
- `find_contact` -> `GET /contacts/find`
|
|
161
|
+
- `delete_contact` -> `POST /contacts/delete`
|
|
162
|
+
- `create_contact_property` -> `POST /contacts/properties`
|
|
163
|
+
- `list_contact_properties` -> `GET /contacts/properties`
|
|
164
|
+
- `mailing_lists`
|
|
165
|
+
- `list_mailing_lists` -> `GET /mailing-lists`
|
|
166
|
+
- `events`
|
|
167
|
+
- `send_event` -> `POST /events/send`
|
|
168
|
+
- `transactional`
|
|
169
|
+
- `send_transactional_email` -> `POST /transactional`
|
|
170
|
+
- `list_transactional_emails` -> `GET /transactional`
|
|
171
|
+
- `account`
|
|
172
|
+
- `verify_api_key` -> `GET /api-key`
|
|
173
|
+
- `list_dedicated_sending_ips` -> `GET /dedicated-sending-ips`
|
|
174
|
+
|
|
175
|
+
## Idempotency support
|
|
176
|
+
|
|
177
|
+
For endpoints that support idempotency, pass `idempotency_key`:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
client.events.send_event(
|
|
181
|
+
{"email": "user@example.com", "eventName": "signup"},
|
|
182
|
+
idempotency_key="signup-user@example.com-2026-02-28",
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Build and publish
|
|
187
|
+
|
|
188
|
+
Build sdist + wheel:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv build
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Artifacts:
|
|
195
|
+
|
|
196
|
+
- `dist/*.tar.gz`
|
|
197
|
+
- `dist/*.whl`
|
|
198
|
+
|
|
199
|
+
Publish (requires PyPI token):
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
export UV_PUBLISH_TOKEN="pypi-..."
|
|
203
|
+
uv publish
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
TestPyPI:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
uv run ruff check .
|
|
216
|
+
uv run pytest
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|