waapi 0.1.0__py3-none-any.whl
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.
- waapi/__init__.py +33 -0
- waapi/_actions.py +152 -0
- waapi/_generated.py +4094 -0
- waapi/_http.py +97 -0
- waapi/client.py +118 -0
- waapi/exceptions.py +60 -0
- waapi/py.typed +0 -0
- waapi-0.1.0.dist-info/METADATA +185 -0
- waapi-0.1.0.dist-info/RECORD +11 -0
- waapi-0.1.0.dist-info/WHEEL +4 -0
- waapi-0.1.0.dist-info/licenses/LICENSE +21 -0
waapi/_http.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""HTTP transport and error mapping.
|
|
2
|
+
|
|
3
|
+
Both the sync and the async client route every call through :func:`interpret`,
|
|
4
|
+
so the two cannot drift apart on what counts as a failure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json as _json
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
FailedActionError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
ServerError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
WaAPIError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
DEFAULT_BASE_URL = "https://waapi.app/api/v1"
|
|
26
|
+
DEFAULT_TIMEOUT = 30.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_headers(token: str, user_agent: str) -> dict[str, str]:
|
|
30
|
+
return {
|
|
31
|
+
"authorization": f"Bearer {token}",
|
|
32
|
+
"accept": "application/json",
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
"user-agent": user_agent,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _decode(response: httpx.Response) -> Any:
|
|
39
|
+
try:
|
|
40
|
+
return response.json()
|
|
41
|
+
except (ValueError, _json.JSONDecodeError):
|
|
42
|
+
return response.text
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def interpret(response: httpx.Response, *, check_body_status: bool = True) -> Any:
|
|
46
|
+
"""Turn a response into a payload, or raise the matching exception.
|
|
47
|
+
|
|
48
|
+
``check_body_status`` exists because a few endpoints answer without the
|
|
49
|
+
envelope. Leave it on unless you know the endpoint is one of them: turning
|
|
50
|
+
it off is what makes a failed send look like a successful one.
|
|
51
|
+
"""
|
|
52
|
+
body = _decode(response)
|
|
53
|
+
status = response.status_code
|
|
54
|
+
|
|
55
|
+
if status == 401 or status == 403:
|
|
56
|
+
raise AuthenticationError(
|
|
57
|
+
f"WaAPI rejected the API token (HTTP {status}): {_brief(body)}"
|
|
58
|
+
)
|
|
59
|
+
if status == 404:
|
|
60
|
+
raise NotFoundError()
|
|
61
|
+
if status == 422:
|
|
62
|
+
raise ValidationError(body)
|
|
63
|
+
if status == 429:
|
|
64
|
+
retry_after = response.headers.get("retry-after")
|
|
65
|
+
raise RateLimitError(
|
|
66
|
+
f"Rate limited by WaAPI (HTTP 429): {_brief(body)}",
|
|
67
|
+
retry_after=float(retry_after) if retry_after else None,
|
|
68
|
+
)
|
|
69
|
+
if status == 400:
|
|
70
|
+
raise FailedActionError(f"WaAPI rejected the request: {_brief(body)}", body)
|
|
71
|
+
if 500 <= status < 600:
|
|
72
|
+
raise ServerError(f"WaAPI returned HTTP {status}: {_brief(body)}")
|
|
73
|
+
if not 200 <= status < 300:
|
|
74
|
+
raise WaAPIError(f"WaAPI returned HTTP {status}: {_brief(body)}")
|
|
75
|
+
|
|
76
|
+
# A 2xx is not proof the action happened. The API answers 200 with
|
|
77
|
+
# {"status": "error"} when, for example, the instance is not connected.
|
|
78
|
+
if check_body_status and isinstance(body, Mapping):
|
|
79
|
+
envelope = body.get("status")
|
|
80
|
+
if envelope is not None and envelope != "success":
|
|
81
|
+
raise FailedActionError(
|
|
82
|
+
"WaAPI accepted the request but did not carry the action out: "
|
|
83
|
+
+ _brief(body),
|
|
84
|
+
body,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return body
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _brief(body: Any, limit: int = 400) -> str:
|
|
91
|
+
text = body if isinstance(body, str) else _json.dumps(body, ensure_ascii=False)
|
|
92
|
+
return text if len(text) <= limit else text[:limit] + "..."
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def prune(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
96
|
+
"""Drop keys whose value is None so optional parameters stay optional."""
|
|
97
|
+
return {k: v for k, v in payload.items() if v is not None}
|
waapi/client.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Synchronous and asynchronous WaAPI clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from ._actions import ActionsMixin, AsyncActionsMixin
|
|
11
|
+
from ._http import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, build_headers, interpret
|
|
12
|
+
|
|
13
|
+
_USER_AGENT = f"waapi-python-sdk/{__version__}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Base:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
token: str,
|
|
20
|
+
*,
|
|
21
|
+
instance_id: int | str | None = None,
|
|
22
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
23
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
24
|
+
) -> None:
|
|
25
|
+
if not token:
|
|
26
|
+
raise ValueError("an API token is required — create one at https://waapi.app/user/api-tokens")
|
|
27
|
+
self.token = token
|
|
28
|
+
self.instance_id = instance_id
|
|
29
|
+
self.base_url = base_url.rstrip("/")
|
|
30
|
+
self.timeout = timeout
|
|
31
|
+
|
|
32
|
+
def _resolve_instance(self, instance_id: int | str | None) -> int | str:
|
|
33
|
+
"""Fall back to the client-wide instance so single-instance code stays short."""
|
|
34
|
+
resolved = instance_id if instance_id is not None else self.instance_id
|
|
35
|
+
if resolved is None:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"no instance id given and none configured on the client — "
|
|
38
|
+
"pass instance_id=... to the call or to WaAPI(...)"
|
|
39
|
+
)
|
|
40
|
+
return resolved
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WaAPI(_Base, ActionsMixin):
|
|
44
|
+
"""Blocking client.
|
|
45
|
+
|
|
46
|
+
>>> client = WaAPI(token="...", instance_id=123)
|
|
47
|
+
>>> client.send_message(chat_id="4915112345678@c.us", message="Hello")
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, token: str, **kwargs: Any) -> None:
|
|
51
|
+
transport = kwargs.pop("transport", None)
|
|
52
|
+
super().__init__(token, **kwargs)
|
|
53
|
+
self._client = httpx.Client(
|
|
54
|
+
base_url=self.base_url,
|
|
55
|
+
headers=build_headers(self.token, _USER_AGENT),
|
|
56
|
+
timeout=self.timeout,
|
|
57
|
+
transport=transport,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def request(
|
|
61
|
+
self,
|
|
62
|
+
method: str,
|
|
63
|
+
path: str,
|
|
64
|
+
*,
|
|
65
|
+
json: Any = None,
|
|
66
|
+
params: Any = None,
|
|
67
|
+
check_body_status: bool = True,
|
|
68
|
+
) -> Any:
|
|
69
|
+
response = self._client.request(method, path, json=json, params=params)
|
|
70
|
+
return interpret(response, check_body_status=check_body_status)
|
|
71
|
+
|
|
72
|
+
def close(self) -> None:
|
|
73
|
+
self._client.close()
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> WaAPI:
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __exit__(self, *exc: object) -> None:
|
|
79
|
+
self.close()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AsyncWaAPI(_Base, AsyncActionsMixin):
|
|
83
|
+
"""Non-blocking client with the same method names as :class:`WaAPI`.
|
|
84
|
+
|
|
85
|
+
>>> async with AsyncWaAPI(token="...", instance_id=123) as client:
|
|
86
|
+
... await client.send_message(chat_id="4915112345678@c.us", message="Hi")
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, token: str, **kwargs: Any) -> None:
|
|
90
|
+
transport = kwargs.pop("transport", None)
|
|
91
|
+
super().__init__(token, **kwargs)
|
|
92
|
+
self._client = httpx.AsyncClient(
|
|
93
|
+
base_url=self.base_url,
|
|
94
|
+
headers=build_headers(self.token, _USER_AGENT),
|
|
95
|
+
timeout=self.timeout,
|
|
96
|
+
transport=transport,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def request(
|
|
100
|
+
self,
|
|
101
|
+
method: str,
|
|
102
|
+
path: str,
|
|
103
|
+
*,
|
|
104
|
+
json: Any = None,
|
|
105
|
+
params: Any = None,
|
|
106
|
+
check_body_status: bool = True,
|
|
107
|
+
) -> Any:
|
|
108
|
+
response = await self._client.request(method, path, json=json, params=params)
|
|
109
|
+
return interpret(response, check_body_status=check_body_status)
|
|
110
|
+
|
|
111
|
+
async def aclose(self) -> None:
|
|
112
|
+
await self._client.aclose()
|
|
113
|
+
|
|
114
|
+
async def __aenter__(self) -> AsyncWaAPI:
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
118
|
+
await self.aclose()
|
waapi/exceptions.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Exceptions raised by the SDK.
|
|
2
|
+
|
|
3
|
+
The hierarchy mirrors the PHP SDK so that the same API condition carries the
|
|
4
|
+
same name in both: a 422 is a validation problem, a 404 is a missing resource,
|
|
5
|
+
a 400 is a rejected action.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WaAPIError(Exception):
|
|
14
|
+
"""Base class for every error this SDK raises."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ValidationError(WaAPIError):
|
|
18
|
+
"""The request body failed the API's validation (HTTP 422)."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, errors: Any) -> None:
|
|
21
|
+
super().__init__("The given data failed to pass validation.")
|
|
22
|
+
self.errors = errors
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotFoundError(WaAPIError):
|
|
26
|
+
"""The addressed resource does not exist (HTTP 404)."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str = "The resource you are looking for could not be found.") -> None:
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthenticationError(WaAPIError):
|
|
33
|
+
"""The API token is missing, wrong or lacks the required scope (HTTP 401/403)."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RateLimitError(WaAPIError):
|
|
37
|
+
"""Too many requests (HTTP 429)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, retry_after: float | None = None) -> None:
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.retry_after = retry_after
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FailedActionError(WaAPIError):
|
|
45
|
+
"""The API accepted the request but did not carry the action out.
|
|
46
|
+
|
|
47
|
+
This covers two cases that look different on the wire but mean the same
|
|
48
|
+
thing to a caller: an HTTP 400, and an HTTP 200 whose body says
|
|
49
|
+
``{"status": "error"}``. The second is the dangerous one — it is a
|
|
50
|
+
successful HTTP exchange, so code that only checks the status code treats
|
|
51
|
+
a message that was never sent as delivered.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message: str, response: Any = None) -> None:
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
self.response = response
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ServerError(WaAPIError):
|
|
60
|
+
"""The API returned a 5xx."""
|
waapi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: waapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the WaAPI REST API
|
|
5
|
+
Project-URL: Homepage, https://waapi.app
|
|
6
|
+
Project-URL: Documentation, https://waapi.app/docs
|
|
7
|
+
Project-URL: Source, https://github.com/WaAPIapp/waapi-python-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/WaAPIapp/waapi-python-sdk/issues
|
|
9
|
+
Author-email: WaAPI <info@waapi.app>
|
|
10
|
+
License: The MIT License (MIT)
|
|
11
|
+
|
|
12
|
+
Copyright (c) WaAPI <info@waapi.app>
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: api,automation,chatbot,messaging,sdk,waapi
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Communications :: Chat
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.9
|
|
46
|
+
Requires-Dist: httpx<1.0,>=0.24
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
51
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
|
|
55
|
+
# WaAPI Python SDK
|
|
56
|
+
|
|
57
|
+
Official Python client for the [WaAPI](https://waapi.app) REST API — send and
|
|
58
|
+
receive WhatsApp messages, manage chats, groups and channels from Python.
|
|
59
|
+
|
|
60
|
+
[](https://pypi.org/project/waapi/)
|
|
61
|
+
[](https://pypi.org/project/waapi/)
|
|
62
|
+
[](LICENSE)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install waapi
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from waapi import WaAPI
|
|
72
|
+
|
|
73
|
+
client = WaAPI(token="YOUR_API_TOKEN", instance_id=123)
|
|
74
|
+
|
|
75
|
+
client.send_message(
|
|
76
|
+
chat_id="4915112345678@c.us",
|
|
77
|
+
message="Deployment finished.",
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Get a token at [waapi.app/user/api-tokens](https://waapi.app/user/api-tokens)
|
|
82
|
+
and create an instance connected to your number.
|
|
83
|
+
|
|
84
|
+
### The chat ID is the one thing to get right
|
|
85
|
+
|
|
86
|
+
Its suffix decides where the message lands, and a wrong suffix is accepted and
|
|
87
|
+
delivers nothing:
|
|
88
|
+
|
|
89
|
+
| Target | Format |
|
|
90
|
+
|---|---|
|
|
91
|
+
| One person | `4915112345678@c.us` |
|
|
92
|
+
| Group | `123456789-123456789@g.us` |
|
|
93
|
+
| Channel | `123456789@newsletter` |
|
|
94
|
+
|
|
95
|
+
## Async
|
|
96
|
+
|
|
97
|
+
Same method names, awaited:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from waapi import AsyncWaAPI
|
|
101
|
+
|
|
102
|
+
async with AsyncWaAPI(token="YOUR_API_TOKEN", instance_id=123) as client:
|
|
103
|
+
await client.send_message(chat_id="4915112345678@c.us", message="Hi")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Errors
|
|
107
|
+
|
|
108
|
+
A successful HTTP exchange is not proof the message was sent. The API answers
|
|
109
|
+
`200` with `{"status": "error"}` when, for example, the instance is not
|
|
110
|
+
connected — so the SDK raises on that too, rather than handing back a body that
|
|
111
|
+
looks like success.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from waapi import WaAPI, FailedActionError, AuthenticationError, RateLimitError
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
client.send_message(chat_id="4915112345678@c.us", message="Hi")
|
|
118
|
+
except AuthenticationError:
|
|
119
|
+
... # token wrong, expired, or missing scopes
|
|
120
|
+
except RateLimitError as e:
|
|
121
|
+
time.sleep(e.retry_after or 5)
|
|
122
|
+
except FailedActionError as e:
|
|
123
|
+
... # accepted but not carried out — e.response has the detail
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Exception | Raised on |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `AuthenticationError` | HTTP 401, 403 |
|
|
129
|
+
| `NotFoundError` | HTTP 404 |
|
|
130
|
+
| `ValidationError` | HTTP 422 — `.errors` holds the field errors |
|
|
131
|
+
| `RateLimitError` | HTTP 429 — `.retry_after` in seconds when the API sends it |
|
|
132
|
+
| `FailedActionError` | HTTP 400, **and HTTP 200 with `status: error`** |
|
|
133
|
+
| `ServerError` | HTTP 5xx |
|
|
134
|
+
|
|
135
|
+
All inherit from `WaAPIError`.
|
|
136
|
+
|
|
137
|
+
## Coverage
|
|
138
|
+
|
|
139
|
+
All **122 client actions** are wrapped, typed, and available on both clients:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
client.create_group(group_name="Ops", group_participants=["4915112345678@c.us"])
|
|
143
|
+
client.send_media(chat_id="4915112345678@c.us", media_url="https://example.com/report.pdf")
|
|
144
|
+
client.get_contacts()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
They are generated from the same OpenAPI specification the n8n node and the MCP
|
|
148
|
+
tools come from, so they track the API instead of drifting behind it — see
|
|
149
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
150
|
+
|
|
151
|
+
An action added to the API since the last release is still reachable by name:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
client.action("some-new-action", {"chatId": "4915112345678@c.us"})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
WaAPI(
|
|
161
|
+
token="...", # required
|
|
162
|
+
instance_id=123, # optional; per-call instance_id overrides it
|
|
163
|
+
base_url="https://waapi.app/api/v1",
|
|
164
|
+
timeout=30.0,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Passing `instance_id` to the client keeps single-instance code short. Any call
|
|
169
|
+
can still override it, and a call with neither raises before a request is sent.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
python -m venv .venv && source .venv/bin/activate
|
|
175
|
+
pip install -e ".[dev]"
|
|
176
|
+
pytest
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The suite runs entirely against `httpx.MockTransport` — no network, no token,
|
|
180
|
+
no connected account.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT. Not affiliated with, endorsed or sponsored by WhatsApp LLC or Meta.
|
|
185
|
+
WhatsApp is a trademark of WhatsApp LLC.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
waapi/__init__.py,sha256=ZwQguHWIZ4yj5vRQF3Nl8djc8Dtf7Eg8wymJ4Q-jrwE,659
|
|
2
|
+
waapi/_actions.py,sha256=0WnfpKGaK1mPrQ_ldlhTR90ypccITpKCmNqd7z1qRpI,5446
|
|
3
|
+
waapi/_generated.py,sha256=Ge0dFy50kSw9y6aiPjrd698kVQu80nPwn1P4XoZqEzs,105033
|
|
4
|
+
waapi/_http.py,sha256=LHTmpV7Imv7LbtqQ0phMKp0R0q01tct4_qbE6N7SSEI,3156
|
|
5
|
+
waapi/client.py,sha256=U9YsxmMNCwzHuOXcwgkLSlhA7hxDNKaP9EUyg2jsnrM,3628
|
|
6
|
+
waapi/exceptions.py,sha256=H979M-QskB-FAQISVAj1HeaETDOkcjxPkAv7Jyy62ws,1870
|
|
7
|
+
waapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
waapi-0.1.0.dist-info/METADATA,sha256=ogxwxcM-hVfMhRlZxVpEBRtg9SQX3jrrvkGv_wnzaHo,6468
|
|
9
|
+
waapi-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
10
|
+
waapi-0.1.0.dist-info/licenses/LICENSE,sha256=I_-EKdTBwwmElru8k9Gje8LTfCO4lG6OUJqk5zvne-c,1084
|
|
11
|
+
waapi-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|