rmyndharis-openwa 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.
Files changed (30) hide show
  1. rmyndharis_openwa-0.1.0/PKG-INFO +94 -0
  2. rmyndharis_openwa-0.1.0/README.md +78 -0
  3. rmyndharis_openwa-0.1.0/openwa/__init__.py +49 -0
  4. rmyndharis_openwa-0.1.0/openwa/_http.py +115 -0
  5. rmyndharis_openwa-0.1.0/openwa/client.py +148 -0
  6. rmyndharis_openwa-0.1.0/openwa/errors.py +99 -0
  7. rmyndharis_openwa-0.1.0/openwa/py.typed +0 -0
  8. rmyndharis_openwa-0.1.0/openwa/resources/__init__.py +35 -0
  9. rmyndharis_openwa-0.1.0/openwa/resources/catalog.py +58 -0
  10. rmyndharis_openwa-0.1.0/openwa/resources/channels.py +41 -0
  11. rmyndharis_openwa-0.1.0/openwa/resources/chats.py +46 -0
  12. rmyndharis_openwa-0.1.0/openwa/resources/contacts.py +53 -0
  13. rmyndharis_openwa-0.1.0/openwa/resources/groups.py +87 -0
  14. rmyndharis_openwa-0.1.0/openwa/resources/health.py +27 -0
  15. rmyndharis_openwa-0.1.0/openwa/resources/labels.py +40 -0
  16. rmyndharis_openwa-0.1.0/openwa/resources/messages.py +109 -0
  17. rmyndharis_openwa-0.1.0/openwa/resources/py.typed +0 -0
  18. rmyndharis_openwa-0.1.0/openwa/resources/sessions.py +56 -0
  19. rmyndharis_openwa-0.1.0/openwa/resources/status.py +38 -0
  20. rmyndharis_openwa-0.1.0/openwa/resources/templates.py +34 -0
  21. rmyndharis_openwa-0.1.0/openwa/resources/webhooks.py +41 -0
  22. rmyndharis_openwa-0.1.0/openwa/types.py +673 -0
  23. rmyndharis_openwa-0.1.0/pyproject.toml +40 -0
  24. rmyndharis_openwa-0.1.0/rmyndharis_openwa.egg-info/PKG-INFO +94 -0
  25. rmyndharis_openwa-0.1.0/rmyndharis_openwa.egg-info/SOURCES.txt +28 -0
  26. rmyndharis_openwa-0.1.0/rmyndharis_openwa.egg-info/dependency_links.txt +1 -0
  27. rmyndharis_openwa-0.1.0/rmyndharis_openwa.egg-info/requires.txt +5 -0
  28. rmyndharis_openwa-0.1.0/rmyndharis_openwa.egg-info/top_level.txt +1 -0
  29. rmyndharis_openwa-0.1.0/setup.cfg +4 -0
  30. rmyndharis_openwa-0.1.0/tests/test_sdk.py +499 -0
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: rmyndharis-openwa
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for OpenWA WhatsApp API Gateway
5
+ Author: OpenWA Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/rmyndharis/OpenWA
8
+ Project-URL: Repository, https://github.com/rmyndharis/OpenWA
9
+ Keywords: whatsapp,api,sdk,openwa,gateway
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx<1.0,>=0.25.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.0; extra == "dev"
15
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
16
+
17
+ # rmyndharis-openwa
18
+
19
+ Official Python SDK for the [OpenWA](https://github.com/rmyndharis/OpenWA) WhatsApp API Gateway.
20
+
21
+ A synchronous client built on [httpx](https://www.python-httpx.org/), with bundled type hints (PEP 561).
22
+
23
+ ## Install
24
+
25
+ > **Not yet published to PyPI.** Until the first release, install from a checkout
26
+ > of the repo (`cd sdk/python && pip install -e .`) or from git.
27
+
28
+ ```bash
29
+ pip install rmyndharis-openwa # once published
30
+ ```
31
+
32
+ Requires Python 3.9+. The importable module is `openwa`.
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from openwa import OpenWAClient
38
+
39
+ client = OpenWAClient(
40
+ base_url="https://your-gateway.example.com",
41
+ api_key="owa_k1_…",
42
+ )
43
+
44
+ client.sessions.start("my-session")
45
+
46
+ result = client.messages.send_text("my-session", {
47
+ "chatId": "628123456789@c.us",
48
+ "text": "Hello from the OpenWA Python SDK!",
49
+ })
50
+ print(result["messageId"])
51
+ ```
52
+
53
+ The client is also a context manager (it closes the underlying connection pool on exit):
54
+
55
+ ```python
56
+ with OpenWAClient(base_url="…", api_key="…") as client:
57
+ client.messages.send_text("my-session", {"chatId": "…@c.us", "text": "hi"})
58
+ ```
59
+
60
+ For tests, pass an httpx transport — no global monkey-patching required:
61
+
62
+ ```python
63
+ import httpx
64
+ client = OpenWAClient(base_url="…", api_key="…", transport=httpx.MockTransport(handler))
65
+ ```
66
+
67
+ ## Errors
68
+
69
+ A non-2xx response raises a typed `OpenWAApiError` subclass — `OpenWAAuthError` (401),
70
+ `OpenWAForbiddenError` (403), `OpenWANotFoundError` (404), `OpenWAConflictError` (409),
71
+ `OpenWARateLimitError` (429), `OpenWANotImplementedError` (501) — each carrying `.status`
72
+ and the parsed `.body`. A timeout raises `OpenWATimeoutError`.
73
+
74
+ ```python
75
+ from openwa import OpenWANotFoundError
76
+
77
+ try:
78
+ client.sessions.get("missing")
79
+ except OpenWANotFoundError as e:
80
+ print(e.status) # 404
81
+ ```
82
+
83
+ ## Notes
84
+
85
+ - **Use HTTPS in production** — the API key is sent as `X-API-Key` and is bearer-equivalent.
86
+ - The SDK does **not** retry, and **never follows redirects** (so the key is never re-sent to
87
+ a redirect target). Path segments are percent-encoded; a base-URL path prefix (e.g. behind a
88
+ reverse proxy) is preserved.
89
+ - Escape hatch for endpoints the SDK does not wrap:
90
+ `client.request(method, path, query=…, body=…)`.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,78 @@
1
+ # rmyndharis-openwa
2
+
3
+ Official Python SDK for the [OpenWA](https://github.com/rmyndharis/OpenWA) WhatsApp API Gateway.
4
+
5
+ A synchronous client built on [httpx](https://www.python-httpx.org/), with bundled type hints (PEP 561).
6
+
7
+ ## Install
8
+
9
+ > **Not yet published to PyPI.** Until the first release, install from a checkout
10
+ > of the repo (`cd sdk/python && pip install -e .`) or from git.
11
+
12
+ ```bash
13
+ pip install rmyndharis-openwa # once published
14
+ ```
15
+
16
+ Requires Python 3.9+. The importable module is `openwa`.
17
+
18
+ ## Usage
19
+
20
+ ```python
21
+ from openwa import OpenWAClient
22
+
23
+ client = OpenWAClient(
24
+ base_url="https://your-gateway.example.com",
25
+ api_key="owa_k1_…",
26
+ )
27
+
28
+ client.sessions.start("my-session")
29
+
30
+ result = client.messages.send_text("my-session", {
31
+ "chatId": "628123456789@c.us",
32
+ "text": "Hello from the OpenWA Python SDK!",
33
+ })
34
+ print(result["messageId"])
35
+ ```
36
+
37
+ The client is also a context manager (it closes the underlying connection pool on exit):
38
+
39
+ ```python
40
+ with OpenWAClient(base_url="…", api_key="…") as client:
41
+ client.messages.send_text("my-session", {"chatId": "…@c.us", "text": "hi"})
42
+ ```
43
+
44
+ For tests, pass an httpx transport — no global monkey-patching required:
45
+
46
+ ```python
47
+ import httpx
48
+ client = OpenWAClient(base_url="…", api_key="…", transport=httpx.MockTransport(handler))
49
+ ```
50
+
51
+ ## Errors
52
+
53
+ A non-2xx response raises a typed `OpenWAApiError` subclass — `OpenWAAuthError` (401),
54
+ `OpenWAForbiddenError` (403), `OpenWANotFoundError` (404), `OpenWAConflictError` (409),
55
+ `OpenWARateLimitError` (429), `OpenWANotImplementedError` (501) — each carrying `.status`
56
+ and the parsed `.body`. A timeout raises `OpenWATimeoutError`.
57
+
58
+ ```python
59
+ from openwa import OpenWANotFoundError
60
+
61
+ try:
62
+ client.sessions.get("missing")
63
+ except OpenWANotFoundError as e:
64
+ print(e.status) # 404
65
+ ```
66
+
67
+ ## Notes
68
+
69
+ - **Use HTTPS in production** — the API key is sent as `X-API-Key` and is bearer-equivalent.
70
+ - The SDK does **not** retry, and **never follows redirects** (so the key is never re-sent to
71
+ a redirect target). Path segments are percent-encoded; a base-URL path prefix (e.g. behind a
72
+ reverse proxy) is preserved.
73
+ - Escape hatch for endpoints the SDK does not wrap:
74
+ `client.request(method, path, query=…, body=…)`.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,49 @@
1
+ """
2
+ OpenWA Python SDK.
3
+
4
+ Official client library for the OpenWA WhatsApp API Gateway.
5
+
6
+ Example usage::
7
+
8
+ from openwa import OpenWAClient
9
+
10
+ client = OpenWAClient(
11
+ base_url="http://localhost:2785",
12
+ api_key="owa_k1_…",
13
+ )
14
+
15
+ client.sessions.start("my-session")
16
+ result = client.messages.send_text("my-session", {
17
+ "chatId": "628123456789@c.us",
18
+ "text": "Hello from the OpenWA Python SDK!",
19
+ })
20
+ print(result["messageId"])
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from .client import OpenWAClient
26
+ from .errors import (
27
+ OpenWAApiError,
28
+ OpenWAAuthError,
29
+ OpenWAConflictError,
30
+ OpenWAError,
31
+ OpenWAForbiddenError,
32
+ OpenWANotFoundError,
33
+ OpenWANotImplementedError,
34
+ OpenWARateLimitError,
35
+ OpenWATimeoutError,
36
+ )
37
+
38
+ __all__ = [
39
+ "OpenWAClient",
40
+ "OpenWAError",
41
+ "OpenWAApiError",
42
+ "OpenWAAuthError",
43
+ "OpenWAForbiddenError",
44
+ "OpenWANotFoundError",
45
+ "OpenWAConflictError",
46
+ "OpenWARateLimitError",
47
+ "OpenWANotImplementedError",
48
+ "OpenWATimeoutError",
49
+ ]
@@ -0,0 +1,115 @@
1
+ """HTTP transport wrapper for the OpenWA Python SDK.
2
+
3
+ The client never builds a bare :class:`httpx.Client` with a hard-coded
4
+ transport. Instead it accepts an optional ``transport`` (an ``httpx.BaseTransport``)
5
+ that overrides the default. This makes the SDK trivially testable — a test
6
+ passes ``httpx.MockTransport(handler)`` instead of monkey-patching — and lets
7
+ consumers intercept/observability-wrap outbound calls.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Mapping
13
+ from urllib.parse import quote
14
+
15
+ import httpx
16
+
17
+ from .errors import OpenWAApiError, OpenWATimeoutError, classify
18
+
19
+ HttpMethod = str # "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
20
+
21
+
22
+ def quote_segment(segment: Any) -> str:
23
+ """Percent-encode a single path segment so a value containing ``/``, ``#`` or
24
+ ``?`` can't break out of its path position. WhatsApp-id characters that are
25
+ already path-safe (``@``, ``:``, ``+``) are kept readable.
26
+ """
27
+ return quote(str(segment), safe="@:+")
28
+
29
+
30
+ def build_url(base_url: str, path: str, query: Mapping[str, Any] | None = None) -> str:
31
+ """Build a URL, serializing query params and skipping ``None`` values."""
32
+ url = f"{base_url.rstrip('/')}{path}"
33
+ if not query:
34
+ return url
35
+
36
+ def _serialize(v: Any) -> str:
37
+ # Booleans must be lowercase: the backend reads query flags as `=== 'true'`,
38
+ # so Python's default str(True) == 'True' would be silently ignored.
39
+ if v is True:
40
+ return "true"
41
+ if v is False:
42
+ return "false"
43
+ return str(v)
44
+
45
+ params = {k: _serialize(v) for k, v in query.items() if v is not None}
46
+ if not params:
47
+ return url
48
+ req = httpx.Request("GET", url, params=params)
49
+ # httpx.Request already encoded params into the URL string.
50
+ return str(req.url)
51
+
52
+
53
+ class HttpExecutor:
54
+ """Owns the :class:`httpx.Client` and performs JSON requests.
55
+
56
+ Constructed once per :class:`OpenWAClient`; the transport is taken from
57
+ the client config so all requests share one connection pool.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ base_url: str,
63
+ api_key: str,
64
+ timeout: float = 30.0,
65
+ default_headers: Mapping[str, str] | None = None,
66
+ transport: httpx.BaseTransport | None = None,
67
+ ) -> None:
68
+ # Caller-supplied default headers are applied FIRST so the auth/JSON
69
+ # headers below always win and can never be clobbered (mirrors the JS SDK).
70
+ headers: dict[str, str] = {}
71
+ if default_headers:
72
+ headers.update(default_headers)
73
+ headers["Content-Type"] = "application/json"
74
+ headers["X-API-Key"] = api_key
75
+ client_kwargs: dict[str, Any] = {
76
+ "base_url": base_url.rstrip("/"),
77
+ "headers": headers,
78
+ "timeout": timeout,
79
+ # Never auto-follow redirects: doing so would re-send the X-API-Key
80
+ # header to the redirect target (potentially a different origin).
81
+ # (This is also httpx's default; set explicitly so it can't regress.)
82
+ "follow_redirects": False,
83
+ }
84
+ if transport is not None:
85
+ client_kwargs["transport"] = transport
86
+ self._client = httpx.Client(**client_kwargs)
87
+ self._timeout = timeout
88
+
89
+ def close(self) -> None:
90
+ self._client.close()
91
+
92
+ def __enter__(self) -> "HttpExecutor":
93
+ return self
94
+
95
+ def __exit__(self, *exc: Any) -> None:
96
+ self.close()
97
+
98
+ def request(self, method: HttpMethod, path: str, *, query: Mapping[str, Any] | None = None, body: Any = None) -> Any:
99
+ """Perform one request and return the parsed JSON (or ``None`` for 204)."""
100
+ url = build_url("", path, query)
101
+ try:
102
+ res = self._client.request(method, url, json=body if body is not None else None)
103
+ except httpx.TimeoutException as e:
104
+ raise OpenWATimeoutError(self._timeout) from e
105
+ if res.status_code >= 400:
106
+ context = f"{method} {path}"
107
+ raise OpenWAApiError.from_response(res.status_code, res.text, context)
108
+ if res.status_code == 204 or not res.content:
109
+ return None
110
+ try:
111
+ return res.json()
112
+ except ValueError:
113
+ # A 2xx body that isn't JSON surfaces as text rather than a raw
114
+ # JSONDecodeError (mirrors the JS and PHP transports).
115
+ return res.text
@@ -0,0 +1,148 @@
1
+ """OpenWA Python SDK — client core.
2
+
3
+ The :class:`OpenWAClient` is the single entry point. It owns an
4
+ :class:`HttpExecutor` (which wraps :class:`httpx.Client` with an injectable
5
+ transport) and exposes domain resources as properties::
6
+
7
+ from openwa import OpenWAClient
8
+
9
+ client = OpenWAClient(
10
+ base_url="http://localhost:2785",
11
+ api_key="owa_k1_…",
12
+ )
13
+
14
+ client.sessions.start("my-session")
15
+ client.messages.send_text("my-session", {
16
+ "chatId": "628123456789@c.us",
17
+ "text": "Hello from the OpenWA SDK!",
18
+ })
19
+
20
+ Pass ``transport=httpx.MockTransport(handler)`` for testability — no global
21
+ monkey-patching required.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from types import TracebackType
27
+ from typing import Any, Mapping
28
+
29
+ import httpx
30
+
31
+ from ._http import HttpExecutor, HttpMethod
32
+ from .resources import (
33
+ CatalogResource,
34
+ ChannelsResource,
35
+ ChatsResource,
36
+ ContactsResource,
37
+ GroupsResource,
38
+ HealthResource,
39
+ LabelsResource,
40
+ MessagesResource,
41
+ SessionsResource,
42
+ StatusResource,
43
+ TemplatesResource,
44
+ WebhooksResource,
45
+ )
46
+ from .types import AuthValidateResponse
47
+
48
+
49
+ class OpenWAClient:
50
+ def __init__(
51
+ self,
52
+ base_url: str,
53
+ api_key: str,
54
+ *,
55
+ timeout: float = 30.0,
56
+ default_headers: Mapping[str, str] | None = None,
57
+ transport: httpx.BaseTransport | None = None,
58
+ ) -> None:
59
+ if not base_url:
60
+ raise ValueError("OpenWAClient: base_url is required")
61
+ if not api_key:
62
+ raise ValueError("OpenWAClient: api_key is required")
63
+ self._http = HttpExecutor(
64
+ base_url=base_url,
65
+ api_key=api_key,
66
+ timeout=timeout,
67
+ default_headers=default_headers,
68
+ transport=transport,
69
+ )
70
+
71
+ # ── Resources ────────────────────────────────────────────────────
72
+
73
+ @property
74
+ def sessions(self) -> SessionsResource:
75
+ return SessionsResource(self._http)
76
+
77
+ @property
78
+ def messages(self) -> MessagesResource:
79
+ return MessagesResource(self._http)
80
+
81
+ @property
82
+ def contacts(self) -> ContactsResource:
83
+ return ContactsResource(self._http)
84
+
85
+ @property
86
+ def groups(self) -> GroupsResource:
87
+ return GroupsResource(self._http)
88
+
89
+ @property
90
+ def webhooks(self) -> WebhooksResource:
91
+ return WebhooksResource(self._http)
92
+
93
+ @property
94
+ def chats(self) -> ChatsResource:
95
+ return ChatsResource(self._http)
96
+
97
+ @property
98
+ def status(self) -> StatusResource:
99
+ return StatusResource(self._http)
100
+
101
+ @property
102
+ def health(self) -> HealthResource:
103
+ return HealthResource(self._http)
104
+
105
+ @property
106
+ def labels(self) -> LabelsResource:
107
+ return LabelsResource(self._http)
108
+
109
+ @property
110
+ def channels(self) -> ChannelsResource:
111
+ return ChannelsResource(self._http)
112
+
113
+ @property
114
+ def catalog(self) -> CatalogResource:
115
+ return CatalogResource(self._http)
116
+
117
+ @property
118
+ def templates(self) -> TemplatesResource:
119
+ return TemplatesResource(self._http)
120
+
121
+ # ── Auth ─────────────────────────────────────────────────────────
122
+
123
+ def auth(self) -> AuthValidateResponse:
124
+ return self._http.request("POST", "/api/auth/validate")
125
+
126
+ # ── Raw request escape hatch ─────────────────────────────────────
127
+
128
+ def request(
129
+ self,
130
+ method: str,
131
+ path: str,
132
+ *,
133
+ query: Mapping[str, Any] | None = None,
134
+ body: Any = None,
135
+ ) -> Any:
136
+ """Issue a raw request against the API (advanced use). ``path`` begins with ``/``."""
137
+ return self._http.request(method, path, query=query, body=body)
138
+
139
+ # ── Lifecycle ────────────────────────────────────────────────────
140
+
141
+ def close(self) -> None:
142
+ self._http.close()
143
+
144
+ def __enter__(self) -> "OpenWAClient":
145
+ return self
146
+
147
+ def __exit__(self, *exc: Any) -> None:
148
+ self.close()
@@ -0,0 +1,99 @@
1
+ """Typed error hierarchy for the OpenWA Python SDK.
2
+
3
+ The OpenWA API returns NestJS-default errors of the shape::
4
+
5
+ {"statusCode": int, "message": str | list[str], "error": str}
6
+
7
+ This module maps that to a typed, ergonomic error tree so callers can
8
+ ``isinstance``-check or branch on ``.status``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+
16
+ class OpenWAError(Exception):
17
+ """Base class for every error raised by the SDK."""
18
+
19
+
20
+ class OpenWAApiError(OpenWAError):
21
+ """Raised when the API responds with a non-2xx status.
22
+
23
+ Attributes:
24
+ status: HTTP status code.
25
+ body: Parsed JSON body if available, otherwise the raw text.
26
+ error_kind: Value of the ``error`` field in the NestJS envelope.
27
+ """
28
+
29
+ def __init__(self, message: str, status: int, body: Any = None, error_kind: str | None = None) -> None:
30
+ super().__init__(message)
31
+ self.status = status
32
+ self.body = body
33
+ self.error_kind = error_kind
34
+
35
+ @classmethod
36
+ def from_response(cls, status_code: int, text: str, context: str) -> "OpenWAApiError":
37
+ import json
38
+
39
+ body: Any = None
40
+ if text:
41
+ try:
42
+ body = json.loads(text)
43
+ except ValueError:
44
+ body = text
45
+ envelope = body if isinstance(body, dict) and "statusCode" in body else None
46
+ raw_message = envelope.get("message") if envelope else body
47
+ if isinstance(raw_message, list):
48
+ message_text = ", ".join(str(m) for m in raw_message)
49
+ elif isinstance(raw_message, str):
50
+ message_text = raw_message
51
+ else:
52
+ message_text = str(raw_message)
53
+ message = f"OpenWA API {status_code} — {context}: {message_text}"
54
+ return classify(status_code, message, body, envelope.get("error") if envelope else None)
55
+
56
+
57
+ class OpenWAAuthError(OpenWAApiError):
58
+ """401 Unauthorized — missing or invalid API key."""
59
+
60
+
61
+ class OpenWAForbiddenError(OpenWAApiError):
62
+ """403 Forbidden — insufficient role."""
63
+
64
+
65
+ class OpenWANotFoundError(OpenWAApiError):
66
+ """404 Not Found."""
67
+
68
+
69
+ class OpenWAConflictError(OpenWAApiError):
70
+ """409 Conflict — typically an engine-not-ready condition."""
71
+
72
+
73
+ class OpenWARateLimitError(OpenWAApiError):
74
+ """429 Too Many Requests."""
75
+
76
+
77
+ class OpenWANotImplementedError(OpenWAApiError):
78
+ """501 Not Implemented — the active engine does not support this operation."""
79
+
80
+
81
+ class OpenWATimeoutError(OpenWAError):
82
+ """Raised when a request exceeds the configured timeout."""
83
+
84
+ def __init__(self, timeout: float) -> None:
85
+ super().__init__(f"Request timed out after {timeout}s")
86
+ self.timeout = timeout
87
+
88
+
89
+ def classify(status: int, message: str, body: Any, error_kind: str | None) -> OpenWAApiError:
90
+ """Pick the most specific :class:`OpenWAApiError` subclass for a status."""
91
+ cls = {
92
+ 401: OpenWAAuthError,
93
+ 403: OpenWAForbiddenError,
94
+ 404: OpenWANotFoundError,
95
+ 409: OpenWAConflictError,
96
+ 429: OpenWARateLimitError,
97
+ 501: OpenWANotImplementedError,
98
+ }.get(status, OpenWAApiError)
99
+ return cls(message, status, body, error_kind)
File without changes
@@ -0,0 +1,35 @@
1
+ """Resource modules for the OpenWA Python SDK.
2
+
3
+ Each module defines a small ``_*Resource`` class whose methods map 1:1 to an
4
+ API path group. They are constructed by :class:`openwa.client.OpenWAClient`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .catalog import CatalogResource
10
+ from .channels import ChannelsResource
11
+ from .chats import ChatsResource
12
+ from .contacts import ContactsResource
13
+ from .groups import GroupsResource
14
+ from .health import HealthResource
15
+ from .labels import LabelsResource
16
+ from .messages import MessagesResource
17
+ from .sessions import SessionsResource
18
+ from .status import StatusResource
19
+ from .templates import TemplatesResource
20
+ from .webhooks import WebhooksResource
21
+
22
+ __all__ = [
23
+ "CatalogResource",
24
+ "ChannelsResource",
25
+ "ChatsResource",
26
+ "ContactsResource",
27
+ "GroupsResource",
28
+ "HealthResource",
29
+ "LabelsResource",
30
+ "MessagesResource",
31
+ "SessionsResource",
32
+ "StatusResource",
33
+ "TemplatesResource",
34
+ "WebhooksResource",
35
+ ]
@@ -0,0 +1,58 @@
1
+ """Catalog resource — WhatsApp Business catalog, products, and product/catalog sends.
2
+
3
+ Backed by ``src/modules/catalog/catalog.controller.ts``
4
+ (``@Controller('sessions/:sessionId')``). NOTE: the catalog controller is
5
+ mounted under the session root, so catalog reads are ``/catalog...`` while
6
+ product/catalog SENDS share the messages namespace
7
+ (``/messages/send-product``, ``/messages/send-catalog``).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from .._http import quote_segment
15
+ from ..types import (
16
+ CatalogInfo,
17
+ CatalogProduct,
18
+ CatalogProductsQuery,
19
+ MessageResponse,
20
+ PaginatedProducts,
21
+ SendCatalogRequest,
22
+ SendProductRequest,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from .._http import HttpExecutor
27
+
28
+
29
+ class CatalogResource:
30
+ def __init__(self, http: "HttpExecutor") -> None:
31
+ self._http = http
32
+
33
+ def info(self, session_id: str) -> CatalogInfo:
34
+ return self._http.request("GET", f"/api/sessions/{quote_segment(session_id)}/catalog")
35
+
36
+ def products(
37
+ self, session_id: str, query: CatalogProductsQuery | None = None
38
+ ) -> PaginatedProducts:
39
+ return self._http.request(
40
+ "GET", f"/api/sessions/{quote_segment(session_id)}/catalog/products", query=query
41
+ )
42
+
43
+ def product(self, session_id: str, product_id: str) -> CatalogProduct:
44
+ return self._http.request(
45
+ "GET", f"/api/sessions/{quote_segment(session_id)}/catalog/products/{quote_segment(product_id)}"
46
+ )
47
+
48
+ def send_product(self, session_id: str, body: SendProductRequest) -> MessageResponse:
49
+ """Send a product message. Requires an OPERATOR-level key. Shares the messages path."""
50
+ return self._http.request(
51
+ "POST", f"/api/sessions/{quote_segment(session_id)}/messages/send-product", body=body
52
+ )
53
+
54
+ def send_catalog(self, session_id: str, body: SendCatalogRequest) -> MessageResponse:
55
+ """Send a catalog link message. Requires an OPERATOR-level key. Shares the messages path."""
56
+ return self._http.request(
57
+ "POST", f"/api/sessions/{quote_segment(session_id)}/messages/send-catalog", body=body
58
+ )