Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ VERSION=$(shell grep '__version__' $(PACKAGE_NAME)/__init__.py | cut -d '"' -f 2

# Install pipenv and project dependencies
install:
pipenv install --dev
pipenv install --dev --categories encryption

# Run tests with pytest or unittest
test:
Expand Down
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
142 changes: 134 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,12 +115,68 @@ 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(
f"{self.base_url}/webhooks", headers=self.headers
)
]

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",
payload=webhook.asdict(),
headers=self.headers,
)
)

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)


class AsyncAPIClient(BaseClient):
def __init__(
Expand All @@ -127,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 @@ -153,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)
Loading
Loading