Skip to content

Commit 4039f6e

Browse files
committed
Initial Python SDK
Sync and async clients for the WaAPI REST API, sharing one error mapping so the two cannot disagree about what counts as a failure. The mapping's important case is not a status code: the API answers HTTP 200 with {"status": "error"} when an instance is not connected, so code that checks only the status code reports a message that was never sent as delivered. That is a FailedActionError here, the same class as an HTTP 400, because a caller can do nothing different about the two. Method surface is deliberately small. The API has 122 client actions, and GenerateSdkMethods in the proxy already emits them for the PHP and Laravel flavours; _actions.py is the target for its Python flavour. Wrapping them by hand now would put every future API change in four places, so unwrapped actions go through action(name, payload) meanwhile. 25 tests, all against httpx.MockTransport - no network, no token, no connected account. One of them asserts the sync and async surfaces are identical. Publishing runs on a v* tag through PyPI Trusted Publishing, so no token is stored in the repository.
0 parents  commit 4039f6e

14 files changed

Lines changed: 1104 additions & 0 deletions

File tree

.github/workflows/publish.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Publish to PyPI
2+
3+
# Publishes on a v* tag using PyPI Trusted Publishing (OIDC) — no API token is
4+
# stored in the repository. Configure the publisher once at
5+
# https://pypi.org/manage/project/waapi/settings/publishing/
6+
# owner: WaAPIapp repository: waapi-python-sdk
7+
# workflow: publish.yml environment: pypi
8+
9+
on:
10+
push:
11+
tags: ['v*']
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: '3.12'
21+
- run: pip install build twine
22+
- run: python -m build
23+
- run: twine check dist/*
24+
- uses: actions/upload-artifact@v4
25+
with:
26+
name: dist
27+
path: dist/
28+
29+
publish:
30+
needs: build
31+
runs-on: ubuntu-latest
32+
environment: pypi
33+
permissions:
34+
id-token: write
35+
steps:
36+
- uses: actions/download-artifact@v4
37+
with:
38+
name: dist
39+
path: dist/
40+
- uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/tests.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.python-version }}
20+
- run: pip install -e ".[dev]"
21+
- run: pytest -q
22+
23+
lint:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.12'
30+
- run: pip install -e ".[dev]"
31+
- run: ruff check .
32+
- run: mypy src/waapi

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
__pycache__/
2+
*.py[cod]
3+
*.egg-info/
4+
build/
5+
dist/
6+
.venv/
7+
venv/
8+
.pytest_cache/
9+
.ruff_cache/
10+
.mypy_cache/
11+
.coverage
12+
htmlcov/
13+
.DS_Store
14+
dist_test/

CONTRIBUTING.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Contributing
2+
3+
## The method surface is generated, not hand-written
4+
5+
The API exposes 122 client actions across 130 paths. The n8n node and the MCP
6+
tools are generated from `storage/swagger.json`, which is why both stay
7+
current, while a hand-maintained SDK falls behind — the Laravel SDK named 25 of
8+
122 actions before its methods were generated.
9+
10+
`GenerateSdkMethods` in the proxy repository emits the PHP and Laravel
11+
flavours today. `src/waapi/_actions.py` is the target for its Python flavour.
12+
Until that lands, prefer widening `client.action(...)` usage in the README over
13+
adding hand-written wrappers: every hand-written method is one more place a
14+
future API change has to reach.
15+
16+
If you do add one by hand, mirror it in **both** `ActionsMixin` and
17+
`AsyncActionsMixin``test_sync_and_async_expose_the_same_methods` fails
18+
otherwise, on purpose.
19+
20+
## Conventions
21+
22+
- Public arguments are `snake_case`; the JSON keys they map to stay
23+
`camelCase` as the API defines them.
24+
- Optional arguments default to `None` and are dropped from the payload by
25+
`prune()`. Do not send explicit nulls.
26+
- Anything that can be checked without a request (a missing instance id, a
27+
media call with no source) raises `ValueError` before the call goes out.
28+
29+
## Tests
30+
31+
```bash
32+
pip install -e ".[dev]"
33+
pytest
34+
```
35+
36+
No test may touch the network. Use `httpx.MockTransport`.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) WaAPI <info@waapi.app>
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.

README.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# WaAPI Python SDK
2+
3+
Official Python client for the [WaAPI](https://waapi.app) REST API — send and
4+
receive WhatsApp messages, manage chats, groups and channels from Python.
5+
6+
[![PyPI](https://img.shields.io/pypi/v/waapi?style=for-the-badge)](https://pypi.org/project/waapi/)
7+
[![Python](https://img.shields.io/pypi/pyversions/waapi?style=for-the-badge)](https://pypi.org/project/waapi/)
8+
[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
9+
10+
```bash
11+
pip install waapi
12+
```
13+
14+
## Quick start
15+
16+
```python
17+
from waapi import WaAPI
18+
19+
client = WaAPI(token="YOUR_API_TOKEN", instance_id=123)
20+
21+
client.send_message(
22+
chat_id="4915112345678@c.us",
23+
message="Deployment finished.",
24+
)
25+
```
26+
27+
Get a token at [waapi.app/user/api-tokens](https://waapi.app/user/api-tokens)
28+
and create an instance connected to your number.
29+
30+
### The chat ID is the one thing to get right
31+
32+
Its suffix decides where the message lands, and a wrong suffix is accepted and
33+
delivers nothing:
34+
35+
| Target | Format |
36+
|---|---|
37+
| One person | `4915112345678@c.us` |
38+
| Group | `123456789-123456789@g.us` |
39+
| Channel | `123456789@newsletter` |
40+
41+
## Async
42+
43+
Same method names, awaited:
44+
45+
```python
46+
from waapi import AsyncWaAPI
47+
48+
async with AsyncWaAPI(token="YOUR_API_TOKEN", instance_id=123) as client:
49+
await client.send_message(chat_id="4915112345678@c.us", message="Hi")
50+
```
51+
52+
## Errors
53+
54+
A successful HTTP exchange is not proof the message was sent. The API answers
55+
`200` with `{"status": "error"}` when, for example, the instance is not
56+
connected — so the SDK raises on that too, rather than handing back a body that
57+
looks like success.
58+
59+
```python
60+
from waapi import WaAPI, FailedActionError, AuthenticationError, RateLimitError
61+
62+
try:
63+
client.send_message(chat_id="4915112345678@c.us", message="Hi")
64+
except AuthenticationError:
65+
... # token wrong, expired, or missing scopes
66+
except RateLimitError as e:
67+
time.sleep(e.retry_after or 5)
68+
except FailedActionError as e:
69+
... # accepted but not carried out — e.response has the detail
70+
```
71+
72+
| Exception | Raised on |
73+
|---|---|
74+
| `AuthenticationError` | HTTP 401, 403 |
75+
| `NotFoundError` | HTTP 404 |
76+
| `ValidationError` | HTTP 422 — `.errors` holds the field errors |
77+
| `RateLimitError` | HTTP 429 — `.retry_after` in seconds when the API sends it |
78+
| `FailedActionError` | HTTP 400, **and HTTP 200 with `status: error`** |
79+
| `ServerError` | HTTP 5xx |
80+
81+
All inherit from `WaAPIError`.
82+
83+
## Endpoints not wrapped yet
84+
85+
The API exposes 122 client actions. This release wraps the common ones; every
86+
other action is reachable by name:
87+
88+
```python
89+
client.action("send-seen", {"chatId": "4915112345678@c.us"})
90+
client.action("create-group", {"title": "Ops", "participants": ["4915112345678@c.us"]})
91+
```
92+
93+
The remaining wrappers are generated from the OpenAPI specification rather than
94+
written by hand — see [CONTRIBUTING.md](CONTRIBUTING.md).
95+
96+
## Configuration
97+
98+
```python
99+
WaAPI(
100+
token="...", # required
101+
instance_id=123, # optional; per-call instance_id overrides it
102+
base_url="https://waapi.app/api/v1",
103+
timeout=30.0,
104+
)
105+
```
106+
107+
Passing `instance_id` to the client keeps single-instance code short. Any call
108+
can still override it, and a call with neither raises before a request is sent.
109+
110+
## Development
111+
112+
```bash
113+
python -m venv .venv && source .venv/bin/activate
114+
pip install -e ".[dev]"
115+
pytest
116+
```
117+
118+
The suite runs entirely against `httpx.MockTransport` — no network, no token,
119+
no connected account.
120+
121+
## License
122+
123+
MIT. Not affiliated with, endorsed or sponsored by WhatsApp LLC or Meta.
124+
WhatsApp is a trademark of WhatsApp LLC.

pyproject.toml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "waapi"
7+
version = "0.1.0"
8+
description = "Official Python SDK for the WaAPI REST API"
9+
readme = "README.md"
10+
license = { file = "LICENSE" }
11+
requires-python = ">=3.9"
12+
authors = [{ name = "WaAPI", email = "info@waapi.app" }]
13+
keywords = ["waapi", "messaging", "api", "sdk", "automation", "chatbot"]
14+
classifiers = [
15+
"Development Status :: 4 - Beta",
16+
"Intended Audience :: Developers",
17+
"License :: OSI Approved :: MIT License",
18+
"Programming Language :: Python :: 3",
19+
"Programming Language :: Python :: 3.9",
20+
"Programming Language :: Python :: 3.10",
21+
"Programming Language :: Python :: 3.11",
22+
"Programming Language :: Python :: 3.12",
23+
"Programming Language :: Python :: 3.13",
24+
"Topic :: Communications :: Chat",
25+
"Topic :: Software Development :: Libraries :: Python Modules",
26+
"Typing :: Typed",
27+
]
28+
dependencies = ["httpx>=0.24,<1.0"]
29+
30+
[project.urls]
31+
Homepage = "https://waapi.app"
32+
Documentation = "https://waapi.app/docs"
33+
Source = "https://github.com/WaAPIapp/waapi-python-sdk"
34+
Issues = "https://github.com/WaAPIapp/waapi-python-sdk/issues"
35+
36+
[project.optional-dependencies]
37+
dev = ["pytest>=7", "pytest-asyncio>=0.23", "respx>=0.20", "ruff>=0.4", "mypy>=1.8"]
38+
39+
[tool.hatch.build.targets.wheel]
40+
packages = ["src/waapi"]
41+
42+
[tool.ruff]
43+
line-length = 100
44+
src = ["src", "tests"]
45+
46+
[tool.ruff.lint]
47+
# PYI034 wants __enter__/__aenter__ to return typing.Self, which landed in
48+
# 3.11. requires-python is 3.9, and pulling in typing_extensions as a runtime
49+
# dependency for one annotation is a worse trade than the lint.
50+
ignore = ["PYI034"]
51+
52+
[tool.pytest.ini_options]
53+
testpaths = ["tests"]
54+
asyncio_mode = "auto"

src/waapi/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Official Python SDK for the WaAPI REST API.
2+
3+
from waapi import WaAPI
4+
5+
client = WaAPI(token="YOUR_TOKEN", instance_id=123)
6+
client.send_message(chat_id="4915112345678@c.us", message="Hello")
7+
"""
8+
9+
__version__ = "0.1.0"
10+
11+
from .client import AsyncWaAPI, WaAPI
12+
from .exceptions import (
13+
AuthenticationError,
14+
FailedActionError,
15+
NotFoundError,
16+
RateLimitError,
17+
ServerError,
18+
ValidationError,
19+
WaAPIError,
20+
)
21+
22+
__all__ = [
23+
"AsyncWaAPI",
24+
"AuthenticationError",
25+
"FailedActionError",
26+
"NotFoundError",
27+
"RateLimitError",
28+
"ServerError",
29+
"ValidationError",
30+
"WaAPI",
31+
"WaAPIError",
32+
"__version__",
33+
]

0 commit comments

Comments
 (0)