goomzo 0.1.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.
- goomzo-0.1.0/.gitignore +5 -0
- goomzo-0.1.0/LICENSE +21 -0
- goomzo-0.1.0/PKG-INFO +77 -0
- goomzo-0.1.0/README.md +59 -0
- goomzo-0.1.0/package.json +12 -0
- goomzo-0.1.0/pyproject.toml +65 -0
- goomzo-0.1.0/src/goomzo/__init__.py +26 -0
- goomzo-0.1.0/src/goomzo/_client.py +119 -0
- goomzo-0.1.0/src/goomzo/_errors.py +49 -0
- goomzo-0.1.0/src/goomzo/_pagination.py +77 -0
- goomzo-0.1.0/src/goomzo/_transport.py +229 -0
- goomzo-0.1.0/src/goomzo/_version.py +3 -0
- goomzo-0.1.0/src/goomzo/_webhooks.py +80 -0
- goomzo-0.1.0/src/goomzo/py.typed +0 -0
- goomzo-0.1.0/src/goomzo/resources/__init__.py +0 -0
- goomzo-0.1.0/src/goomzo/resources/broadcasts.py +95 -0
- goomzo-0.1.0/src/goomzo/resources/customers.py +283 -0
- goomzo-0.1.0/src/goomzo/resources/messages.py +143 -0
- goomzo-0.1.0/src/goomzo/resources/segments.py +117 -0
- goomzo-0.1.0/src/goomzo/resources/templates.py +123 -0
- goomzo-0.1.0/src/goomzo/resources/webhook_endpoints.py +72 -0
- goomzo-0.1.0/src/goomzo/types.py +407 -0
goomzo-0.1.0/.gitignore
ADDED
goomzo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Goomzo
|
|
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.
|
goomzo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: goomzo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Goomzo API for Python: messages, templates, customers and webhooks.
|
|
5
|
+
Project-URL: Homepage, https://goomzo.africa
|
|
6
|
+
Project-URL: Documentation, https://docs.goomzo.africa/docs/developers/python-sdk
|
|
7
|
+
Author-email: Goomzo <hello@goomzo.africa>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api,customers,goomzo,sms,tanzania,whatsapp
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: httpx<1,>=0.27
|
|
16
|
+
Requires-Dist: typing-extensions>=4.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# goomzo
|
|
20
|
+
|
|
21
|
+
The [Goomzo API](https://docs.goomzo.africa/docs/developers/python-sdk) for Python: send SMS and WhatsApp messages,
|
|
22
|
+
send saved templates, keep customers in step, and check webhooks. Sync and asyncio, fully typed.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install goomzo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from goomzo import Goomzo
|
|
30
|
+
|
|
31
|
+
goomzo = Goomzo() # reads GOOMZO_API_KEY
|
|
32
|
+
|
|
33
|
+
# One message.
|
|
34
|
+
goomzo.messages.send_one({'to': '+255712345678', 'channel': 'sms', 'content': {'text': 'Your order is ready.'}})
|
|
35
|
+
|
|
36
|
+
# A saved template. {{secret.code}} is delivered, but kept as ••••••.
|
|
37
|
+
goomzo.messages.send_one({'to': '+255712345678', 'template': 'auth_1', 'variables': {'code': '482913'}})
|
|
38
|
+
|
|
39
|
+
# Add or update a customer, with their segments and activity.
|
|
40
|
+
goomzo.customers.create(
|
|
41
|
+
[{'name': 'Amina Juma', 'phone': '+255712345678', 'segments': [{'name': 'Shoppers'}]}],
|
|
42
|
+
if_exists='update',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Every customer in a segment, a page at a time behind the scenes.
|
|
46
|
+
for customer in goomzo.segments.customers.list('seg_4hq2v9n1c8x0t6pk'):
|
|
47
|
+
print(customer['name'])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
With asyncio:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from goomzo import AsyncGoomzo
|
|
54
|
+
|
|
55
|
+
goomzo = AsyncGoomzo()
|
|
56
|
+
await goomzo.messages.send_one({'to': '+255712345678', 'template': 'auth_1', 'variables': {'code': '482913'}})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## What it does for you
|
|
60
|
+
|
|
61
|
+
- **Typed:** every request and answer is a typed dict, with the API's own keys.
|
|
62
|
+
- **A fixed version:** it pins the API version it was written for.
|
|
63
|
+
- **Safe retries:** network errors, 429 and 5xx are tried again, twice by default. Every POST carries an
|
|
64
|
+
`Idempotency-Key`, the same on each try, so nothing is done twice.
|
|
65
|
+
- **Lists page themselves:** loop over a list for every item.
|
|
66
|
+
- **Errors you can act on:** a `GoomzoError` has the `status`, the `code`, the `fields` that were wrong and the
|
|
67
|
+
`request_id`.
|
|
68
|
+
|
|
69
|
+
## Webhooks
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from goomzo import verify_webhook
|
|
73
|
+
|
|
74
|
+
event = verify_webhook(request.body, request.headers, os.environ['GOOMZO_WEBHOOK_SECRET'])
|
|
75
|
+
if event['type'] == 'message.delivered':
|
|
76
|
+
...
|
|
77
|
+
```
|
goomzo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# goomzo
|
|
2
|
+
|
|
3
|
+
The [Goomzo API](https://docs.goomzo.africa/docs/developers/python-sdk) for Python: send SMS and WhatsApp messages,
|
|
4
|
+
send saved templates, keep customers in step, and check webhooks. Sync and asyncio, fully typed.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install goomzo
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from goomzo import Goomzo
|
|
12
|
+
|
|
13
|
+
goomzo = Goomzo() # reads GOOMZO_API_KEY
|
|
14
|
+
|
|
15
|
+
# One message.
|
|
16
|
+
goomzo.messages.send_one({'to': '+255712345678', 'channel': 'sms', 'content': {'text': 'Your order is ready.'}})
|
|
17
|
+
|
|
18
|
+
# A saved template. {{secret.code}} is delivered, but kept as ••••••.
|
|
19
|
+
goomzo.messages.send_one({'to': '+255712345678', 'template': 'auth_1', 'variables': {'code': '482913'}})
|
|
20
|
+
|
|
21
|
+
# Add or update a customer, with their segments and activity.
|
|
22
|
+
goomzo.customers.create(
|
|
23
|
+
[{'name': 'Amina Juma', 'phone': '+255712345678', 'segments': [{'name': 'Shoppers'}]}],
|
|
24
|
+
if_exists='update',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Every customer in a segment, a page at a time behind the scenes.
|
|
28
|
+
for customer in goomzo.segments.customers.list('seg_4hq2v9n1c8x0t6pk'):
|
|
29
|
+
print(customer['name'])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With asyncio:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from goomzo import AsyncGoomzo
|
|
36
|
+
|
|
37
|
+
goomzo = AsyncGoomzo()
|
|
38
|
+
await goomzo.messages.send_one({'to': '+255712345678', 'template': 'auth_1', 'variables': {'code': '482913'}})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What it does for you
|
|
42
|
+
|
|
43
|
+
- **Typed:** every request and answer is a typed dict, with the API's own keys.
|
|
44
|
+
- **A fixed version:** it pins the API version it was written for.
|
|
45
|
+
- **Safe retries:** network errors, 429 and 5xx are tried again, twice by default. Every POST carries an
|
|
46
|
+
`Idempotency-Key`, the same on each try, so nothing is done twice.
|
|
47
|
+
- **Lists page themselves:** loop over a list for every item.
|
|
48
|
+
- **Errors you can act on:** a `GoomzoError` has the `status`, the `code`, the `fields` that were wrong and the
|
|
49
|
+
`request_id`.
|
|
50
|
+
|
|
51
|
+
## Webhooks
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from goomzo import verify_webhook
|
|
55
|
+
|
|
56
|
+
event = verify_webhook(request.body, request.headers, os.environ['GOOMZO_WEBHOOK_SECRET'])
|
|
57
|
+
if event['type'] == 'message.delivered':
|
|
58
|
+
...
|
|
59
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goomzo/sdk-python",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The goomzo Python package, run by turbo through uv.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"lint": "uv run --frozen ruff check && uv run --frozen ruff format --check",
|
|
8
|
+
"check-types": "uv run --frozen pyright && uv run --frozen ty check src tests",
|
|
9
|
+
"test": "uv run --frozen pytest -q",
|
|
10
|
+
"build": "uv build"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "goomzo"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "The Goomzo API for Python: messages, templates, customers and webhooks."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Goomzo", email = "hello@goomzo.africa" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["httpx>=0.27,<1", "typing-extensions>=4.10"]
|
|
11
|
+
keywords = ["goomzo", "sms", "whatsapp", "customers", "tanzania", "api"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Typing :: Typed",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://goomzo.africa"
|
|
20
|
+
Documentation = "https://docs.goomzo.africa/docs/developers/python-sdk"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8",
|
|
25
|
+
"pytest-asyncio>=0.24",
|
|
26
|
+
"pyright>=1.1.390",
|
|
27
|
+
"ruff>=0.8",
|
|
28
|
+
"pyyaml>=6",
|
|
29
|
+
"ty>=0.0.101",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling>=1.26"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.version]
|
|
37
|
+
path = "package.json"
|
|
38
|
+
pattern = '"version": "(?P<version>[^"]+)"'
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/goomzo"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
include = ["src/goomzo", "README.md", "LICENSE", "package.json"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 120
|
|
48
|
+
target-version = "py310"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.format]
|
|
54
|
+
quote-style = "single"
|
|
55
|
+
|
|
56
|
+
[tool.pyright]
|
|
57
|
+
include = ["src", "tests"]
|
|
58
|
+
pythonVersion = "3.10"
|
|
59
|
+
typeCheckingMode = "strict"
|
|
60
|
+
executionEnvironments = [{ root = "tests", reportPrivateUsage = false }]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
asyncio_mode = "auto"
|
|
65
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The Goomzo API for Python: messages, templates, customers and webhooks."""
|
|
2
|
+
|
|
3
|
+
from ._client import AsyncGoomzo, Goomzo
|
|
4
|
+
from ._errors import FieldProblem, GoomzoConnectionError, GoomzoError, WebhookVerificationError
|
|
5
|
+
from ._pagination import AsyncPage, AsyncPager, Page
|
|
6
|
+
from ._transport import API_VERSION, Media
|
|
7
|
+
from ._version import VERSION
|
|
8
|
+
from ._webhooks import verify_webhook
|
|
9
|
+
|
|
10
|
+
__version__ = VERSION
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'API_VERSION',
|
|
14
|
+
'AsyncGoomzo',
|
|
15
|
+
'AsyncPage',
|
|
16
|
+
'AsyncPager',
|
|
17
|
+
'FieldProblem',
|
|
18
|
+
'Goomzo',
|
|
19
|
+
'GoomzoConnectionError',
|
|
20
|
+
'GoomzoError',
|
|
21
|
+
'Media',
|
|
22
|
+
'Page',
|
|
23
|
+
'WebhookVerificationError',
|
|
24
|
+
'__version__',
|
|
25
|
+
'verify_webhook',
|
|
26
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from ._transport import AsyncTransport, SyncTransport, make_config
|
|
9
|
+
from .resources.broadcasts import AsyncBroadcasts, Broadcasts
|
|
10
|
+
from .resources.customers import ActivityRecorder, AsyncActivityRecorder, AsyncCustomers, Customers
|
|
11
|
+
from .resources.messages import AsyncMessages, Messages
|
|
12
|
+
from .resources.segments import AsyncSegments, Segments
|
|
13
|
+
from .resources.templates import AsyncTemplates, AsyncWhatsApp, Templates, WhatsApp
|
|
14
|
+
from .resources.webhook_endpoints import AsyncWebhookEndpoints, WebhookEndpoints
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Goomzo:
|
|
18
|
+
"""
|
|
19
|
+
The Goomzo API client. Make one and share it across your app.
|
|
20
|
+
|
|
21
|
+
from goomzo import Goomzo
|
|
22
|
+
|
|
23
|
+
goomzo = Goomzo() # reads GOOMZO_API_KEY
|
|
24
|
+
goomzo.messages.send_one({'to': '+255712345678', 'channel': 'sms', 'content': {'text': 'Your order is ready.'}})
|
|
25
|
+
|
|
26
|
+
`api_key` is your API key's secret, `gzk_…`: keep it on your server. A failed request is tried again
|
|
27
|
+
`max_retries` times after a network error, a 429 or a 5xx, and each try waits up to `timeout` seconds.
|
|
28
|
+
|
|
29
|
+
Docs: https://docs.goomzo.africa/docs/developers/python-sdk
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
base_url: str | None = None,
|
|
37
|
+
max_retries: int = 2,
|
|
38
|
+
timeout: float = 60.0,
|
|
39
|
+
http_client: httpx.Client | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._transport = SyncTransport(make_config(api_key, base_url, max_retries, timeout), http_client)
|
|
42
|
+
self.messages = Messages(self._transport)
|
|
43
|
+
"""Send SMS and WhatsApp messages, each to one person."""
|
|
44
|
+
self.broadcasts = Broadcasts(self._transport)
|
|
45
|
+
"""Send one message to many people."""
|
|
46
|
+
self.templates = Templates(self._transport)
|
|
47
|
+
"""Saved templates, sent by name."""
|
|
48
|
+
self.whatsapp = WhatsApp(self._transport)
|
|
49
|
+
"""The business's WhatsApp account and its templates."""
|
|
50
|
+
self.customers = Customers(self._transport)
|
|
51
|
+
"""Add and update customers."""
|
|
52
|
+
self.activity = ActivityRecorder(self._transport)
|
|
53
|
+
"""Record customer activity, by phone, email or id."""
|
|
54
|
+
self.segments = Segments(self._transport)
|
|
55
|
+
"""Groups of customers."""
|
|
56
|
+
self.webhook_endpoints = WebhookEndpoints(self._transport)
|
|
57
|
+
"""URLs that hear webhook events."""
|
|
58
|
+
|
|
59
|
+
def close(self) -> None:
|
|
60
|
+
"""Closes the connections it opened. Not needed for an http_client you gave it."""
|
|
61
|
+
self._transport.close()
|
|
62
|
+
|
|
63
|
+
def __enter__(self) -> Self:
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def __exit__(self, kind: type[BaseException] | None, error: BaseException | None, tb: TracebackType | None) -> None:
|
|
67
|
+
self.close()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AsyncGoomzo:
|
|
71
|
+
"""
|
|
72
|
+
The Goomzo API client, for asyncio. The same as `Goomzo`, with every call awaited.
|
|
73
|
+
|
|
74
|
+
from goomzo import AsyncGoomzo
|
|
75
|
+
|
|
76
|
+
goomzo = AsyncGoomzo()
|
|
77
|
+
await goomzo.messages.send_one({'to': '+255712345678', 'template': 'auth_1', 'variables': {'code': '482913'}})
|
|
78
|
+
|
|
79
|
+
Docs: https://docs.goomzo.africa/docs/developers/python-sdk
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
api_key: str | None = None,
|
|
86
|
+
base_url: str | None = None,
|
|
87
|
+
max_retries: int = 2,
|
|
88
|
+
timeout: float = 60.0,
|
|
89
|
+
http_client: httpx.AsyncClient | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
self._transport = AsyncTransport(make_config(api_key, base_url, max_retries, timeout), http_client)
|
|
92
|
+
self.messages = AsyncMessages(self._transport)
|
|
93
|
+
"""Send SMS and WhatsApp messages, each to one person."""
|
|
94
|
+
self.broadcasts = AsyncBroadcasts(self._transport)
|
|
95
|
+
"""Send one message to many people."""
|
|
96
|
+
self.templates = AsyncTemplates(self._transport)
|
|
97
|
+
"""Saved templates, sent by name."""
|
|
98
|
+
self.whatsapp = AsyncWhatsApp(self._transport)
|
|
99
|
+
"""The business's WhatsApp account and its templates."""
|
|
100
|
+
self.customers = AsyncCustomers(self._transport)
|
|
101
|
+
"""Add and update customers."""
|
|
102
|
+
self.activity = AsyncActivityRecorder(self._transport)
|
|
103
|
+
"""Record customer activity, by phone, email or id."""
|
|
104
|
+
self.segments = AsyncSegments(self._transport)
|
|
105
|
+
"""Groups of customers."""
|
|
106
|
+
self.webhook_endpoints = AsyncWebhookEndpoints(self._transport)
|
|
107
|
+
"""URLs that hear webhook events."""
|
|
108
|
+
|
|
109
|
+
async def close(self) -> None:
|
|
110
|
+
"""Closes the connections it opened. Not needed for an http_client you gave it."""
|
|
111
|
+
await self._transport.close()
|
|
112
|
+
|
|
113
|
+
async def __aenter__(self) -> Self:
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
async def __aexit__(
|
|
117
|
+
self, kind: type[BaseException] | None, error: BaseException | None, tb: TracebackType | None
|
|
118
|
+
) -> None:
|
|
119
|
+
await self.close()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing_extensions import TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FieldProblem(TypedDict):
|
|
7
|
+
"""A problem with one field of a request."""
|
|
8
|
+
|
|
9
|
+
code: str
|
|
10
|
+
message: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GoomzoError(Exception):
|
|
14
|
+
"""
|
|
15
|
+
The API answered with an error.
|
|
16
|
+
|
|
17
|
+
`code` says what went wrong, such as `invalid`, `not_found` or `insufficient_sms`. `fields` names the fields that
|
|
18
|
+
were wrong, such as `customers[1].phone`. Include `request_id` when you contact Goomzo.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
status: int,
|
|
24
|
+
code: str,
|
|
25
|
+
message: str,
|
|
26
|
+
fields: dict[str, FieldProblem] | None = None,
|
|
27
|
+
request_id: str | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.status = status
|
|
31
|
+
self.code = code
|
|
32
|
+
self.message = message
|
|
33
|
+
self.fields: dict[str, FieldProblem] = fields or {}
|
|
34
|
+
self.request_id = request_id
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str:
|
|
37
|
+
return f'GoomzoError(status={self.status}, code={self.code!r}, message={self.message!r})'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GoomzoConnectionError(Exception):
|
|
41
|
+
"""No answer came: the network failed, or the request took longer than the timeout."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, message: str, *, timed_out: bool) -> None:
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.timed_out = timed_out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class WebhookVerificationError(Exception):
|
|
49
|
+
"""A webhook's signature or timestamp didn't check out. Don't trust its body."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Generator, Iterator
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar('T')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Page(Generic[T]):
|
|
10
|
+
"""
|
|
11
|
+
One page of a list. Loop over it for every item: the next pages are asked for as you go.
|
|
12
|
+
|
|
13
|
+
page = goomzo.customers.list(limit=200)
|
|
14
|
+
page.items, page.next_cursor, page.total
|
|
15
|
+
|
|
16
|
+
for customer in goomzo.customers.list():
|
|
17
|
+
print(customer['name'])
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, data: Any, fetch: Callable[[str], Page[T]]) -> None:
|
|
21
|
+
self.items: list[T] = data['items']
|
|
22
|
+
self.next_cursor: str | None = data.get('nextCursor')
|
|
23
|
+
self.total: int | None = data.get('total')
|
|
24
|
+
"""How many match, on every page. Only customers, messages and broadcasts have it."""
|
|
25
|
+
self._fetch = fetch
|
|
26
|
+
|
|
27
|
+
def next_page(self) -> Page[T] | None:
|
|
28
|
+
"""The next page, or None on the last."""
|
|
29
|
+
return self._fetch(self.next_cursor) if self.next_cursor else None
|
|
30
|
+
|
|
31
|
+
def __iter__(self) -> Iterator[T]:
|
|
32
|
+
page: Page[T] | None = self
|
|
33
|
+
while page is not None:
|
|
34
|
+
yield from page.items
|
|
35
|
+
page = page.next_page()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AsyncPage(Generic[T]):
|
|
39
|
+
"""One page of a list, for asyncio. `async for` over it for every item."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, data: Any, fetch: Callable[[str], Awaitable[AsyncPage[T]]]) -> None:
|
|
42
|
+
self.items: list[T] = data['items']
|
|
43
|
+
self.next_cursor: str | None = data.get('nextCursor')
|
|
44
|
+
self.total: int | None = data.get('total')
|
|
45
|
+
self._fetch = fetch
|
|
46
|
+
|
|
47
|
+
async def next_page(self) -> AsyncPage[T] | None:
|
|
48
|
+
"""The next page, or None on the last."""
|
|
49
|
+
return await self._fetch(self.next_cursor) if self.next_cursor else None
|
|
50
|
+
|
|
51
|
+
async def __aiter__(self) -> AsyncIterator[T]:
|
|
52
|
+
page: AsyncPage[T] | None = self
|
|
53
|
+
while page is not None:
|
|
54
|
+
for item in page.items:
|
|
55
|
+
yield item
|
|
56
|
+
page = await page.next_page()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AsyncPager(Generic[T]):
|
|
60
|
+
"""
|
|
61
|
+
A list for asyncio. Await it for the first page, or `async for` over it for every item.
|
|
62
|
+
|
|
63
|
+
page = await goomzo.customers.list(limit=200)
|
|
64
|
+
|
|
65
|
+
async for customer in goomzo.customers.list():
|
|
66
|
+
print(customer['name'])
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, first: Callable[[], Awaitable[AsyncPage[T]]]) -> None:
|
|
70
|
+
self._first = first
|
|
71
|
+
|
|
72
|
+
def __await__(self) -> Generator[Any, None, AsyncPage[T]]:
|
|
73
|
+
return self._first().__await__()
|
|
74
|
+
|
|
75
|
+
async def __aiter__(self) -> AsyncIterator[T]:
|
|
76
|
+
async for item in await self._first():
|
|
77
|
+
yield item
|