pilot-status 0.0.9__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 (35) hide show
  1. pilot_status-0.0.9/PKG-INFO +137 -0
  2. pilot_status-0.0.9/README.md +117 -0
  3. pilot_status-0.0.9/pyproject.toml +33 -0
  4. pilot_status-0.0.9/setup.cfg +4 -0
  5. pilot_status-0.0.9/src/pilot_status/__init__.py +76 -0
  6. pilot_status-0.0.9/src/pilot_status/client.py +36 -0
  7. pilot_status-0.0.9/src/pilot_status/errors.py +81 -0
  8. pilot_status-0.0.9/src/pilot_status/http.py +156 -0
  9. pilot_status-0.0.9/src/pilot_status/resources/__init__.py +13 -0
  10. pilot_status-0.0.9/src/pilot_status/resources/analytics.py +13 -0
  11. pilot_status-0.0.9/src/pilot_status/resources/api_keys.py +24 -0
  12. pilot_status-0.0.9/src/pilot_status/resources/messages.py +33 -0
  13. pilot_status-0.0.9/src/pilot_status/resources/numbers.py +37 -0
  14. pilot_status-0.0.9/src/pilot_status/resources/projects.py +20 -0
  15. pilot_status-0.0.9/src/pilot_status/types.py +180 -0
  16. pilot_status-0.0.9/src/pilot_status/webhooks/__init__.py +19 -0
  17. pilot_status-0.0.9/src/pilot_status/webhooks/parse.py +255 -0
  18. pilot_status-0.0.9/src/pilot_status/webhooks/types.py +245 -0
  19. pilot_status-0.0.9/src/pilot_status.egg-info/PKG-INFO +137 -0
  20. pilot_status-0.0.9/src/pilot_status.egg-info/SOURCES.txt +33 -0
  21. pilot_status-0.0.9/src/pilot_status.egg-info/dependency_links.txt +1 -0
  22. pilot_status-0.0.9/src/pilot_status.egg-info/requires.txt +1 -0
  23. pilot_status-0.0.9/src/pilot_status.egg-info/top_level.txt +2 -0
  24. pilot_status-0.0.9/src/pilot_status_sdk/__init__.py +56 -0
  25. pilot_status-0.0.9/src/pilot_status_sdk/client.py +36 -0
  26. pilot_status-0.0.9/src/pilot_status_sdk/errors.py +81 -0
  27. pilot_status-0.0.9/src/pilot_status_sdk/http.py +156 -0
  28. pilot_status-0.0.9/src/pilot_status_sdk/resources/__init__.py +5 -0
  29. pilot_status-0.0.9/src/pilot_status_sdk/resources/analytics.py +13 -0
  30. pilot_status-0.0.9/src/pilot_status_sdk/resources/messages.py +22 -0
  31. pilot_status-0.0.9/src/pilot_status_sdk/types.py +58 -0
  32. pilot_status-0.0.9/src/pilot_status_sdk/webhooks/__init__.py +19 -0
  33. pilot_status-0.0.9/src/pilot_status_sdk/webhooks/parse.py +241 -0
  34. pilot_status-0.0.9/src/pilot_status_sdk/webhooks/types.py +227 -0
  35. pilot_status-0.0.9/tests/test_webhooks.py +161 -0
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: pilot-status
3
+ Version: 0.0.9
4
+ Summary: Official Python SDK for the Pilot Status public API.
5
+ Author: Pilot Status
6
+ License: MIT
7
+ Project-URL: Homepage, https://pilotstatus.online
8
+ Project-URL: Repository, https://github.com/pilot-status/pilot-status
9
+ Project-URL: Issues, https://github.com/pilot-status/pilot-status/issues
10
+ Keywords: pilot-status,whatsapp,api,sdk,python
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: typing-extensions>=4.8.0
20
+
21
+ # pilot-status (Python SDK)
22
+
23
+ Official Python SDK for the Pilot Status public API.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install pilot-status
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ Create an API key in the dashboard and use it only on the backend.
34
+
35
+ ```python
36
+ import os
37
+
38
+ from pilot_status import PilotStatusClient
39
+
40
+ client = PilotStatusClient(
41
+ api_key=os.environ["PILOT_STATUS_API_KEY"],
42
+ )
43
+
44
+ accepted = client.messages.send(
45
+ {
46
+ "templateId": "onboarding-test",
47
+ "destinationNumber": "+5511999999999",
48
+ "variables": {"name": "John"},
49
+ }
50
+ )
51
+
52
+ message = client.messages.get(accepted["id"])
53
+ print(message["status"])
54
+ ```
55
+
56
+ ## Management (projects, API keys, numbers)
57
+
58
+ These endpoints create resources within the scope (project + environment) of the current `api_key`.
59
+
60
+ ### Projects
61
+
62
+ ```python
63
+ project = client.projects.create(
64
+ {
65
+ "name": "My Project",
66
+ "description": "Optional description",
67
+ }
68
+ )
69
+
70
+ projects = client.projects.list()
71
+ ```
72
+
73
+ ### API keys
74
+
75
+ ```python
76
+ key = client.api_keys.create(
77
+ {
78
+ "name": "Backend Key",
79
+ "retentionDays": 30,
80
+ }
81
+ )
82
+
83
+ keys = client.api_keys.list()
84
+ ```
85
+
86
+ ### Numbers (WhatsApp)
87
+
88
+ ```python
89
+ created = client.numbers.create(
90
+ {
91
+ "name": "My WhatsApp",
92
+ "number": "+5511999999999",
93
+ }
94
+ )
95
+ # created["qrcodeBase64"], created["pairingCode"] (letter code or None)
96
+
97
+ refreshed = client.numbers.connect(created["instance"]["id"])
98
+ # refreshed["qrcodeBase64"], refreshed["pairingCode"]
99
+
100
+ status = client.numbers.get_status(created["instance"]["id"])
101
+ print(status["state"])
102
+ ```
103
+
104
+ ## Opt-in check (destination authorization)
105
+
106
+ In LIVE, sending may require opt-in when using the Pilot Status WhatsApp number. You can check whether a destination is already authorized for your project:
107
+
108
+ ```python
109
+ opt_in = client.messages.check_opt_in("+5511999999999")
110
+ if not opt_in["authorized"]:
111
+ raise Exception(f"Missing opt-in: {opt_in['reason']}")
112
+ ```
113
+
114
+ ## Analytics
115
+
116
+ ```python
117
+ stats = client.analytics.get_dashboard_stats(tz="America/Sao_Paulo")
118
+ print(stats["totalSent"], stats["failureRate"])
119
+ ```
120
+
121
+ ## Webhooks (parse / validation)
122
+
123
+ ```python
124
+ from pilot_status import parse_customer_webhook
125
+
126
+ def handler(payload: dict):
127
+ event = parse_customer_webhook(payload)
128
+
129
+ if event["event"] == "message.failed":
130
+ print(event["data"]["errorMessage"])
131
+ ```
132
+
133
+ Notes:
134
+ - Customer webhook payloads do not include: `projectSlug`, `lastMessageId`. Optional `correlationId` (same as HTTP 202 when present) may appear on outbound status events and on `message.reply` / `message.received` when correlated to a prior send.
135
+ - `message.received` includes `fromMe` (boolean).
136
+ - `message.group` is delivered for inbound group messages (includes `groupName`).
137
+ - Supported events in the parser: `message.sent`, `message.delivered`, `message.read`, `message.failed`, `message.reply`, `message.received`, `message.group`, `optin.created`.
@@ -0,0 +1,117 @@
1
+ # pilot-status (Python SDK)
2
+
3
+ Official Python SDK for the Pilot Status public API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pilot-status
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ Create an API key in the dashboard and use it only on the backend.
14
+
15
+ ```python
16
+ import os
17
+
18
+ from pilot_status import PilotStatusClient
19
+
20
+ client = PilotStatusClient(
21
+ api_key=os.environ["PILOT_STATUS_API_KEY"],
22
+ )
23
+
24
+ accepted = client.messages.send(
25
+ {
26
+ "templateId": "onboarding-test",
27
+ "destinationNumber": "+5511999999999",
28
+ "variables": {"name": "John"},
29
+ }
30
+ )
31
+
32
+ message = client.messages.get(accepted["id"])
33
+ print(message["status"])
34
+ ```
35
+
36
+ ## Management (projects, API keys, numbers)
37
+
38
+ These endpoints create resources within the scope (project + environment) of the current `api_key`.
39
+
40
+ ### Projects
41
+
42
+ ```python
43
+ project = client.projects.create(
44
+ {
45
+ "name": "My Project",
46
+ "description": "Optional description",
47
+ }
48
+ )
49
+
50
+ projects = client.projects.list()
51
+ ```
52
+
53
+ ### API keys
54
+
55
+ ```python
56
+ key = client.api_keys.create(
57
+ {
58
+ "name": "Backend Key",
59
+ "retentionDays": 30,
60
+ }
61
+ )
62
+
63
+ keys = client.api_keys.list()
64
+ ```
65
+
66
+ ### Numbers (WhatsApp)
67
+
68
+ ```python
69
+ created = client.numbers.create(
70
+ {
71
+ "name": "My WhatsApp",
72
+ "number": "+5511999999999",
73
+ }
74
+ )
75
+ # created["qrcodeBase64"], created["pairingCode"] (letter code or None)
76
+
77
+ refreshed = client.numbers.connect(created["instance"]["id"])
78
+ # refreshed["qrcodeBase64"], refreshed["pairingCode"]
79
+
80
+ status = client.numbers.get_status(created["instance"]["id"])
81
+ print(status["state"])
82
+ ```
83
+
84
+ ## Opt-in check (destination authorization)
85
+
86
+ In LIVE, sending may require opt-in when using the Pilot Status WhatsApp number. You can check whether a destination is already authorized for your project:
87
+
88
+ ```python
89
+ opt_in = client.messages.check_opt_in("+5511999999999")
90
+ if not opt_in["authorized"]:
91
+ raise Exception(f"Missing opt-in: {opt_in['reason']}")
92
+ ```
93
+
94
+ ## Analytics
95
+
96
+ ```python
97
+ stats = client.analytics.get_dashboard_stats(tz="America/Sao_Paulo")
98
+ print(stats["totalSent"], stats["failureRate"])
99
+ ```
100
+
101
+ ## Webhooks (parse / validation)
102
+
103
+ ```python
104
+ from pilot_status import parse_customer_webhook
105
+
106
+ def handler(payload: dict):
107
+ event = parse_customer_webhook(payload)
108
+
109
+ if event["event"] == "message.failed":
110
+ print(event["data"]["errorMessage"])
111
+ ```
112
+
113
+ Notes:
114
+ - Customer webhook payloads do not include: `projectSlug`, `lastMessageId`. Optional `correlationId` (same as HTTP 202 when present) may appear on outbound status events and on `message.reply` / `message.received` when correlated to a prior send.
115
+ - `message.received` includes `fromMe` (boolean).
116
+ - `message.group` is delivered for inbound group messages (includes `groupName`).
117
+ - Supported events in the parser: `message.sent`, `message.delivered`, `message.read`, `message.failed`, `message.reply`, `message.received`, `message.group`, `optin.created`.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pilot-status"
7
+ version = "0.0.9"
8
+ description = "Official Python SDK for the Pilot Status public API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["typing-extensions>=4.8.0"]
12
+ license = { text = "MIT" }
13
+ authors = [{ name = "Pilot Status" }]
14
+ keywords = ["pilot-status", "whatsapp", "api", "sdk", "python"]
15
+ classifiers = [
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://pilotstatus.online"
26
+ Repository = "https://github.com/pilot-status/pilot-status"
27
+ Issues = "https://github.com/pilot-status/pilot-status/issues"
28
+
29
+ [tool.setuptools]
30
+ package-dir = { "" = "src" }
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,76 @@
1
+ from .client import PilotStatusClient
2
+ from .errors import (
3
+ AuthenticationError,
4
+ ConflictError,
5
+ ForbiddenError,
6
+ NotFoundError,
7
+ PilotStatusError,
8
+ PilotStatusHttpError,
9
+ RateLimitError,
10
+ ServerError,
11
+ ValidationError,
12
+ )
13
+ from .types import (
14
+ ApiKeyCreateInput,
15
+ ApiKeyCreated,
16
+ ApiKeyListItem,
17
+ DashboardStats,
18
+ CreateNumberInput,
19
+ CreateNumberResult,
20
+ CreateProjectInput,
21
+ Environment,
22
+ MessageDetails,
23
+ MessageStatus,
24
+ NumberConnectResult,
25
+ NumberStatusResult,
26
+ OptInCheckResult,
27
+ Project,
28
+ SendMessageAccepted,
29
+ SendMessageInput,
30
+ WhatsAppNumberInstance,
31
+ )
32
+ from .webhooks.parse import is_customer_webhook_event, parse_customer_webhook
33
+ from .webhooks.types import (
34
+ CustomerWebhookEvent,
35
+ MessageFailedEvent,
36
+ MessageReadEvent,
37
+ MessageReplyEvent,
38
+ MessageSentEvent,
39
+ )
40
+
41
+ __all__ = [
42
+ "PilotStatusClient",
43
+ "PilotStatusError",
44
+ "PilotStatusHttpError",
45
+ "AuthenticationError",
46
+ "ValidationError",
47
+ "ForbiddenError",
48
+ "NotFoundError",
49
+ "ConflictError",
50
+ "RateLimitError",
51
+ "ServerError",
52
+ "Environment",
53
+ "MessageStatus",
54
+ "SendMessageInput",
55
+ "SendMessageAccepted",
56
+ "MessageDetails",
57
+ "DashboardStats",
58
+ "OptInCheckResult",
59
+ "Project",
60
+ "CreateProjectInput",
61
+ "ApiKeyCreateInput",
62
+ "ApiKeyCreated",
63
+ "ApiKeyListItem",
64
+ "WhatsAppNumberInstance",
65
+ "CreateNumberInput",
66
+ "CreateNumberResult",
67
+ "NumberConnectResult",
68
+ "NumberStatusResult",
69
+ "CustomerWebhookEvent",
70
+ "MessageSentEvent",
71
+ "MessageReadEvent",
72
+ "MessageFailedEvent",
73
+ "MessageReplyEvent",
74
+ "parse_customer_webhook",
75
+ "is_customer_webhook_event",
76
+ ]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ import urllib.request
6
+
7
+ from .http import HttpClient, HttpClientOptions
8
+ from .resources import AnalyticsResource, ApiKeysResource, MessagesResource, NumbersResource, ProjectsResource
9
+
10
+
11
+ class PilotStatusClient:
12
+ def __init__(
13
+ self,
14
+ *,
15
+ api_key: str,
16
+ timeout_s: float = 30.0,
17
+ user_agent: str | None = None,
18
+ get_retries: int = 2,
19
+ transport: Callable[[urllib.request.Request, float], tuple[int, dict[str, str], str]] | None = None,
20
+ ):
21
+ base_url = "https://pilotstatus.online"
22
+ http = HttpClient(
23
+ HttpClientOptions(
24
+ base_url=base_url,
25
+ api_key=api_key,
26
+ timeout_s=timeout_s,
27
+ user_agent=user_agent,
28
+ get_retries=get_retries,
29
+ transport=transport,
30
+ )
31
+ )
32
+ self.messages = MessagesResource(http)
33
+ self.analytics = AnalyticsResource(http)
34
+ self.projects = ProjectsResource(http)
35
+ self.api_keys = ApiKeysResource(http)
36
+ self.numbers = NumbersResource(http)
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ PilotStatusErrorBody = Any
8
+
9
+
10
+ class PilotStatusError(Exception):
11
+ pass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PilotStatusHttpError(PilotStatusError):
16
+ message: str
17
+ status: int
18
+ method: str
19
+ url: str
20
+ headers: dict[str, str]
21
+ body: PilotStatusErrorBody | None = None
22
+ raw_body: str | None = None
23
+
24
+ def __str__(self) -> str:
25
+ return self.message
26
+
27
+
28
+ class AuthenticationError(PilotStatusHttpError):
29
+ pass
30
+
31
+
32
+ class ValidationError(PilotStatusHttpError):
33
+ pass
34
+
35
+
36
+ class ForbiddenError(PilotStatusHttpError):
37
+ pass
38
+
39
+
40
+ class NotFoundError(PilotStatusHttpError):
41
+ pass
42
+
43
+
44
+ class ConflictError(PilotStatusHttpError):
45
+ pass
46
+
47
+
48
+ class RateLimitError(PilotStatusHttpError):
49
+ pass
50
+
51
+
52
+ class ServerError(PilotStatusHttpError):
53
+ pass
54
+
55
+
56
+ def create_http_error(
57
+ *,
58
+ message: str,
59
+ status: int,
60
+ method: str,
61
+ url: str,
62
+ headers: dict[str, str],
63
+ body: PilotStatusErrorBody | None = None,
64
+ raw_body: str | None = None,
65
+ ) -> PilotStatusHttpError:
66
+ if status == 401:
67
+ return AuthenticationError(message, status, method, url, headers, body, raw_body)
68
+ if status == 400:
69
+ return ValidationError(message, status, method, url, headers, body, raw_body)
70
+ if status == 403:
71
+ return ForbiddenError(message, status, method, url, headers, body, raw_body)
72
+ if status == 404:
73
+ return NotFoundError(message, status, method, url, headers, body, raw_body)
74
+ if status == 409:
75
+ return ConflictError(message, status, method, url, headers, body, raw_body)
76
+ if status == 429:
77
+ return RateLimitError(message, status, method, url, headers, body, raw_body)
78
+ if status >= 500:
79
+ return ServerError(message, status, method, url, headers, body, raw_body)
80
+ return PilotStatusHttpError(message, status, method, url, headers, body, raw_body)
81
+
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+ from dataclasses import dataclass
9
+ from typing import Any, Callable, Mapping
10
+
11
+ from .errors import create_http_error
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class HttpClientOptions:
16
+ base_url: str
17
+ api_key: str
18
+ timeout_s: float
19
+ user_agent: str | None
20
+ get_retries: int
21
+ transport: Callable[[urllib.request.Request, float], tuple[int, Mapping[str, str], str]] | None = None
22
+
23
+
24
+ def _normalize_base_url(base_url: str) -> str:
25
+ return base_url.rstrip("/")
26
+
27
+
28
+ def _join_url(base_url: str, path: str) -> str:
29
+ base = _normalize_base_url(base_url)
30
+ if not path.startswith("/"):
31
+ return f"{base}/{path}"
32
+ return f"{base}{path}"
33
+
34
+
35
+ def _build_url(
36
+ base_url: str,
37
+ path: str,
38
+ query: Mapping[str, str | int | float | bool | None] | None,
39
+ ) -> str:
40
+ url = _join_url(base_url, path)
41
+ if not query:
42
+ return url
43
+ parts = list(urllib.parse.urlsplit(url))
44
+ current = urllib.parse.parse_qsl(parts[3], keep_blank_values=True)
45
+ for k, v in query.items():
46
+ if v is None:
47
+ continue
48
+ current.append((k, str(v)))
49
+ parts[3] = urllib.parse.urlencode(current)
50
+ return urllib.parse.urlunsplit(parts)
51
+
52
+
53
+ def _parse_json_if_possible(text: str) -> Any:
54
+ trimmed = text.strip()
55
+ if not trimmed:
56
+ return None
57
+ try:
58
+ return json.loads(trimmed)
59
+ except Exception:
60
+ return None
61
+
62
+
63
+ def _default_transport(req: urllib.request.Request, timeout_s: float) -> tuple[int, Mapping[str, str], str]:
64
+ with urllib.request.urlopen(req, timeout=timeout_s) as res:
65
+ status = int(getattr(res, "status", 200))
66
+ headers = {k.lower(): v for k, v in dict(res.headers).items()}
67
+ raw = res.read().decode("utf-8", errors="replace")
68
+ return status, headers, raw
69
+
70
+
71
+ def _should_retry(method: str, status: int | None) -> bool:
72
+ if method != "GET":
73
+ return False
74
+ if status is None:
75
+ return True
76
+ return status >= 500
77
+
78
+
79
+ class HttpClient:
80
+ def __init__(self, options: HttpClientOptions):
81
+ self._base_url = _normalize_base_url(options.base_url)
82
+ self._api_key = options.api_key
83
+ self._timeout_s = options.timeout_s
84
+ self._user_agent = options.user_agent
85
+ self._get_retries = options.get_retries
86
+ self._transport = options.transport or _default_transport
87
+
88
+ def request_json(
89
+ self,
90
+ *,
91
+ method: str,
92
+ path: str,
93
+ query: Mapping[str, str | int | float | bool | None] | None = None,
94
+ body: Any | None = None,
95
+ ) -> Any:
96
+ url = _build_url(self._base_url, path, query)
97
+
98
+ headers: dict[str, str] = {
99
+ "x-api-key": self._api_key,
100
+ "accept": "application/json",
101
+ }
102
+ if self._user_agent:
103
+ headers["user-agent"] = self._user_agent
104
+
105
+ data: bytes | None = None
106
+ if body is not None:
107
+ headers["content-type"] = "application/json"
108
+ data = json.dumps(body).encode("utf-8")
109
+
110
+ req = urllib.request.Request(url=url, data=data, method=method, headers=headers)
111
+
112
+ attempt = 0
113
+ while True:
114
+ status: int | None = None
115
+ response_headers: Mapping[str, str] = {}
116
+ raw: str = ""
117
+ try:
118
+ status, response_headers, raw = self._transport(req, self._timeout_s)
119
+ except urllib.error.HTTPError as e:
120
+ status = int(getattr(e, "code", 0) or 0) or None
121
+ response_headers = {k.lower(): v for k, v in dict(getattr(e, "headers", {}) or {}).items()}
122
+ raw = (e.read() or b"").decode("utf-8", errors="replace")
123
+ except urllib.error.URLError as e:
124
+ if attempt >= self._get_retries or not _should_retry(method, None):
125
+ raise e
126
+ backoff_ms = 250 * (2**attempt)
127
+ time.sleep(backoff_ms / 1000.0)
128
+ attempt += 1
129
+ continue
130
+
131
+ parsed = _parse_json_if_possible(raw)
132
+
133
+ if status is None or not (200 <= status < 300):
134
+ message = None
135
+ if isinstance(parsed, dict) and isinstance(parsed.get("error"), str):
136
+ message = str(parsed["error"])
137
+ if not message:
138
+ message = f"HTTP {status}" if status is not None else "HTTP error"
139
+ err = create_http_error(
140
+ message=message,
141
+ status=int(status or 0),
142
+ method=method,
143
+ url=url,
144
+ headers=dict(response_headers),
145
+ body=parsed,
146
+ raw_body=raw or None,
147
+ )
148
+ if attempt < self._get_retries and _should_retry(method, err.status):
149
+ backoff_ms = 250 * (2**attempt)
150
+ time.sleep(backoff_ms / 1000.0)
151
+ attempt += 1
152
+ continue
153
+ raise err
154
+
155
+ return parsed
156
+
@@ -0,0 +1,13 @@
1
+ from .analytics import AnalyticsResource
2
+ from .api_keys import ApiKeysResource
3
+ from .messages import MessagesResource
4
+ from .numbers import NumbersResource
5
+ from .projects import ProjectsResource
6
+
7
+ __all__ = [
8
+ "MessagesResource",
9
+ "AnalyticsResource",
10
+ "ProjectsResource",
11
+ "ApiKeysResource",
12
+ "NumbersResource",
13
+ ]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from ..http import HttpClient
4
+ from ..types import DashboardStats
5
+
6
+
7
+ class AnalyticsResource:
8
+ def __init__(self, http: HttpClient):
9
+ self._http = http
10
+
11
+ def get_dashboard_stats(self, *, tz: str | None = None) -> DashboardStats:
12
+ query = None if tz is None else {"tz": tz}
13
+ return self._http.request_json(method="GET", path="/v1/analytics/dashboard", query=query)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..http import HttpClient
6
+ from ..types import ApiKeyCreateInput, ApiKeyCreated, ApiKeyListItem
7
+
8
+
9
+ class ApiKeysResource:
10
+ def __init__(self, http: HttpClient):
11
+ self._http = http
12
+
13
+ def list(self) -> list[ApiKeyListItem]:
14
+ return self._http.request_json(method="GET", path="/v1/api-keys")
15
+
16
+ def create(self, input: ApiKeyCreateInput) -> ApiKeyCreated:
17
+ body: dict[str, Any] = {"name": input["name"]}
18
+ if "webhookId" in input:
19
+ body["webhookId"] = input["webhookId"]
20
+ if "whatsappInstanceId" in input:
21
+ body["whatsappInstanceId"] = input["whatsappInstanceId"]
22
+ if "retentionDays" in input:
23
+ body["retentionDays"] = input["retentionDays"]
24
+ return self._http.request_json(method="POST", path="/v1/api-keys", body=body)