Skip to content

Commit f831aa3

Browse files
committed
Merge branch 'develop'
Preparing for v2.0 release.
2 parents 5a0106e + 2248f20 commit f831aa3

25 files changed

+1075
-721
lines changed

.gitignore

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,103 @@
1+
# Created by https://www.gitignore.io/api/python
2+
3+
### Python ###
4+
# Byte-compiled / optimized / DLL files
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
9+
# C extensions
10+
*.so
11+
12+
# Distribution / packaging
13+
.Python
14+
env/
115
build/
16+
develop-eggs/
217
dist/
3-
nexmo.egg-info/
18+
downloads/
19+
eggs/
20+
.eggs/
21+
lib/
22+
lib64/
23+
parts/
24+
sdist/
25+
var/
26+
wheels/
27+
*.egg-info/
28+
.installed.cfg
29+
*.egg
30+
31+
# PyInstaller
32+
# Usually these files are written by a python script from a template
33+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
34+
*.manifest
35+
*.spec
36+
37+
# Installer logs
38+
pip-log.txt
39+
pip-delete-this-directory.txt
40+
41+
# Unit test / coverage reports
42+
htmlcov/
43+
.tox/
44+
.coverage
45+
.coverage.*
46+
.cache
47+
nosetests.xml
48+
coverage.xml
49+
*,cover
50+
.hypothesis/
51+
52+
# Translations
53+
*.mo
54+
*.pot
55+
56+
# Django stuff:
57+
*.log
58+
local_settings.py
59+
60+
# Flask stuff:
61+
instance/
62+
.webassets-cache
63+
64+
# Scrapy stuff:
65+
.scrapy
66+
67+
# Sphinx documentation
68+
docs/_build/
69+
70+
# PyBuilder
71+
target/
72+
73+
# Jupyter Notebook
74+
.ipynb_checkpoints
75+
76+
# pyenv
77+
.python-version
78+
79+
# celery beat schedule file
80+
celerybeat-schedule
81+
82+
# SageMath parsed files
83+
*.sage.py
84+
85+
# dotenv
86+
.env
87+
88+
# virtualenv
89+
.venv
90+
venv/
91+
ENV/
92+
93+
# Spyder project settings
94+
.spyderproject
95+
.spyproject
96+
97+
# Rope project settings
98+
.ropeproject
99+
100+
# mkdocs documentation
101+
/site
102+
103+
.requirements.txt

.travis.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ language: python
22

33
python:
44
- "2.7"
5-
- "3.3"
65
- "3.4"
76
- "3.5"
87
- "3.6"
98

109
install:
11-
- pip install --quiet requests responses
12-
- python setup.py install
10+
- make install
1311

1412
script:
15-
- python test_nexmo.py
13+
- make test

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Development
2+
* Drop support for Python 3.3 (in line with the cryptography library we depend upon)
3+
* Ensure timestamp is added the params list if signing requests
4+
15
# 1.5.0
26

37
* Added ability to provide a file path as private_key param no the nexmo.Client constructor

Makefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.PHONY: test install requirements release release-test
2+
test:
3+
pytest -v
4+
5+
release-test:
6+
python setup.py sdist bdist_wheel upload -r pypitest
7+
8+
release:
9+
python setup.py sdist bdist_wheel upload
10+
11+
install: requirements
12+
13+
requirements: .requirements.txt
14+
15+
.requirements.txt: requirements.txt
16+
pip install --upgrade pip setuptools
17+
pip install -r requirements.txt
18+
pip freeze > .requirements.txt

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Alternatively you can clone the repository:
3232
Usage
3333
-----
3434

35-
Begin by importing the nexmo module:
35+
Begin by importing the `nexmo` module:
3636

3737
```python
3838
import nexmo
@@ -351,10 +351,31 @@ API Coverage
351351
* [X] Text-To-Speech Prompt
352352

353353

354+
Contributing
355+
------------
356+
357+
We :heart: contributions! But if you plan to work on something big or controversial, please [contact us](mailto:devrel@nexmo.com) first!
358+
359+
We recommend working on `nexmo-python` with a [virtualenv][virtualenv]. The following command will install all the Python dependencies you need to run the tests:
360+
361+
```bash
362+
make install
363+
```
364+
365+
The tests are all written with pytest. You run them with:
366+
367+
```bash
368+
make test
369+
```
370+
371+
354372
License
355373
-------
356374

357375
This library is released under the [MIT License][license]
358376

377+
[virtualenv]: https://virtualenv.pypa.io/en/stable/
378+
[report-a-bug]: https://github.com/Nexmo/nexmo-python/issues/new
379+
[pull-request]: https://github.com/Nexmo/nexmo-python/pulls
359380
[signup]: https://dashboard.nexmo.com/sign-up?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library
360381
[license]: LICENSE.txt

docs/quickstart.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ In order to check signatures for incoming webhook requests, you'll also
6262
need to specify the ``signature_secret`` argument (or the
6363
``NEXMO_SIGNATURE_SECRET`` environment variable).
6464

65+
If the argument ``signature_method`` is omitted, it will default to the md5 hash
66+
algorithm. Otherwise, it will use the selected method as in md5, sha1, sha256 or
67+
sha512 with hmac.
68+
6569
SMS API
6670
-------
6771

@@ -256,6 +260,16 @@ Validate webhook signatures
256260
else:
257261
# invalid signature
258262
263+
264+
or by using signature method via POST:
265+
266+
client = nexmo.Client(signature_secret='secret', signature_method='sha256')
267+
268+
if client.check_signature(request.body.decode()):
269+
# valid signature
270+
else:
271+
# invalid signature
272+
259273
Docs:
260274
`https://docs.nexmo.com/messaging/signing-messages <https://docs.nexmo.com/messaging/signing-messages?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library>`__
261275

nexmo/__init__.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
__version__ = '1.5.0'
2-
3-
import requests, os, warnings, hashlib, hmac, jwt, time, uuid, sys
4-
51
from platform import python_version
62

3+
import hashlib
4+
import hmac
5+
import jwt
6+
import os
7+
import requests
8+
import sys
9+
import time
10+
from uuid import uuid4
11+
import warnings
12+
713
if sys.version_info[0] == 3:
814
string_types = (str, bytes)
915
else:
1016
string_types = (unicode, str)
1117

18+
__version__ = '1.5.0'
19+
1220

1321
class Error(Exception):
1422
pass
@@ -33,6 +41,16 @@ def __init__(self, **kwargs):
3341
self.api_secret = kwargs.get('secret', None) or os.environ.get('NEXMO_API_SECRET', None)
3442

3543
self.signature_secret = kwargs.get('signature_secret', None) or os.environ.get('NEXMO_SIGNATURE_SECRET', None)
44+
self.signature_method = kwargs.get('signature_method', None) or os.environ.get('NEXMO_SIGNATURE_METHOD', None)
45+
46+
if self.signature_method == 'md5':
47+
self.signature_method = hashlib.md5
48+
elif self.signature_method == 'sha1':
49+
self.signature_method = hashlib.sha1
50+
elif self.signature_method == 'sha256':
51+
self.signature_method = hashlib.sha256
52+
elif self.signature_method == 'sha512':
53+
self.signature_method = hashlib.sha512
3654

3755
self.application_id = kwargs.get('application_id', None)
3856

@@ -237,24 +255,37 @@ def send_dtmf(self, uuid, params=None, **kwargs):
237255
def check_signature(self, params):
238256
params = dict(params)
239257

240-
signature = params.pop('sig', '')
258+
signature = params.pop('sig', '').lower()
241259

242260
return hmac.compare_digest(signature, self.signature(params))
243261

244262
def signature(self, params):
245-
md5 = hashlib.md5()
263+
if self.signature_method:
264+
hasher = hmac.new(self.signature_secret.encode(), digestmod=self.signature_method)
265+
else:
266+
hasher = hashlib.md5()
267+
268+
# Add timestamp if not already present
269+
if not params.get("timestamp"):
270+
params["timestamp"] = int(time.time())
246271

247272
for key in sorted(params):
248-
md5.update('&{0}={1}'.format(key, params[key]).encode('utf-8'))
273+
value = params[key]
274+
275+
if isinstance(value, str):
276+
value = value.replace('&', '_').replace('=', '_')
249277

250-
md5.update(self.signature_secret.encode('utf-8'))
278+
hasher.update('&{0}={1}'.format(key, value).encode('utf-8'))
251279

252-
return md5.hexdigest()
280+
if self.signature_method is None:
281+
hasher.update(self.signature_secret.encode())
253282

254-
def get(self, host, request_uri, params={}):
283+
return hasher.hexdigest()
284+
285+
def get(self, host, request_uri, params=None):
255286
uri = 'https://' + host + request_uri
256287

257-
params = dict(params, api_key=self.api_key, api_secret=self.api_secret)
288+
params = dict(params or {}, api_key=self.api_key, api_secret=self.api_secret)
258289

259290
return self.parse(host, requests.get(uri, params=params, headers=self.headers))
260291

@@ -295,10 +326,10 @@ def parse(self, host, response):
295326

296327
raise ServerError(message)
297328

298-
def __get(self, request_uri, params={}):
329+
def __get(self, request_uri, params=None):
299330
uri = 'https://' + self.api_host + request_uri
300331

301-
return self.parse(self.api_host, requests.get(uri, params=params, headers=self.__headers()))
332+
return self.parse(self.api_host, requests.get(uri, params=params or {}, headers=self.__headers()))
302333

303334
def __post(self, request_uri, params):
304335
uri = 'https://' + self.api_host + request_uri
@@ -322,7 +353,7 @@ def __headers(self):
322353
payload.setdefault('application_id', self.application_id)
323354
payload.setdefault('iat', iat)
324355
payload.setdefault('exp', iat + 60)
325-
payload.setdefault('jti', str(uuid.uuid4()))
356+
payload.setdefault('jti', str(uuid4()))
326357

327358
token = jwt.encode(payload, self.private_key, algorithm='RS256')
328359

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-e .
2+
pytest==3.1.2
3+
pytest-cov==2.5.1
4+
responses==0.5.1

setup.cfg

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
1+
[tool:pytest]
2+
testpaths=tests
3+
addopts=--tb=short -p no:doctest
4+
norecursedirs = bin dist docs htmlcov .* {args}
5+
16
[pycodestyle]
27
max-line-length=120
8+
9+
[coverage:run]
10+
source= nexmo

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
'Programming Language :: Python :: 2',
2323
'Programming Language :: Python :: 2.7',
2424
'Programming Language :: Python :: 3',
25-
'Programming Language :: Python :: 3.3',
2625
'Programming Language :: Python :: 3.4',
2726
'Programming Language :: Python :: 3.5',
2827
'Programming Language :: Python :: 3.6',

0 commit comments

Comments
 (0)