Skip to content

Commit baea017

Browse files
authored
Add sms channel (#65)
* Added tests for send SMS message endpoint * Update after pre-commit hooks * Refactoring of send_message model * Added send binary sms endpoint and tests * Updated README.md * Added send sms message over query parameters method * Added model tests for send sms over query parameters * Added integration tests for send sms over query parameters * added preview sms message endpoint * Added preview sms message tests * Added delivery reports model tests * Added delivery reports integration tests * Added get message logs end point * Added model tests for get message logs * Added integration tests for get message logs * Added tests for get inbound sms messages endpoint * Added get scheduled sms messages endpoint * Added tests for get scheduled sms messages * Added tests for reschedule sms messages * Updated __init__ file with SMSChannel * Added scheduled sms message status endpoint * Added update scheduled sms messages endpoint * Added integrations tests for update scheduled sms messages * added new cls methods to DateTimeValidator class * Changed README.md * Updated pull request
1 parent 6ffd802 commit baea017

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3322
-91
lines changed

README.md

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# infobip-api-python-sdk
2-
Python client for Infobip's API channels.
1+
# Infobip API Python SDK
32

43
[![Version](https://img.shields.io/pypi/v/infobip-api-python-sdk)](https://pypi.org/project/infobip-api-python-sdk/)
54
![Python](https://img.shields.io/pypi/pyversions/infobip-api-python-sdk)
@@ -8,43 +7,52 @@ Python client for Infobip's API channels.
87
[![Licence](https://img.shields.io/github/license/infobip-community/infobip-api-python-sdk)](LICENSE)
98
![Lines](https://img.shields.io/tokei/lines/github/infobip-community/infobip-api-php-sdk)
109

11-
# Supported channels
12-
- Whatsapp -> [Docs](https://www.infobip.com/docs/api#channels/whatsapp)
13-
- WebRTC -> [Docs](https://www.infobip.com/docs/api#channels/webrtc/)
14-
- MMS -> [Docs](https://www.infobip.com/docs/api#channels/mms)
15-
- RCS -> [Docs](https://www.infobip.com/docs/api#channels/rcs)
10+
Python client for Infobip's API channels.
11+
12+
---
1613

17-
#### Table of contents:
14+
## 📡 Supported channels
15+
- [SMS Reference](https://www.infobip.com/docs/api#channels/sms)
16+
- [Whatsapp Reference](https://www.infobip.com/docs/api#channels/whatsapp)
17+
- [WebRTC Reference](https://www.infobip.com/docs/api#channels/webrtc/)
18+
- [MMS Reference](https://www.infobip.com/docs/api#channels/mms)
19+
- [RCS Reference](https://www.infobip.com/docs/api#channels/rcs)
1820

19-
- [General Info](#general-info)
20-
- [License](#license)
21-
- [Installation](#installation)
22-
- [Code example](#code-example)
23-
- [Testing](#testing)
24-
- [Enable pre-commit hooks](#enable-pre-commit-hooks)
21+
More channels to be added in the near future.
2522

26-
## General Info
23+
## ℹ️ General Info
2724

28-
For `infobip-api-python-sdk` versioning we use [Semantic Versioning](https://semver.org) scheme.
25+
For `infobip-api-python-sdk` versioning we use
26+
[Semantic Versioning](https://semver.org) scheme.
2927

3028
Python 3.6 is minimum supported version by this library.
3129

32-
## License
30+
## 🔐 Authentication
3331

34-
Published under [MIT License](LICENSE).
32+
Currently, infobip-api-python-sdk only supports API Key authentication,
33+
and the key needs to be passed during client creation.
34+
This will most likely change with future versions,
35+
once more authentication methods are included.
3536

36-
## Installation
37+
## 📦 Installation
38+
To install infobip SDK you will need to run:
3739

38-
Install the library by using the following command:
3940
```bash
4041
pip install infobip-api-python-sdk
4142
```
4243

43-
## Code Example
44+
Details of the package can be found
45+
[here](https://pypi.org/project/infobip-api-python-sdk/)
46+
47+
## 🚀 Usage
48+
49+
### Code Example
4450
To use the package you'll need an Infobip account.
45-
If you don't already have one, you can create a free trial account [here](https://www.infobip.com/signup).
51+
If you don't already have one, you can create a free trial account
52+
[here](https://www.infobip.com/signup).
4653

4754
In this example we will show how to send WhatsApp text message.
55+
Similar can be done for other channels.
4856
First step is to import necessary channel, in this case WhatsApp channel.
4957

5058
```python
@@ -75,14 +83,14 @@ response = c.send_text_message(
7583
}
7684
)
7785
```
78-
## Testing
86+
## 🧪 Testing
7987
To run tests position yourself in the project's root while your virtual environment
8088
is active and run:
8189
```bash
8290
python -m pytest
8391
```
8492

85-
## Enable pre-commit hooks
93+
## Enable pre-commit hooks
8694
To enable pre-commit hooks run:
8795
```bash
8896
pip install -r requirements/dev.txt
@@ -113,3 +121,7 @@ pre-commit run --all-files
113121
If setup was successful pre-commit will run on every commit.
114122
Every time you clone a project that uses pre-commit, running `pre-commit install`
115123
should be the first thing you do.
124+
125+
## ⚖️ License
126+
127+
This library is distributed under the MIT license found in the [License](LICENSE).

infobip_channels/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .mms.channel import MMSChannel
22
from .rcs.channel import RCSChannel
3+
from .sms.channel import SMSChannel
34
from .web_rtc.channel import WebRtcChannel
45
from .whatsapp.channel import WhatsAppChannel
56

6-
__all__ = ["WhatsAppChannel", "WebRtcChannel", "MMSChannel", "RCSChannel"]
7+
__all__ = ["WhatsAppChannel", "WebRtcChannel", "MMSChannel", "RCSChannel", "SMSChannel"]

infobip_channels/core/channel.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
GetHeaders,
1111
MessageBodyBase,
1212
PostHeaders,
13+
QueryParameter,
1314
ResponseBase,
1415
)
1516
from infobip_channels.web_rtc.models.path_parameters.core import PathParameter
@@ -62,6 +63,24 @@ def from_provided_client(cls, client: Any) -> "Channel":
6263
"""
6364
return cls(client)
6465

66+
@staticmethod
67+
def validate_query_parameter(
68+
parameter: Union[QueryParameter, Dict], parameter_type: Type[QueryParameter]
69+
) -> QueryParameter:
70+
"""
71+
Validate the query parameter by trying to instantiate the provided class.
72+
If the passed parameter is already of that type, just return it as is.
73+
74+
:param parameter: Query parameter to validate
75+
:param parameter_type: Type of the query parameter
76+
:return: Class instance corresponding to the provided parameter type
77+
"""
78+
return (
79+
parameter
80+
if isinstance(parameter, parameter_type)
81+
else parameter_type(**parameter)
82+
)
83+
6584
@staticmethod
6685
def validate_auth_params(
6786
base_url: Union[AnyHttpUrl, str], api_key: str

infobip_channels/core/http_client.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,26 +71,34 @@ def get(
7171
return requests.get(url=url, headers=headers.dict(by_alias=True), params=params)
7272

7373
def put(
74-
self, endpoint: str, body: Dict, headers: RequestHeaders = None
74+
self,
75+
endpoint: str,
76+
body: Dict,
77+
headers: RequestHeaders = None,
78+
params: Dict = None,
7579
) -> requests.Response:
7680
"""Send an HTTP put request to base_url + endpoint.
7781
7882
:param endpoint: Which endpoint to hit
7983
:param headers: Request headers
8084
:param body: Body to send with the request
85+
:param params: Dictionary of query parameters
8186
:return: Received response
8287
"""
8388
headers = headers or self.put_headers
8489
url = self.auth.base_url + endpoint
8590

86-
return requests.put(url=url, json=body, headers=headers.dict(by_alias=True))
91+
return requests.put(
92+
url=url, json=body, headers=headers.dict(by_alias=True), params=params
93+
)
8794

8895
def delete(
8996
self, endpoint: str, headers: RequestHeaders = None
9097
) -> requests.Response:
9198
"""Send an HTTP delete request to base_url + endpoint.
9299
93100
:param endpoint: Which endpoint to hit
101+
:param headers: Request headers
94102
:return: Received response
95103
"""
96104
headers = headers or self.delete_headers

infobip_channels/core/models.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import json
22
import os
3+
import urllib.parse
34
import xml.etree.ElementTree as ET
5+
from datetime import datetime, timedelta
6+
from enum import Enum
47
from http import HTTPStatus
58
from io import IOBase
69
from typing import Any, Dict, List, Optional, Tuple, Union
@@ -19,6 +22,20 @@ def to_header_specific_case(string: str) -> str:
1922
return "-".join(word.capitalize() for word in string.split("_"))
2023

2124

25+
def url_encoding(string_to_encode: str, safe: str = "", encoding: str = "utf-8") -> str:
26+
"""
27+
Special characters and user credentials are properly encoded.
28+
Use a URL encoding reference as a guide:
29+
https://www.w3schools.com/tags/ref_urlencode.asp
30+
31+
The optional safe parameter specifies additional ASCII characters
32+
that should not be quoted — its default value is '/'.
33+
"""
34+
return urllib.parse.quote(
35+
string_to_encode, safe=safe, encoding=encoding, errors=None
36+
)
37+
38+
2239
class Authentication(BaseModel):
2340
base_url: AnyHttpUrl
2441
api_key: constr(min_length=1)
@@ -48,6 +65,71 @@ def validate_url_length(cls, value: str) -> str:
4865
return value
4966

5067

68+
class FromAndToTimeValidator:
69+
_MINIMUM_DELIVERY_WINDOW_MINUTES = 60
70+
71+
@classmethod
72+
def _validate_time_differences(cls, from_time, to_time):
73+
from_time_in_minutes = from_time.hour * 60 + from_time.minute
74+
to_time_in_minutes = to_time.hour * 60 + to_time.minute
75+
76+
if (
77+
to_time_in_minutes - from_time_in_minutes
78+
< cls._MINIMUM_DELIVERY_WINDOW_MINUTES
79+
):
80+
raise ValueError(
81+
f"Minimum of {cls._MINIMUM_DELIVERY_WINDOW_MINUTES} minutes has to pass "
82+
f"between from and to delivery window times."
83+
)
84+
85+
@classmethod
86+
def validate_from_and_to(cls, values):
87+
if not values.get("from_time") and not values.get("to"):
88+
return values
89+
90+
if values.get("from_time") and not values.get("to"):
91+
raise ValueError("If 'from_time' is set, 'to' has to be set also")
92+
93+
if values.get("to") and not values.get("from_time"):
94+
raise ValueError("If 'to' is set, 'from_time' has to be set also")
95+
96+
cls._validate_time_differences(values["from_time"], values["to"])
97+
98+
return values
99+
100+
101+
class DateTimeValidator:
102+
_MAX_TIME_LIMIT = 180
103+
_EXPECTED_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
104+
105+
@classmethod
106+
def convert_to_date_time_format(cls, value):
107+
if not value:
108+
return
109+
if isinstance(value, str):
110+
value = datetime.fromisoformat(value)
111+
112+
return value
113+
114+
@classmethod
115+
def convert_time_to_correct_format(cls, value) -> str:
116+
date_time_format = cls.convert_to_date_time_format(value)
117+
118+
return date_time_format.strftime(cls._EXPECTED_TIME_FORMAT)
119+
120+
@classmethod
121+
def convert_time_to_correct_format_validate_limit(cls, value):
122+
123+
date_time_format = cls.convert_to_date_time_format(value)
124+
125+
if date_time_format > datetime.now() + timedelta(days=cls._MAX_TIME_LIMIT):
126+
raise ValueError(
127+
"Scheduled message must be sooner than 180 days from today"
128+
)
129+
130+
return value.strftime(cls._EXPECTED_TIME_FORMAT)
131+
132+
51133
class CamelCaseModel(BaseModel):
52134
class Config:
53135
alias_generator = to_camel_case
@@ -133,6 +215,41 @@ def validate(cls, value):
133215
return cls(value)
134216

135217

218+
class LanguageEnum(str, Enum):
219+
TURKISH = "TR"
220+
SPANISH = "ES"
221+
PORTUGUESE = "PT"
222+
AUTODETECT = "AUTODETECT"
223+
224+
225+
class TransliterationEnum(str, Enum):
226+
TURKISH = "TURKISH"
227+
GREEK = "GREEK"
228+
CYRILLIC = "CYRILLIC"
229+
SERBIAN_CYRILLIC = "SERBIAN_CYRILLIC"
230+
CENTRAL_EUROPEAN = "CENTRAL_EUROPEAN"
231+
BALTIC = "BALTIC"
232+
NON_UNICODE = "NON_UNICODE"
233+
234+
235+
class GeneralStatus(str, Enum):
236+
ACCEPTED = "ACCEPTED"
237+
PENDING = "PENDING"
238+
UNDELIVERABLE = "UNDELIVERABLE"
239+
DELIVERED = "DELIVERED"
240+
REJECTED = "REJECTED"
241+
EXPIRED = "EXPIRED"
242+
243+
244+
class MessageStatus(str, Enum):
245+
PENDING = "PENDING"
246+
PAUSED = "PAUSED"
247+
PROCESSING = "PROCESSING"
248+
CANCELED = "CANCELED"
249+
FINISHED = "FINISHED"
250+
FAILED = "FAILED"
251+
252+
136253
class MultipartMixin:
137254
"""Mixin used for allowing models to export their fields to a multipart/form-data
138255
format. Field types currently supported are listed in the
@@ -211,3 +328,13 @@ def _get_json_for_field(model: Union[CamelCaseModel, List[CamelCaseModel]]) -> s
211328
model_aliased = model.dict(by_alias=True)
212329

213330
return json.dumps(model_aliased)
331+
332+
333+
class DaysEnum(str, Enum):
334+
MONDAY = "MONDAY"
335+
TUESDAY = "TUESDAY"
336+
WEDNESDAY = "WEDNESDAY"
337+
THURSDAY = "THURSDAY"
338+
FRIDAY = "FRIDAY"
339+
SATURDAY = "SATURDAY"
340+
SUNDAY = "SUNDAY"

infobip_channels/mms/channel.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import requests
55

66
from infobip_channels.core.channel import Channel
7-
from infobip_channels.core.models import PostHeaders, QueryParameter, ResponseBase
7+
from infobip_channels.core.models import PostHeaders, ResponseBase
88
from infobip_channels.mms.models.body.send_mms import MMSMessageBody
99
from infobip_channels.mms.models.query_parameters.get_inbound_mms_messages import (
1010
GetInboundMMSMessagesQueryParameters,
@@ -26,24 +26,6 @@ class MMSChannel(Channel):
2626

2727
MMS_URL_TEMPLATE = "/mms/1/"
2828

29-
@staticmethod
30-
def validate_query_parameter(
31-
parameter: Union[QueryParameter, Dict], parameter_type: Type[QueryParameter]
32-
) -> QueryParameter:
33-
"""
34-
Validate the query parameter by trying to instantiate the provided class.
35-
If the passed parameter is already of that type, just return it as is.
36-
37-
:param parameter: Query parameter to validate
38-
:param parameter_type: Type of the query parameter
39-
:return: Class instance corresponding to the provided parameter type
40-
"""
41-
return (
42-
parameter
43-
if isinstance(parameter, parameter_type)
44-
else parameter_type(**parameter)
45-
)
46-
4729
def _get_custom_response_class(
4830
self,
4931
raw_response: Union[requests.Response, Any],

0 commit comments

Comments
 (0)