Skip to content

Commit

Permalink
[webhooks] add webhooks API support
Browse files Browse the repository at this point in the history
  • Loading branch information
capcom6 committed Jan 20, 2025
1 parent fb18eed commit e31dcdc
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 21 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Optional:
pip install android_sms_gateway
```

You can also install with preferred http client:
You can also install it with the preferred HTTP client:

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

### Methods

There are two methods:
There are two groups of methods:

**Messages**

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

**Webhooks**

- `get_webhooks() -> list[domain.Webhook]`: Retrieve a list of all webhooks registered for the account.
- `create_webhook(webhook: domain.Webhook) -> domain.Webhook`: Create a new webhook.
- `delete_webhook(_id: str)`: Delete a webhook by its ID.

## HTTP Client

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:
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:

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

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

# Contributing

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

# License

This library is open-sourced software licensed under the [Apache-2.0 license](LICENSE).
This library is open-sourced software licensed under the [Apache-2.0 license](LICENSE).
63 changes: 57 additions & 6 deletions android_sms_gateway/ahttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ async def post(
self, url: str, payload: dict, *, headers: t.Optional[t.Dict[str, str]] = None
) -> dict: ...

@abc.abstractmethod
async def delete(
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
) -> None:
"""
Sends a DELETE request to the specified URL.
Args:
url: The URL to send the DELETE request to.
headers: Optional dictionary of HTTP headers to send with the request.
Returns:
None
"""

async def __aenter__(self):
pass

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

async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._session is None:
return

await self._session.close()
self._session = None

async def get(
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
) -> dict:
response = await self._session.get(url, headers=headers)
response.raise_for_status()
if self._session is None:
raise ValueError("Session not initialized")

return await response.json()
async with self._session.get(url, headers=headers) as response:
response.raise_for_status()
return await response.json()

async def post(
self,
Expand All @@ -57,10 +77,23 @@ async def post(
*,
headers: t.Optional[t.Dict[str, str]] = None,
) -> dict:
response = await self._session.post(url, headers=headers, json=payload)
response.raise_for_status()
if self._session is None:
raise ValueError("Session not initialized")

async with self._session.post(
url, headers=headers, json=payload
) as response:
response.raise_for_status()
return await response.json()

return await response.json()
async def delete(
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
) -> None:
if self._session is None:
raise ValueError("Session not initialized")

async with self._session.delete(url, headers=headers) as response:
response.raise_for_status()

DEFAULT_CLIENT = AiohttpAsyncHttpClient
except ImportError:
Expand All @@ -82,12 +115,18 @@ async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._client is None:
return

await self._client.aclose()
self._client = None

async def get(
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
) -> dict:
if self._client is None:
raise ValueError("Client not initialized")

response = await self._client.get(url, headers=headers)

return response.raise_for_status().json()
Expand All @@ -99,10 +138,22 @@ async def post(
*,
headers: t.Optional[t.Dict[str, str]] = None,
) -> dict:
if self._client is None:
raise ValueError("Client not initialized")

response = await self._client.post(url, headers=headers, json=payload)

return response.raise_for_status().json()

async def delete(
self, url: str, *, headers: t.Optional[t.Dict[str, str]] = None
) -> None:
if self._client is None:
raise ValueError("Client not initialized")

response = await self._client.delete(url, headers=headers)
response.raise_for_status()

DEFAULT_CLIENT = HttpxAsyncHttpClient
except ImportError:
pass
Expand Down
122 changes: 114 additions & 8 deletions android_sms_gateway/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,27 @@ def __init__(
) -> None:
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
self.http = http
self.default_http = None

def __enter__(self):
if self.http is not None:
raise ValueError("HTTP client already initialized")
return self

self.http = http.get_client().__enter__()
self.http = self.default_http = http.get_client().__enter__()

return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.http.__exit__(exc_type, exc_val, exc_tb)
self.http = None
if self.default_http is None:
return

self.default_http.__exit__(exc_type, exc_val, exc_tb)
self.http = self.default_http = None

def send(self, message: domain.Message) -> domain.MessageState:
if self.http is None:
raise ValueError("HTTP client not initialized")

message = self._encrypt(message)
return self._decrypt(
domain.MessageState.from_dict(
Expand All @@ -108,13 +115,25 @@ def send(self, message: domain.Message) -> domain.MessageState:
)

def get_state(self, _id: str) -> domain.MessageState:
if self.http is None:
raise ValueError("HTTP client not initialized")

return self._decrypt(
domain.MessageState.from_dict(
self.http.get(f"{self.base_url}/message/{_id}", headers=self.headers)
)
)

def get_webhooks(self) -> t.List[domain.Webhook]:
"""
Retrieves a list of all webhooks registered for the account.
Returns:
A list of Webhook instances.
"""
if self.http is None:
raise ValueError("HTTP client not initialized")

return [
domain.Webhook.from_dict(webhook)
for webhook in self.http.get(
Expand All @@ -123,6 +142,18 @@ def get_webhooks(self) -> t.List[domain.Webhook]:
]

def create_webhook(self, webhook: domain.Webhook) -> domain.Webhook:
"""
Creates a new webhook.
Args:
webhook: The webhook to create.
Returns:
The created webhook.
"""
if self.http is None:
raise ValueError("HTTP client not initialized")

return domain.Webhook.from_dict(
self.http.post(
f"{self.base_url}/webhooks",
Expand All @@ -132,6 +163,18 @@ def create_webhook(self, webhook: domain.Webhook) -> domain.Webhook:
)

def delete_webhook(self, _id: str) -> None:
"""
Deletes a webhook.
Args:
_id: The ID of the webhook to delete.
Returns:
None
"""
if self.http is None:
raise ValueError("HTTP client not initialized")

self.http.delete(f"{self.base_url}/webhooks/{_id}", headers=self.headers)


Expand All @@ -147,20 +190,27 @@ def __init__(
) -> None:
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
self.http = http_client
self.default_http = None

async def __aenter__(self):
if self.http is not None:
raise ValueError("HTTP client already initialized")
return self

self.http = await ahttp.get_client().__aenter__()
self.http = self.default_http = await ahttp.get_client().__aenter__()

return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.http.__aexit__(exc_type, exc_val, exc_tb)
self.http = None
if self.default_http is None:
return

await self.default_http.__aexit__(exc_type, exc_val, exc_tb)
self.http = self.default_http = None

async def send(self, message: domain.Message) -> domain.MessageState:
if self.http is None:
raise ValueError("HTTP client not initialized")

message = self._encrypt(message)
return self._decrypt(
domain.MessageState.from_dict(
Expand All @@ -173,10 +223,66 @@ async def send(self, message: domain.Message) -> domain.MessageState:
)

async def get_state(self, _id: str) -> domain.MessageState:
if self.http is None:
raise ValueError("HTTP client not initialized")

return self._decrypt(
domain.MessageState.from_dict(
await self.http.get(
f"{self.base_url}/message/{_id}", headers=self.headers
)
)
)

async def get_webhooks(self) -> t.List[domain.Webhook]:
"""
Retrieves a list of all webhooks registered for the account.
Returns:
A list of Webhook instances.
"""
if self.http is None:
raise ValueError("HTTP client not initialized")

return [
domain.Webhook.from_dict(webhook)
for webhook in await self.http.get(
f"{self.base_url}/webhooks", headers=self.headers
)
]

async def create_webhook(self, webhook: domain.Webhook) -> domain.Webhook:
"""
Creates a new webhook.
Args:
webhook: The webhook to create.
Returns:
The created webhook.
"""
if self.http is None:
raise ValueError("HTTP client not initialized")

return domain.Webhook.from_dict(
await self.http.post(
f"{self.base_url}/webhooks",
payload=webhook.asdict(),
headers=self.headers,
)
)

async def delete_webhook(self, _id: str) -> None:
"""
Deletes a webhook.
Args:
_id: The ID of the webhook to delete.
Returns:
None
"""
if self.http is None:
raise ValueError("HTTP client not initialized")

await self.http.delete(f"{self.base_url}/webhooks/{_id}", headers=self.headers)
2 changes: 1 addition & 1 deletion android_sms_gateway/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def from_dict(cls, payload: t.Dict[str, t.Any]) -> "Webhook":
A Webhook instance.
"""
return cls(
id=payload["id"],
id=payload.get("id"),
url=payload["url"],
event=WebhookEvent(payload["event"]),
)
Expand Down
Loading

0 comments on commit e31dcdc

Please sign in to comment.