Skip to content

Commit e31dcdc

Browse files
committed
[webhooks] add webhooks API support
1 parent fb18eed commit e31dcdc

File tree

7 files changed

+338
-21
lines changed

7 files changed

+338
-21
lines changed

README.md

+13-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Optional:
2828
pip install android_sms_gateway
2929
```
3030

31-
You can also install with preferred http client:
31+
You can also install it with the preferred HTTP client:
3232

3333
```bash
3434
pip install android_sms_gateway[requests]
@@ -101,25 +101,33 @@ implement the same interface and can be used as context managers.
101101

102102
### Methods
103103

104-
There are two methods:
104+
There are two groups of methods:
105+
106+
**Messages**
105107

106108
- `send(message: domain.Message) -> domain.MessageState`: Send a new SMS message.
107109
- `get_state(_id: str) -> domain.MessageState`: Retrieve the state of a previously sent message by its ID.
108110

111+
**Webhooks**
112+
113+
- `get_webhooks() -> list[domain.Webhook]`: Retrieve a list of all webhooks registered for the account.
114+
- `create_webhook(webhook: domain.Webhook) -> domain.Webhook`: Create a new webhook.
115+
- `delete_webhook(_id: str)`: Delete a webhook by its ID.
116+
109117
## HTTP Client
110118

111-
The API clients abstract away the HTTP client used to make requests. The library includes support for some popular HTTP clients and trys to discover them automatically:
119+
The API clients abstract away the HTTP client used to make requests. The library includes support for some popular HTTP clients and tries to discover them automatically:
112120

113121
- [requests](https://pypi.org/project/requests/) - `APIClient` only
114122
- [aiohttp](https://pypi.org/project/aiohttp/) - `AsyncAPIClient` only
115123
- [httpx](https://pypi.org/project/httpx/) - `APIClient` and `AsyncAPIClient`
116124

117-
Also you can implement your own HTTP client that conforms to the `http.HttpClient` or `ahttp.HttpClient` protocol.
125+
You can also implement your own HTTP client that conforms to the `http.HttpClient` or `ahttp.HttpClient` protocol.
118126

119127
# Contributing
120128

121129
Contributions are welcome! Please submit a pull request or create an issue for anything you'd like to add or change.
122130

123131
# License
124132

125-
This library is open-sourced software licensed under the [Apache-2.0 license](LICENSE).
133+
This library is open-sourced software licensed under the [Apache-2.0 license](LICENSE).

android_sms_gateway/ahttp.py

+57-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ async def post(
1313
self, url: str, payload: dict, *, headers: t.Optional[t.Dict[str, str]] = None
1414
) -> dict: ...
1515

16+
@abc.abstractmethod
17+
async def delete(
18+
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
19+
) -> None:
20+
"""
21+
Sends a DELETE request to the specified URL.
22+
23+
Args:
24+
url: The URL to send the DELETE request to.
25+
headers: Optional dictionary of HTTP headers to send with the request.
26+
27+
Returns:
28+
None
29+
"""
30+
1631
async def __aenter__(self):
1732
pass
1833

@@ -39,16 +54,21 @@ async def __aenter__(self):
3954
return self
4055

4156
async def __aexit__(self, exc_type, exc_val, exc_tb):
57+
if self._session is None:
58+
return
59+
4260
await self._session.close()
4361
self._session = None
4462

4563
async def get(
4664
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
4765
) -> dict:
48-
response = await self._session.get(url, headers=headers)
49-
response.raise_for_status()
66+
if self._session is None:
67+
raise ValueError("Session not initialized")
5068

51-
return await response.json()
69+
async with self._session.get(url, headers=headers) as response:
70+
response.raise_for_status()
71+
return await response.json()
5272

5373
async def post(
5474
self,
@@ -57,10 +77,23 @@ async def post(
5777
*,
5878
headers: t.Optional[t.Dict[str, str]] = None,
5979
) -> dict:
60-
response = await self._session.post(url, headers=headers, json=payload)
61-
response.raise_for_status()
80+
if self._session is None:
81+
raise ValueError("Session not initialized")
82+
83+
async with self._session.post(
84+
url, headers=headers, json=payload
85+
) as response:
86+
response.raise_for_status()
87+
return await response.json()
6288

63-
return await response.json()
89+
async def delete(
90+
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
91+
) -> None:
92+
if self._session is None:
93+
raise ValueError("Session not initialized")
94+
95+
async with self._session.delete(url, headers=headers) as response:
96+
response.raise_for_status()
6497

6598
DEFAULT_CLIENT = AiohttpAsyncHttpClient
6699
except ImportError:
@@ -82,12 +115,18 @@ async def __aenter__(self):
82115
return self
83116

84117
async def __aexit__(self, exc_type, exc_val, exc_tb):
118+
if self._client is None:
119+
return
120+
85121
await self._client.aclose()
86122
self._client = None
87123

88124
async def get(
89125
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
90126
) -> dict:
127+
if self._client is None:
128+
raise ValueError("Client not initialized")
129+
91130
response = await self._client.get(url, headers=headers)
92131

93132
return response.raise_for_status().json()
@@ -99,10 +138,22 @@ async def post(
99138
*,
100139
headers: t.Optional[t.Dict[str, str]] = None,
101140
) -> dict:
141+
if self._client is None:
142+
raise ValueError("Client not initialized")
143+
102144
response = await self._client.post(url, headers=headers, json=payload)
103145

104146
return response.raise_for_status().json()
105147

148+
async def delete(
149+
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
150+
) -> None:
151+
if self._client is None:
152+
raise ValueError("Client not initialized")
153+
154+
response = await self._client.delete(url, headers=headers)
155+
response.raise_for_status()
156+
106157
DEFAULT_CLIENT = HttpxAsyncHttpClient
107158
except ImportError:
108159
pass

android_sms_gateway/client.py

+114-8
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,27 @@ def __init__(
8282
) -> None:
8383
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
8484
self.http = http
85+
self.default_http = None
8586

8687
def __enter__(self):
8788
if self.http is not None:
88-
raise ValueError("HTTP client already initialized")
89+
return self
8990

90-
self.http = http.get_client().__enter__()
91+
self.http = self.default_http = http.get_client().__enter__()
9192

9293
return self
9394

9495
def __exit__(self, exc_type, exc_val, exc_tb):
95-
self.http.__exit__(exc_type, exc_val, exc_tb)
96-
self.http = None
96+
if self.default_http is None:
97+
return
98+
99+
self.default_http.__exit__(exc_type, exc_val, exc_tb)
100+
self.http = self.default_http = None
97101

98102
def send(self, message: domain.Message) -> domain.MessageState:
103+
if self.http is None:
104+
raise ValueError("HTTP client not initialized")
105+
99106
message = self._encrypt(message)
100107
return self._decrypt(
101108
domain.MessageState.from_dict(
@@ -108,13 +115,25 @@ def send(self, message: domain.Message) -> domain.MessageState:
108115
)
109116

110117
def get_state(self, _id: str) -> domain.MessageState:
118+
if self.http is None:
119+
raise ValueError("HTTP client not initialized")
120+
111121
return self._decrypt(
112122
domain.MessageState.from_dict(
113123
self.http.get(f"{self.base_url}/message/{_id}", headers=self.headers)
114124
)
115125
)
116126

117127
def get_webhooks(self) -> t.List[domain.Webhook]:
128+
"""
129+
Retrieves a list of all webhooks registered for the account.
130+
131+
Returns:
132+
A list of Webhook instances.
133+
"""
134+
if self.http is None:
135+
raise ValueError("HTTP client not initialized")
136+
118137
return [
119138
domain.Webhook.from_dict(webhook)
120139
for webhook in self.http.get(
@@ -123,6 +142,18 @@ def get_webhooks(self) -> t.List[domain.Webhook]:
123142
]
124143

125144
def create_webhook(self, webhook: domain.Webhook) -> domain.Webhook:
145+
"""
146+
Creates a new webhook.
147+
148+
Args:
149+
webhook: The webhook to create.
150+
151+
Returns:
152+
The created webhook.
153+
"""
154+
if self.http is None:
155+
raise ValueError("HTTP client not initialized")
156+
126157
return domain.Webhook.from_dict(
127158
self.http.post(
128159
f"{self.base_url}/webhooks",
@@ -132,6 +163,18 @@ def create_webhook(self, webhook: domain.Webhook) -> domain.Webhook:
132163
)
133164

134165
def delete_webhook(self, _id: str) -> None:
166+
"""
167+
Deletes a webhook.
168+
169+
Args:
170+
_id: The ID of the webhook to delete.
171+
172+
Returns:
173+
None
174+
"""
175+
if self.http is None:
176+
raise ValueError("HTTP client not initialized")
177+
135178
self.http.delete(f"{self.base_url}/webhooks/{_id}", headers=self.headers)
136179

137180

@@ -147,20 +190,27 @@ def __init__(
147190
) -> None:
148191
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
149192
self.http = http_client
193+
self.default_http = None
150194

151195
async def __aenter__(self):
152196
if self.http is not None:
153-
raise ValueError("HTTP client already initialized")
197+
return self
154198

155-
self.http = await ahttp.get_client().__aenter__()
199+
self.http = self.default_http = await ahttp.get_client().__aenter__()
156200

157201
return self
158202

159203
async def __aexit__(self, exc_type, exc_val, exc_tb):
160-
await self.http.__aexit__(exc_type, exc_val, exc_tb)
161-
self.http = None
204+
if self.default_http is None:
205+
return
206+
207+
await self.default_http.__aexit__(exc_type, exc_val, exc_tb)
208+
self.http = self.default_http = None
162209

163210
async def send(self, message: domain.Message) -> domain.MessageState:
211+
if self.http is None:
212+
raise ValueError("HTTP client not initialized")
213+
164214
message = self._encrypt(message)
165215
return self._decrypt(
166216
domain.MessageState.from_dict(
@@ -173,10 +223,66 @@ async def send(self, message: domain.Message) -> domain.MessageState:
173223
)
174224

175225
async def get_state(self, _id: str) -> domain.MessageState:
226+
if self.http is None:
227+
raise ValueError("HTTP client not initialized")
228+
176229
return self._decrypt(
177230
domain.MessageState.from_dict(
178231
await self.http.get(
179232
f"{self.base_url}/message/{_id}", headers=self.headers
180233
)
181234
)
182235
)
236+
237+
async def get_webhooks(self) -> t.List[domain.Webhook]:
238+
"""
239+
Retrieves a list of all webhooks registered for the account.
240+
241+
Returns:
242+
A list of Webhook instances.
243+
"""
244+
if self.http is None:
245+
raise ValueError("HTTP client not initialized")
246+
247+
return [
248+
domain.Webhook.from_dict(webhook)
249+
for webhook in await self.http.get(
250+
f"{self.base_url}/webhooks", headers=self.headers
251+
)
252+
]
253+
254+
async def create_webhook(self, webhook: domain.Webhook) -> domain.Webhook:
255+
"""
256+
Creates a new webhook.
257+
258+
Args:
259+
webhook: The webhook to create.
260+
261+
Returns:
262+
The created webhook.
263+
"""
264+
if self.http is None:
265+
raise ValueError("HTTP client not initialized")
266+
267+
return domain.Webhook.from_dict(
268+
await self.http.post(
269+
f"{self.base_url}/webhooks",
270+
payload=webhook.asdict(),
271+
headers=self.headers,
272+
)
273+
)
274+
275+
async def delete_webhook(self, _id: str) -> None:
276+
"""
277+
Deletes a webhook.
278+
279+
Args:
280+
_id: The ID of the webhook to delete.
281+
282+
Returns:
283+
None
284+
"""
285+
if self.http is None:
286+
raise ValueError("HTTP client not initialized")
287+
288+
await self.http.delete(f"{self.base_url}/webhooks/{_id}", headers=self.headers)

android_sms_gateway/domain.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def from_dict(cls, payload: t.Dict[str, t.Any]) -> "Webhook":
8787
A Webhook instance.
8888
"""
8989
return cls(
90-
id=payload["id"],
90+
id=payload.get("id"),
9191
url=payload["url"],
9292
event=WebhookEvent(payload["event"]),
9393
)

0 commit comments

Comments
 (0)