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.
@@ -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,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
@@ -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