File size: 5,652 Bytes
46252cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"""OpenWA Python SDK β€” client core.

The :class:`OpenWAClient` is the single entry point. It owns an
:class:`HttpExecutor` (which wraps :class:`httpx.Client` with an injectable
transport) and exposes domain resources as properties::

    from openwa import OpenWAClient

    client = OpenWAClient(
        base_url="http://localhost:2785",
        api_key="owa_k1_…",
    )

    client.sessions.start("my-session")
    client.messages.send_text("my-session", {
        "chatId": "628123456789@c.us",
        "text": "Hello from the OpenWA SDK!",
    })

Pass ``transport=httpx.MockTransport(handler)`` for testability β€” no global
monkey-patching required.
"""

from __future__ import annotations

import warnings
from types import TracebackType
from typing import Any, Mapping
from urllib.parse import urlparse

import httpx

from ._http import HttpExecutor, HttpMethod
from .resources import (
    CallsResource,
    CatalogResource,
    ChannelsResource,
    ChatsResource,
    ContactsResource,
    GroupsResource,
    HealthResource,
    LabelsResource,
    MessagesResource,
    ProfileResource,
    SearchResource,
    SessionsResource,
    StatusResource,
    TemplatesResource,
    WebhooksResource,
)
from .types import AuthValidateResponse


_LOCALHOST_HOSTS = {"localhost", "127.0.0.1", "::1"}


def _warn_if_insecure_http(url: str) -> None:
    """Warn (not raise) when base_url is http:// and the host is not localhost.

    The API key is sent as an X-API-Key header on every request β€” over plaintext http
    to a non-local host that's cleartext on the wire. Warning (not refusing) keeps local
    dev and TLS-terminating-proxy topologies working.
    """
    try:
        parsed = urlparse(url)
        host = (parsed.hostname or "").strip("[]")
        if parsed.scheme == "http" and host not in _LOCALHOST_HOSTS:
            warnings.warn(
                f"OpenWAClient: base_url uses an insecure http:// URL (host: {host}). "
                "The API key will be sent in cleartext. Use https:// in production.",
                stacklevel=3,
            )
    except Exception:
        pass


class OpenWAClient:
    def __init__(
        self,
        base_url: str,
        api_key: str,
        *,
        timeout: float = 30.0,
        default_headers: Mapping[str, str] | None = None,
        transport: httpx.BaseTransport | None = None,
    ) -> None:
        if not base_url:
            raise ValueError("OpenWAClient: base_url is required")
        if not api_key:
            raise ValueError("OpenWAClient: api_key is required")
        _warn_if_insecure_http(base_url)
        self._http = HttpExecutor(
            base_url=base_url,
            api_key=api_key,
            timeout=timeout,
            default_headers=default_headers,
            transport=transport,
        )

    # ── Resources ────────────────────────────────────────────────────

    @property
    def sessions(self) -> SessionsResource:
        return SessionsResource(self._http)

    @property
    def messages(self) -> MessagesResource:
        return MessagesResource(self._http)

    @property
    def contacts(self) -> ContactsResource:
        return ContactsResource(self._http)

    @property
    def groups(self) -> GroupsResource:
        return GroupsResource(self._http)

    @property
    def webhooks(self) -> WebhooksResource:
        return WebhooksResource(self._http)

    @property
    def chats(self) -> ChatsResource:
        return ChatsResource(self._http)

    @property
    def status(self) -> StatusResource:
        return StatusResource(self._http)

    @property
    def health(self) -> HealthResource:
        return HealthResource(self._http)

    @property
    def labels(self) -> LabelsResource:
        return LabelsResource(self._http)

    @property
    def channels(self) -> ChannelsResource:
        return ChannelsResource(self._http)

    @property
    def catalog(self) -> CatalogResource:
        return CatalogResource(self._http)

    @property
    def templates(self) -> TemplatesResource:
        return TemplatesResource(self._http)

    @property
    def search(self) -> SearchResource:
        return SearchResource(self._http)

    @property
    def profile(self) -> ProfileResource:
        return ProfileResource(self._http)

    @property
    def calls(self) -> CallsResource:
        return CallsResource(self._http)

    # ── Auth ─────────────────────────────────────────────────────────

    def auth(self) -> AuthValidateResponse:
        return self._http.request("POST", "/api/auth/validate")

    # ── Raw request escape hatch ─────────────────────────────────────

    def request(
        self,
        method: str,
        path: str,
        *,
        query: Mapping[str, Any] | None = None,
        body: Any = None,
    ) -> Any:
        """Issue a raw request against the API (advanced use). ``path`` begins with ``/``."""
        return self._http.request(method, path, query=query, body=body)

    # ── Lifecycle ────────────────────────────────────────────────────

    def close(self) -> None:
        self._http.close()

    def __enter__(self) -> "OpenWAClient":
        return self

    def __exit__(self, *exc: Any) -> None:
        self.close()