pilot-status 0.0.9__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.
- pilot_status/__init__.py +76 -0
- pilot_status/client.py +36 -0
- pilot_status/errors.py +81 -0
- pilot_status/http.py +156 -0
- pilot_status/resources/__init__.py +13 -0
- pilot_status/resources/analytics.py +13 -0
- pilot_status/resources/api_keys.py +24 -0
- pilot_status/resources/messages.py +33 -0
- pilot_status/resources/numbers.py +37 -0
- pilot_status/resources/projects.py +20 -0
- pilot_status/types.py +180 -0
- pilot_status/webhooks/__init__.py +19 -0
- pilot_status/webhooks/parse.py +255 -0
- pilot_status/webhooks/types.py +245 -0
- pilot_status-0.0.9.dist-info/METADATA +137 -0
- pilot_status-0.0.9.dist-info/RECORD +29 -0
- pilot_status-0.0.9.dist-info/WHEEL +5 -0
- pilot_status-0.0.9.dist-info/top_level.txt +2 -0
- pilot_status_sdk/__init__.py +56 -0
- pilot_status_sdk/client.py +36 -0
- pilot_status_sdk/errors.py +81 -0
- pilot_status_sdk/http.py +156 -0
- pilot_status_sdk/resources/__init__.py +5 -0
- pilot_status_sdk/resources/analytics.py +13 -0
- pilot_status_sdk/resources/messages.py +22 -0
- pilot_status_sdk/types.py +58 -0
- pilot_status_sdk/webhooks/__init__.py +19 -0
- pilot_status_sdk/webhooks/parse.py +241 -0
- pilot_status_sdk/webhooks/types.py +227 -0
pilot_status/__init__.py
ADDED
|
@@ -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
|
+
]
|
pilot_status/client.py
ADDED
|
@@ -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)
|
pilot_status/errors.py
ADDED
|
@@ -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
|
+
|
pilot_status/http.py
ADDED
|
@@ -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)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..http import HttpClient
|
|
6
|
+
from ..types import MessageDetails, OptInCheckResult, SendMessageAccepted, SendMessageInput
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessagesResource:
|
|
10
|
+
def __init__(self, http: HttpClient):
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
def send(self, input: SendMessageInput) -> SendMessageAccepted:
|
|
14
|
+
body: dict[str, Any] = {
|
|
15
|
+
"templateId": input["templateId"],
|
|
16
|
+
"destinationNumber": input["destinationNumber"],
|
|
17
|
+
"variables": input.get("variables") or {},
|
|
18
|
+
}
|
|
19
|
+
if "deliverAt" in input:
|
|
20
|
+
body["deliverAt"] = input["deliverAt"]
|
|
21
|
+
if "deliverUntil" in input:
|
|
22
|
+
body["deliverUntil"] = input["deliverUntil"]
|
|
23
|
+
return self._http.request_json(method="POST", path="/v1/messages/send", body=body)
|
|
24
|
+
|
|
25
|
+
def get(self, id: str) -> MessageDetails:
|
|
26
|
+
return self._http.request_json(method="GET", path=f"/v1/messages/{id}")
|
|
27
|
+
|
|
28
|
+
def check_opt_in(self, destination_number: str) -> OptInCheckResult:
|
|
29
|
+
return self._http.request_json(
|
|
30
|
+
method="GET",
|
|
31
|
+
path="/v1/messages/opt-in",
|
|
32
|
+
query={"destinationNumber": destination_number},
|
|
33
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..http import HttpClient
|
|
6
|
+
from ..types import (
|
|
7
|
+
CreateNumberInput,
|
|
8
|
+
CreateNumberResult,
|
|
9
|
+
NumberConnectResult,
|
|
10
|
+
NumberStatusResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NumbersResource:
|
|
15
|
+
def __init__(self, http: HttpClient):
|
|
16
|
+
self._http = http
|
|
17
|
+
|
|
18
|
+
def create(self, input: CreateNumberInput) -> CreateNumberResult:
|
|
19
|
+
body: dict[str, Any] = {
|
|
20
|
+
"name": input["name"],
|
|
21
|
+
"number": input["number"],
|
|
22
|
+
}
|
|
23
|
+
if "linkToApiKey" in input:
|
|
24
|
+
body["linkToApiKey"] = input["linkToApiKey"]
|
|
25
|
+
return self._http.request_json(method="POST", path="/v1/numbers", body=body)
|
|
26
|
+
|
|
27
|
+
def connect(self, id: str) -> NumberConnectResult:
|
|
28
|
+
return self._http.request_json(
|
|
29
|
+
method="GET",
|
|
30
|
+
path=f"/v1/numbers/{id}/connect",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def get_status(self, id: str) -> NumberStatusResult:
|
|
34
|
+
return self._http.request_json(
|
|
35
|
+
method="GET",
|
|
36
|
+
path=f"/v1/numbers/{id}/status",
|
|
37
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..http import HttpClient
|
|
6
|
+
from ..types import CreateProjectInput, Project
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectsResource:
|
|
10
|
+
def __init__(self, http: HttpClient):
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
def list(self) -> list[Project]:
|
|
14
|
+
return self._http.request_json(method="GET", path="/v1/projects")
|
|
15
|
+
|
|
16
|
+
def create(self, input: CreateProjectInput) -> Project:
|
|
17
|
+
body: dict[str, Any] = {"name": input["name"]}
|
|
18
|
+
if "description" in input:
|
|
19
|
+
body["description"] = input["description"]
|
|
20
|
+
return self._http.request_json(method="POST", path="/v1/projects", body=body)
|
pilot_status/types.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
Environment = Literal["TEST", "LIVE"]
|
|
6
|
+
|
|
7
|
+
MessageStatus = Literal["QUEUED", "SENT", "DELIVERED", "FAILED", "READ"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SendMessageInput(TypedDict, total=False):
|
|
11
|
+
templateId: str
|
|
12
|
+
destinationNumber: str
|
|
13
|
+
groupId: str
|
|
14
|
+
variables: dict[str, str]
|
|
15
|
+
labels: list[str]
|
|
16
|
+
deliverAt: str
|
|
17
|
+
deliverUntil: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SendMessageAccepted(TypedDict):
|
|
21
|
+
id: str
|
|
22
|
+
correlationId: str
|
|
23
|
+
status: MessageStatus
|
|
24
|
+
createdAt: str
|
|
25
|
+
origin: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MessageDetails(TypedDict):
|
|
29
|
+
id: str
|
|
30
|
+
status: MessageStatus
|
|
31
|
+
correlationId: str
|
|
32
|
+
destinationNumber: str
|
|
33
|
+
template: str
|
|
34
|
+
createdAt: str
|
|
35
|
+
sentAt: str | None
|
|
36
|
+
deliveredAt: str | None
|
|
37
|
+
readAt: str | None
|
|
38
|
+
errorMessage: str | None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DashboardDailyMessages(TypedDict):
|
|
42
|
+
date: str
|
|
43
|
+
count: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DashboardStatusDistribution(TypedDict):
|
|
47
|
+
queued: int
|
|
48
|
+
sent: int
|
|
49
|
+
delivered: int
|
|
50
|
+
failed: int
|
|
51
|
+
read: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DashboardStats(TypedDict):
|
|
55
|
+
totalSent: int
|
|
56
|
+
totalFailed: int
|
|
57
|
+
sentPercentChange: float
|
|
58
|
+
failedPercentChange: float
|
|
59
|
+
failureRate: float
|
|
60
|
+
dailyMessages: list[DashboardDailyMessages]
|
|
61
|
+
statusDistribution: DashboardStatusDistribution
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
OptInCheckReason = Literal[
|
|
65
|
+
"OPTIN_DISABLED",
|
|
66
|
+
"NON_DEFAULT_INSTANCE",
|
|
67
|
+
"NOT_LIVE",
|
|
68
|
+
"MISSING_PROJECT",
|
|
69
|
+
"INVALID_PHONE",
|
|
70
|
+
"NO_OPTIN",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OptInCheckResult(TypedDict):
|
|
75
|
+
authorized: bool
|
|
76
|
+
required: bool
|
|
77
|
+
enabled: bool
|
|
78
|
+
reason: OptInCheckReason | None
|
|
79
|
+
firstOptInAt: str | None
|
|
80
|
+
lastSeenAt: str | None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Project(TypedDict):
|
|
84
|
+
id: str
|
|
85
|
+
name: str
|
|
86
|
+
slug: str
|
|
87
|
+
description: str
|
|
88
|
+
productionApproved: bool
|
|
89
|
+
createdAt: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CreateProjectInput(TypedDict, total=False):
|
|
93
|
+
name: str
|
|
94
|
+
description: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ApiKeyCreateInput(TypedDict, total=False):
|
|
98
|
+
name: str
|
|
99
|
+
webhookId: str | None
|
|
100
|
+
whatsappInstanceId: str | None
|
|
101
|
+
retentionDays: int
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ApiKeyCreated(TypedDict):
|
|
105
|
+
id: str
|
|
106
|
+
name: str
|
|
107
|
+
environment: Environment
|
|
108
|
+
key: str
|
|
109
|
+
keyPrefix: str
|
|
110
|
+
createdAt: str
|
|
111
|
+
webhookId: str | None
|
|
112
|
+
whatsappInstanceId: str | None
|
|
113
|
+
retentionDays: int
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ApiKeyLinkedWebhook(TypedDict):
|
|
117
|
+
id: str
|
|
118
|
+
name: str
|
|
119
|
+
url: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ApiKeyLinkedWhatsAppInstance(TypedDict):
|
|
123
|
+
id: str
|
|
124
|
+
instanceName: str
|
|
125
|
+
displayName: str
|
|
126
|
+
number: str
|
|
127
|
+
state: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ApiKeyListItem(TypedDict):
|
|
131
|
+
id: str
|
|
132
|
+
name: str
|
|
133
|
+
keyPrefix: str
|
|
134
|
+
environment: Environment
|
|
135
|
+
lastUsedAt: str | None
|
|
136
|
+
createdAt: str
|
|
137
|
+
webhookId: str | None
|
|
138
|
+
projectId: str | None
|
|
139
|
+
webhook: ApiKeyLinkedWebhook | None
|
|
140
|
+
whatsappInstanceId: str | None
|
|
141
|
+
whatsappInstance: ApiKeyLinkedWhatsAppInstance | None
|
|
142
|
+
retentionDays: int
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class WhatsAppNumberInstance(TypedDict):
|
|
146
|
+
id: str
|
|
147
|
+
tenantId: str
|
|
148
|
+
instanceName: str
|
|
149
|
+
number: str
|
|
150
|
+
displayName: str
|
|
151
|
+
state: str
|
|
152
|
+
integration: str
|
|
153
|
+
createdAt: str
|
|
154
|
+
updatedAt: str
|
|
155
|
+
name: str
|
|
156
|
+
status: str
|
|
157
|
+
quality: Literal["GOOD", "UNKNOWN"]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class CreateNumberInput(TypedDict, total=False):
|
|
161
|
+
name: str
|
|
162
|
+
number: str
|
|
163
|
+
linkToApiKey: bool
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class CreateNumberResult(TypedDict):
|
|
167
|
+
instance: WhatsAppNumberInstance
|
|
168
|
+
qrcodeBase64: str | None
|
|
169
|
+
pairingCode: str | None
|
|
170
|
+
linkedApiKeyId: str | None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class NumberConnectResult(TypedDict):
|
|
174
|
+
qrcodeBase64: str | None
|
|
175
|
+
pairingCode: str | None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class NumberStatusResult(TypedDict):
|
|
179
|
+
id: str
|
|
180
|
+
state: str
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .parse import is_customer_webhook_event, parse_customer_webhook
|
|
2
|
+
from .types import (
|
|
3
|
+
CustomerWebhookEvent,
|
|
4
|
+
MessageFailedEvent,
|
|
5
|
+
MessageReadEvent,
|
|
6
|
+
MessageReplyEvent,
|
|
7
|
+
MessageSentEvent,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CustomerWebhookEvent",
|
|
12
|
+
"MessageSentEvent",
|
|
13
|
+
"MessageReadEvent",
|
|
14
|
+
"MessageFailedEvent",
|
|
15
|
+
"MessageReplyEvent",
|
|
16
|
+
"parse_customer_webhook",
|
|
17
|
+
"is_customer_webhook_event",
|
|
18
|
+
]
|
|
19
|
+
|