diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cbaeea99 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Python files 4 space indentation +[*.py] +charset = utf-8 +indent_style = space +indent_size = 4 + +# Makefiles tab indentation +[Makefile] +indent_style = tab + +# Yaml files 2-space indentation +[*.yml] +indent_style = space +indent_size = 2 diff --git a/CHANGES.md b/CHANGES.md index a9a921c1..c90b288f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +# 1.5.0 + +* Added ability to provide a file path as private_key param no the nexmo.Client constructor + +* Added send/stop endpoints for audio/speech/dtmf + +* Added new number insight endpoints + # 1.4.0 * Added new Voice API call methods diff --git a/README.md b/README.md index 1cf6211e..855a28df 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,48 @@ response = client.update_call(uuid, action='hangup') Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#call_modify_single](https://docs.nexmo.com/voice/voice-api/api-reference#call_modify_single?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library) +### Stream audio to a call + +```python +stream_url = 'https://nexmo-community.github.io/ncco-examples/assets/voice_api_audio_streaming.mp3' + +response = client.send_audio(uuid, stream_url=stream_url) +``` + +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#stream_put](https://docs.nexmo.com/voice/voice-api/api-reference#stream_put?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library) + +### Stop streaming audio to a call + +```python +response = client.stop_audio(uuid) +``` + +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#stream_delete](https://docs.nexmo.com/voice/voice-api/api-reference#stream_delete?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library) + +### Send a synthesized speech message to a call + +```python +response = client.send_speech(uuid, text='Hello') +``` + +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#talk_put](https://docs.nexmo.com/voice/voice-api/api-reference#talk_put?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library) + +### Stop sending a synthesized speech message to a call + +```python +response = client.stop_speech(uuid) +``` + +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#talk_delete](https://docs.nexmo.com/voice/voice-api/api-reference#talk_delete?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library) + +### Send DTMF tones to a call + +```python +response = client.send_dtmf(uuid, digits='1234') +``` + +Docs: [https://docs.nexmo.com/voice/voice-api/api-reference#dtmf_put](https://docs.nexmo.com/voice/voice-api/api-reference#dtmf_put?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library) + ## Verify API diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..e35d8850 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_build diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..46afce51 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,228 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +quickstart.rst: ../README.md + pandoc -f markdown -t rst ../README.md -o quickstart.rst + +.PHONY: html +html: quickstart.rst + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Nexmo.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Nexmo.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Nexmo" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Nexmo" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..2c45c73d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +# +# Nexmo documentation build configuration file, created by +# sphinx-quickstart on Sun Sep 18 14:36:55 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Nexmo' +copyright = u'2016, Tim Craft' +author = u'Tim Craft' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'1.4.0' +# The full version, including alpha/beta/rc tags. +release = u'1.4.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + + +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" + +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = u'Nexmo v1.4.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Nexmodoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Nexmo.tex', u'Nexmo Documentation', + u'Tim Craft', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'nexmo', u'Nexmo Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Nexmo', u'Nexmo Documentation', + author, 'Nexmo', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..109e4e3a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ + +Welcome to Nexmo's documentation! +================================= + +.. toctree:: + :maxdepth: 2 + + quickstart + reference + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..8f65cffb --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,281 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. epub3 to make an epub3 + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + echo. dummy to check syntax errors of document sources + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Nexmo.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Nexmo.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "epub3" ( + %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +if "%1" == "dummy" ( + %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. Dummy builder generates no files. + goto end +) + +:end diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 00000000..b8db6014 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,342 @@ +Nexmo Client Library for Python +=============================== + +|PyPI version| |Build Status| + +This is the Python client library for Nexmo's API. To use it you'll need +a Nexmo account. Sign up `for free at +nexmo.com `__. + +- `Installation <#installation>`__ +- `Usage <#usage>`__ +- `SMS API <#sms-api>`__ +- `Voice API <#voice-api>`__ +- `Verify API <#verify-api>`__ +- `Application API <#application-api>`__ +- `Coverage <#api-coverage>`__ +- `License <#license>`__ + +Installation +------------ + +To install the Python client library using pip: + +:: + + pip install nexmo + +Alternatively you can clone the repository: + +:: + + git clone git@github.com:Nexmo/nexmo-python.git + +Usage +----- + +Begin by importing the nexmo module: + +.. code:: python + + import nexmo + +Then construct a client object with your key and secret: + +.. code:: python + + client = nexmo.Client(key=api_key, secret=api_secret) + +For production you can specify the ``NEXMO_API_KEY`` and +``NEXMO_API_SECRET`` environment variables instead of specifying the key +and secret explicitly. + +For newer endpoints that support JWT authentication such as the Voice +API, you can also specify the ``application_id`` and ``private_key`` +arguments: + +.. code:: python + + client = nexmo.Client(application_id=application_id, private_key=private_key) + +In order to check signatures for incoming webhook requests, you'll also +need to specify the ``signature_secret`` argument (or the +``NEXMO_SIGNATURE_SECRET`` environment variable). + +SMS API +------- + +Send a text message +~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.send_message({'from': 'Python', 'to': 'YOUR-NUMBER', 'text': 'Hello world'}) + + response = response['messages'][0] + + if response['status'] == '0': + print 'Sent message', response['message-id'] + + print 'Remaining balance is', response['remaining-balance'] + else: + print 'Error:', response['error-text'] + +Docs: +`https://docs.nexmo.com/messaging/sms-api/api-reference#request `__ + +Voice API +--------- + +Make a call +~~~~~~~~~~~ + +.. code:: python + + response = client.create_call({ + 'to': [{'type': 'phone', 'number': '14843331234'}], + 'from': {'type': 'phone', 'number': '14843335555'}, + 'answer_url': ['https://example.com/answer'] + }) + +Docs: +`https://docs.nexmo.com/voice/voice-api/api-reference#call\_create `__ + +Retrieve a list of calls +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.get_calls() + +Docs: +`https://docs.nexmo.com/voice/voice-api/api-reference#call\_retrieve `__ + +Retrieve a single call +~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.get_call(uuid) + +Docs: +`https://docs.nexmo.com/voice/voice-api/api-reference#call\_retrieve\_single `__ + +Update a call +~~~~~~~~~~~~~ + +.. code:: python + + response = client.update_call(uuid, action='hangup') + +Docs: +`https://docs.nexmo.com/voice/voice-api/api-reference#call\_modify\_single `__ + +Verify API +---------- + +Start a verification +~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.start_verification(number='441632960960', brand='MyApp') + + if response['status'] == '0': + print 'Started verification request_id=' + response['request_id'] + else: + print 'Error:', response['error_text'] + +Docs: +`https://docs.nexmo.com/verify/api-reference/api-reference#vrequest `__ + +The response contains a verification request id which you will need to +store temporarily (in the session, database, url etc). + +Check a verification +~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.check_verification('00e6c3377e5348cdaf567e1417c707a5', code='1234') + + if response['status'] == '0': + print 'Verification complete, event_id=' + response['event_id'] + else: + print 'Error:', response['error_text'] + +Docs: +`https://docs.nexmo.com/verify/api-reference/api-reference#check `__ + +The verification request id comes from the call to the +start\_verification method. The PIN code is entered into your +application by the user. + +Cancel a verification +~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + client.cancel_verification('00e6c3377e5348cdaf567e1417c707a5') + +Docs: +`https://docs.nexmo.com/verify/api-reference/api-reference#control `__ + +Trigger next verification step +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + client.trigger_next_verification_event('00e6c3377e5348cdaf567e1417c707a5') + +Docs: +`https://docs.nexmo.com/verify/api-reference/api-reference#control `__ + +Application API +--------------- + +Create an application +~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.create_application(name='Example App', type='voice', answer_url=answer_url) + +Docs: +`https://docs.nexmo.com/tools/application-api/api-reference#create `__ + +Retrieve a list of applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.get_applications() + +Docs: +`https://docs.nexmo.com/tools/application-api/api-reference#list `__ + +Retrieve a single application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.get_application(uuid) + +Docs: +`https://docs.nexmo.com/tools/application-api/api-reference#retrieve `__ + +Update an application +~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.update_application(uuid, answer_method='POST') + +Docs: +`https://docs.nexmo.com/tools/application-api/api-reference#update `__ + +Delete an application +~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + response = client.delete_application(uuid) + +Docs: +`https://docs.nexmo.com/tools/application-api/api-reference#delete `__ + +Validate webhook signatures +--------------------------- + +.. code:: python + + client = nexmo.Client(signature_secret='secret') + + if client.check_signature(request.query): + # valid signature + else: + # invalid signature + +Docs: +`https://docs.nexmo.com/messaging/signing-messages `__ + +Note: you'll need to contact support@nexmo.com to enable message signing +on your account before you can validate webhook signatures. + +JWT parameters +-------------- + +By default the library generates short lived tokens for JWT +authentication. + +Use the auth method to specify parameters for a longer life token or to +specify a different token identifier: + +.. code:: python + + client.auth(nbf=nbf, exp=exp, jti=jti) + +API Coverage +------------ + +- Account + + - [X] Balance + - [X] Pricing + - [X] Settings + - [X] Top Up + - [X] Numbers + + - [X] Search + - [X] Buy + - [X] Cancel + - [X] Update + +- Number Insight + + - [X] Basic + - [X] Standard + - [X] Advanced + - [ ] Webhook Notification + +- Verify + + - [X] Verify + - [X] Check + - [X] Search + - [X] Control + +- Messaging + + - [X] Send + - [ ] Delivery Receipt + - [ ] Inbound Messages + - [X] Search + + - [X] Message + - [X] Messages + - [X] Rejections + + - [X] US Short Codes + + - [X] Two-Factor Authentication + - [X] Event Based Alerts + + - [X] Sending Alerts + - [X] Campaign Subscription Management + +- Voice + + - [X] Outbound Calls + - [ ] Inbound Call + - [X] Text-To-Speech Call + - [X] Text-To-Speech Prompt + +License +------- + +This library is released under the `MIT License `__ + +.. |PyPI version| image:: https://badge.fury.io/py/nexmo.svg + :target: https://badge.fury.io/py/nexmo +.. |Build Status| image:: https://api.travis-ci.org/Nexmo/nexmo-python.svg?branch=master + :target: https://travis-ci.org/Nexmo/nexmo-python diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 00000000..53ba2356 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,6 @@ +API Reference +============= + +.. automodule:: nexmo + :members: + :undoc-members: \ No newline at end of file diff --git a/nexmo/__init__.py b/nexmo/__init__.py index 87e5e09f..7df4ccb6 100644 --- a/nexmo/__init__.py +++ b/nexmo/__init__.py @@ -1,289 +1,329 @@ -__version__ = '1.4.0' +__version__ = '1.5.0' - -import requests, os, warnings, hashlib, hmac, jwt, time, uuid +import requests, os, warnings, hashlib, hmac, jwt, time, uuid, sys from platform import python_version +if sys.version_info[0] == 3: + string_types = (str, bytes) +else: + string_types = (unicode, str) + class Error(Exception): - pass + pass class ClientError(Error): - pass + pass class ServerError(Error): - pass + pass class AuthenticationError(ClientError): - pass + pass class Client(): - def __init__(self, **kwargs): - self.api_key = kwargs.get('key', None) or os.environ.get('NEXMO_API_KEY', None) + def __init__(self, **kwargs): + self.api_key = kwargs.get('key', None) or os.environ.get('NEXMO_API_KEY', None) + + self.api_secret = kwargs.get('secret', None) or os.environ.get('NEXMO_API_SECRET', None) + + self.signature_secret = kwargs.get('signature_secret', None) or os.environ.get('NEXMO_SIGNATURE_SECRET', None) + + self.application_id = kwargs.get('application_id', None) + + self.private_key = kwargs.get('private_key', None) + + if isinstance(self.private_key, string_types) and '\n' not in self.private_key: + with open(self.private_key, 'rb') as key_file: + self.private_key = key_file.read() + + self.host = 'rest.nexmo.com' + + self.api_host = 'api.nexmo.com' + + user_agent = 'nexmo-python/{0}/{1}'.format(__version__, python_version()) + + if 'app_name' in kwargs and 'app_version' in kwargs: + user_agent += '/{0}/{1}'.format(kwargs['app_name'], kwargs['app_version']) + + self.headers = {'User-Agent': user_agent} + + self.auth_params = {} - self.api_secret = kwargs.get('secret', None) or os.environ.get('NEXMO_API_SECRET', None) + def auth(self, params=None, **kwargs): + self.auth_params = params or kwargs - self.signature_secret = kwargs.get('signature_secret', None) or os.environ.get('NEXMO_SIGNATURE_SECRET', None) + def send_message(self, params): + return self.post(self.host, '/sms/json', params) - self.application_id = kwargs.get('application_id', None) + def get_balance(self): + return self.get(self.host, '/account/get-balance') - self.private_key = kwargs.get('private_key', None) + def get_country_pricing(self, country_code): + return self.get(self.host, '/account/get-pricing/outbound', {'country': country_code}) - self.host = 'rest.nexmo.com' + def get_prefix_pricing(self, prefix): + return self.get(self.host, '/account/get-prefix-pricing/outbound', {'prefix': prefix}) - self.api_host = 'api.nexmo.com' + def get_sms_pricing(self, number): + return self.get(self.host, '/account/get-phone-pricing/outbound/sms', {'phone': number}) - user_agent = 'nexmo-python/{0}/{1}'.format(__version__, python_version()) + def get_voice_pricing(self, number): + return self.get(self.host, '/account/get-phone-pricing/outbound/voice', {'phone': number}) - if 'app_name' in kwargs and 'app_version' in kwargs: - user_agent += '/{0}/{1}'.format(kwargs['app_name'], kwargs['app_version']) + def update_settings(self, params=None, **kwargs): + return self.post(self.host, '/account/settings', params or kwargs) - self.headers = {'User-Agent': user_agent} + def topup(self, params=None, **kwargs): + return self.post(self.host, '/account/top-up', params or kwargs) - self.auth_params = {} + def get_account_numbers(self, params=None, **kwargs): + return self.get(self.host, '/account/numbers', params or kwargs) - def auth(self, params=None, **kwargs): - self.auth_params = params or kwargs + def get_available_numbers(self, country_code, params=None, **kwargs): + return self.get(self.host, '/number/search', dict(params or kwargs, country=country_code)) - def send_message(self, params): - return self.post(self.host, '/sms/json', params) + def buy_number(self, params=None, **kwargs): + return self.post(self.host, '/number/buy', params or kwargs) - def get_balance(self): - return self.get(self.host, '/account/get-balance') + def cancel_number(self, params=None, **kwargs): + return self.post(self.host, '/number/cancel', params or kwargs) - def get_country_pricing(self, country_code): - return self.get(self.host, '/account/get-pricing/outbound', {'country': country_code}) + def update_number(self, params=None, **kwargs): + return self.post(self.host, '/number/update', params or kwargs) - def get_prefix_pricing(self, prefix): - return self.get(self.host, '/account/get-prefix-pricing/outbound', {'prefix': prefix}) + def get_message(self, message_id): + return self.get(self.host, '/search/message', {'id': message_id}) - def get_sms_pricing(self, number): - return self.get(self.host, '/account/get-phone-pricing/outbound/sms', {'phone': number}) + def get_message_rejections(self, params=None, **kwargs): + return self.get(self.host, '/search/rejections', params or kwargs) - def get_voice_pricing(self, number): - return self.get(self.host, '/account/get-phone-pricing/outbound/voice', {'phone': number}) + def search_messages(self, params=None, **kwargs): + return self.get(self.host, '/search/messages', params or kwargs) - def update_settings(self, params=None, **kwargs): - return self.post(self.host, '/account/settings', params or kwargs) + def send_ussd_push_message(self, params=None, **kwargs): + return self.post(self.host, '/ussd/json', params or kwargs) - def topup(self, params=None, **kwargs): - return self.post(self.host, '/account/top-up', params or kwargs) + def send_ussd_prompt_message(self, params=None, **kwargs): + return self.post(self.host, '/ussd-prompt/json', params or kwargs) - def get_account_numbers(self, params=None, **kwargs): - return self.get(self.host, '/account/numbers', params or kwargs) + def send_2fa_message(self, params=None, **kwargs): + return self.post(self.host, '/sc/us/2fa/json', params or kwargs) - def get_available_numbers(self, country_code, params=None, **kwargs): - return self.get(self.host, '/number/search', dict(params or kwargs, country=country_code)) + def send_event_alert_message(self, params=None, **kwargs): + return self.post(self.host, '/sc/us/alert/json', params or kwargs) - def buy_number(self, params=None, **kwargs): - return self.post(self.host, '/number/buy', params or kwargs) + def send_marketing_message(self, params=None, **kwargs): + return self.post(self.host, '/sc/us/marketing/json', params or kwargs) - def cancel_number(self, params=None, **kwargs): - return self.post(self.host, '/number/cancel', params or kwargs) + def get_event_alert_numbers(self): + return self.get(self.host, '/sc/us/alert/opt-in/query/json') - def update_number(self, params=None, **kwargs): - return self.post(self.host, '/number/update', params or kwargs) + def resubscribe_event_alert_number(self, params=None, **kwargs): + return self.post(self.host, '/sc/us/alert/opt-in/manage/json', params or kwargs) - def get_message(self, message_id): - return self.get(self.host, '/search/message', {'id': message_id}) + def initiate_call(self, params=None, **kwargs): + return self.post(self.host, '/call/json', params or kwargs) - def get_message_rejections(self, params=None, **kwargs): - return self.get(self.host, '/search/rejections', params or kwargs) + def initiate_tts_call(self, params=None, **kwargs): + return self.post(self.api_host, '/tts/json', params or kwargs) - def search_messages(self, params=None, **kwargs): - return self.get(self.host, '/search/messages', params or kwargs) + def initiate_tts_prompt_call(self, params=None, **kwargs): + return self.post(self.api_host, '/tts-prompt/json', params or kwargs) - def send_ussd_push_message(self, params=None, **kwargs): - return self.post(self.host, '/ussd/json', params or kwargs) + def start_verification(self, params=None, **kwargs): + return self.post(self.api_host, '/verify/json', params or kwargs) - def send_ussd_prompt_message(self, params=None, **kwargs): - return self.post(self.host, '/ussd-prompt/json', params or kwargs) + def send_verification_request(self, params=None, **kwargs): + warnings.warn('nexmo.Client#send_verification_request is deprecated (use #start_verification instead)', + DeprecationWarning, stacklevel=2) - def send_2fa_message(self, params=None, **kwargs): - return self.post(self.host, '/sc/us/2fa/json', params or kwargs) + return self.post(self.api_host, '/verify/json', params or kwargs) - def send_event_alert_message(self, params=None, **kwargs): - return self.post(self.host, '/sc/us/alert/json', params or kwargs) + def check_verification(self, request_id, params=None, **kwargs): + return self.post(self.api_host, '/verify/check/json', dict(params or kwargs, request_id=request_id)) - def send_marketing_message(self, params=None, **kwargs): - return self.post(self.host, '/sc/us/marketing/json', params or kwargs) + def check_verification_request(self, params=None, **kwargs): + warnings.warn('nexmo.Client#check_verification_request is deprecated (use #check_verification instead)', + DeprecationWarning, stacklevel=2) - def get_event_alert_numbers(self): - return self.get(self.host, '/sc/us/alert/opt-in/query/json') + return self.post(self.api_host, '/verify/check/json', params or kwargs) - def resubscribe_event_alert_number(self, params=None, **kwargs): - return self.post(self.host, '/sc/us/alert/opt-in/manage/json', params or kwargs) + def get_verification(self, request_id): + return self.get(self.api_host, '/verify/search/json', {'request_id': request_id}) - def initiate_call(self, params=None, **kwargs): - return self.post(self.host, '/call/json', params or kwargs) + def get_verification_request(self, request_id): + warnings.warn('nexmo.Client#get_verification_request is deprecated (use #get_verification instead)', + DeprecationWarning, stacklevel=2) - def initiate_tts_call(self, params=None, **kwargs): - return self.post(self.api_host, '/tts/json', params or kwargs) + return self.get(self.api_host, '/verify/search/json', {'request_id': request_id}) - def initiate_tts_prompt_call(self, params=None, **kwargs): - return self.post(self.api_host, '/tts-prompt/json', params or kwargs) + def cancel_verification(self, request_id): + return self.post(self.api_host, '/verify/control/json', {'request_id': request_id, 'cmd': 'cancel'}) - def start_verification(self, params=None, **kwargs): - return self.post(self.api_host, '/verify/json', params or kwargs) + def trigger_next_verification_event(self, request_id): + return self.post(self.api_host, '/verify/control/json', {'request_id': request_id, 'cmd': 'trigger_next_event'}) - def send_verification_request(self, params=None, **kwargs): - warnings.warn('nexmo.Client#send_verification_request is deprecated (use #start_verification instead)', DeprecationWarning, stacklevel=2) + def control_verification_request(self, params=None, **kwargs): + warnings.warn('nexmo.Client#control_verification_request is deprecated', DeprecationWarning, stacklevel=2) - return self.post(self.api_host, '/verify/json', params or kwargs) + return self.post(self.api_host, '/verify/control/json', params or kwargs) - def check_verification(self, request_id, params=None, **kwargs): - return self.post(self.api_host, '/verify/check/json', dict(params or kwargs, request_id=request_id)) + def get_basic_number_insight(self, params=None, **kwargs): + return self.get(self.api_host, '/ni/basic/json', params or kwargs) - def check_verification_request(self, params=None, **kwargs): - warnings.warn('nexmo.Client#check_verification_request is deprecated (use #check_verification instead)', DeprecationWarning, stacklevel=2) + def get_standard_number_insight(self, params=None, **kwargs): + return self.get(self.api_host, '/ni/standard/json', params or kwargs) - return self.post(self.api_host, '/verify/check/json', params or kwargs) + def get_number_insight(self, params=None, **kwargs): + warnings.warn('nexmo.Client#get_number_insight is deprecated (use #get_standard_number_insight instead)', + DeprecationWarning, stacklevel=2) - def get_verification(self, request_id): - return self.get(self.api_host, '/verify/search/json', {'request_id': request_id}) + return self.get(self.api_host, '/number/lookup/json', params or kwargs) - def get_verification_request(self, request_id): - warnings.warn('nexmo.Client#get_verification_request is deprecated (use #get_verification instead)', DeprecationWarning, stacklevel=2) + def get_advanced_number_insight(self, params=None, **kwargs): + return self.get(self.api_host, '/ni/advanced/json', params or kwargs) - return self.get(self.api_host, '/verify/search/json', {'request_id': request_id}) + def request_number_insight(self, params=None, **kwargs): + return self.post(self.host, '/ni/json', params or kwargs) - def cancel_verification(self, request_id): - return self.post(self.api_host, '/verify/control/json', {'request_id': request_id, 'cmd': 'cancel'}) + def get_applications(self, params=None, **kwargs): + return self.get(self.api_host, '/v1/applications', params or kwargs) - def trigger_next_verification_event(self, request_id): - return self.post(self.api_host, '/verify/control/json', {'request_id': request_id, 'cmd': 'trigger_next_event'}) + def get_application(self, application_id): + return self.get(self.api_host, '/v1/applications/' + application_id) - def control_verification_request(self, params=None, **kwargs): - warnings.warn('nexmo.Client#control_verification_request is deprecated', DeprecationWarning, stacklevel=2) + def create_application(self, params=None, **kwargs): + return self.post(self.api_host, '/v1/applications', params or kwargs) - return self.post(self.api_host, '/verify/control/json', params or kwargs) + def update_application(self, application_id, params=None, **kwargs): + return self.put(self.api_host, '/v1/applications/' + application_id, params or kwargs) - def get_basic_number_insight(self, params=None, **kwargs): - return self.get(self.api_host, '/number/format/json', params or kwargs) + def delete_application(self, application_id): + return self.delete(self.api_host, '/v1/applications/' + application_id) - def get_number_insight(self, params=None, **kwargs): - return self.get(self.api_host, '/number/lookup/json', params or kwargs) + def create_call(self, params=None, **kwargs): + return self.__post('/v1/calls', params or kwargs) - def request_number_insight(self, params=None, **kwargs): - return self.post(self.host, '/ni/json', params or kwargs) + def get_calls(self, params=None, **kwargs): + return self.__get('/v1/calls', params or kwargs) - def get_applications(self, params=None, **kwargs): - return self.get(self.api_host, '/v1/applications', params or kwargs) + def get_call(self, uuid): + return self.__get('/v1/calls/' + uuid) - def get_application(self, application_id): - return self.get(self.api_host, '/v1/applications/' + application_id) + def update_call(self, uuid, params=None, **kwargs): + return self.__put('/v1/calls/' + uuid, params or kwargs) - def create_application(self, params=None, **kwargs): - return self.post(self.api_host, '/v1/applications', params or kwargs) + def send_audio(self, uuid, params=None, **kwargs): + return self.__put('/v1/calls/' + uuid + '/stream', params or kwargs) - def update_application(self, application_id, params=None, **kwargs): - return self.put(self.api_host, '/v1/applications/' + application_id, params or kwargs) + def stop_audio(self, uuid): + return self.__delete('/v1/calls/' + uuid + '/stream') - def delete_application(self, application_id): - return self.delete(self.api_host, '/v1/applications/' + application_id) + def send_speech(self, uuid, params=None, **kwargs): + return self.__put('/v1/calls/' + uuid + '/talk', params or kwargs) - def create_call(self, params=None, **kwargs): - return self.__post('/v1/calls', params or kwargs) + def stop_speech(self, uuid): + return self.__delete('/v1/calls/' + uuid + '/talk') - def get_calls(self, params=None, **kwargs): - return self.__get('/v1/calls', params or kwargs) + def send_dtmf(self, uuid, params=None, **kwargs): + return self.__put('/v1/calls/' + uuid + '/dtmf', params or kwargs) - def get_call(self, uuid): - return self.__get('/v1/calls/' + uuid) + def check_signature(self, params): + params = dict(params) - def update_call(self, uuid, params=None, **kwargs): - return self.__put('/v1/calls/' + uuid, params or kwargs) + signature = params.pop('sig', '') - def check_signature(self, params): - params = dict(params) + return hmac.compare_digest(signature, self.signature(params)) - signature = params.pop('sig', '') + def signature(self, params): + md5 = hashlib.md5() - return hmac.compare_digest(signature, self.signature(params)) + for key in sorted(params): + md5.update('&{0}={1}'.format(key, params[key]).encode('utf-8')) - def signature(self, params): - md5 = hashlib.md5() + md5.update(self.signature_secret.encode('utf-8')) - for key in sorted(params): - md5.update('&{0}={1}'.format(key, params[key]).encode('utf-8')) + return md5.hexdigest() - md5.update(self.signature_secret.encode('utf-8')) + def get(self, host, request_uri, params={}): + uri = 'https://' + host + request_uri - return md5.hexdigest() + params = dict(params, api_key=self.api_key, api_secret=self.api_secret) - def get(self, host, request_uri, params={}): - uri = 'https://' + host + request_uri + return self.parse(host, requests.get(uri, params=params, headers=self.headers)) - params = dict(params, api_key=self.api_key, api_secret=self.api_secret) + def post(self, host, request_uri, params): + uri = 'https://' + host + request_uri - return self.parse(host, requests.get(uri, params=params, headers=self.headers)) + params = dict(params, api_key=self.api_key, api_secret=self.api_secret) - def post(self, host, request_uri, params): - uri = 'https://' + host + request_uri + return self.parse(host, requests.post(uri, data=params, headers=self.headers)) - params = dict(params, api_key=self.api_key, api_secret=self.api_secret) + def put(self, host, request_uri, params): + uri = 'https://' + host + request_uri - return self.parse(host, requests.post(uri, data=params, headers=self.headers)) + params = dict(params, api_key=self.api_key, api_secret=self.api_secret) - def put(self, host, request_uri, params): - uri = 'https://' + host + request_uri + return self.parse(host, requests.put(uri, json=params, headers=self.headers)) - params = dict(params, api_key=self.api_key, api_secret=self.api_secret) + def delete(self, host, request_uri): + uri = 'https://' + host + request_uri - return self.parse(host, requests.put(uri, json=params, headers=self.headers)) + params = dict(api_key=self.api_key, api_secret=self.api_secret) - def delete(self, host, request_uri): - uri = 'https://' + host + request_uri + return self.parse(host, requests.delete(uri, params=params, headers=self.headers)) - params = dict(api_key=self.api_key, api_secret=self.api_secret) + def parse(self, host, response): + if response.status_code == 401: + raise AuthenticationError + elif response.status_code == 204: + return None + elif 200 <= response.status_code < 300: + return response.json() + elif 400 <= response.status_code < 500: + message = "{code} response from {host}".format(code=response.status_code, host=host) - return self.parse(host, requests.delete(uri, params=params, headers=self.headers)) + raise ClientError(message) + elif 500 <= response.status_code < 600: + message = "{code} response from {host}".format(code=response.status_code, host=host) - def parse(self, host, response): - if response.status_code == 401: - raise AuthenticationError - elif response.status_code == 204: - return None - elif 200 <= response.status_code < 300: - return response.json() - elif 400 <= response.status_code < 500: - message = "{code} response from {host}".format(code=response.status_code, host=host) + raise ServerError(message) - raise ClientError(message) - elif 500 <= response.status_code < 600: - message = "{code} response from {host}".format(code=response.status_code, host=host) + def __get(self, request_uri, params={}): + uri = 'https://' + self.api_host + request_uri - raise ServerError(message) + return self.parse(self.api_host, requests.get(uri, params=params, headers=self.__headers())) - def __get(self, request_uri, params={}): - uri = 'https://' + self.api_host + request_uri + def __post(self, request_uri, params): + uri = 'https://' + self.api_host + request_uri - return self.parse(self.api_host, requests.get(uri, params=params, headers=self.__headers())) + return self.parse(self.api_host, requests.post(uri, json=params, headers=self.__headers())) - def __post(self, request_uri, params): - uri = 'https://' + self.api_host + request_uri + def __put(self, request_uri, params): + uri = 'https://' + self.api_host + request_uri - return self.parse(self.api_host, requests.post(uri, json=params, headers=self.__headers())) + return self.parse(self.api_host, requests.put(uri, json=params, headers=self.__headers())) - def __put(self, request_uri, params): - uri = 'https://' + self.api_host + request_uri + def __delete(self, request_uri): + uri = 'https://' + self.api_host + request_uri - return self.parse(self.api_host, requests.put(uri, json=params, headers=self.__headers())) + return self.parse(self.api_host, requests.delete(uri, headers=self.__headers())) - def __headers(self): - iat = int(time.time()) + def __headers(self): + iat = int(time.time()) - payload = dict(self.auth_params) - payload.setdefault('application_id', self.application_id) - payload.setdefault('iat', iat) - payload.setdefault('exp', iat + 60) - payload.setdefault('jti', str(uuid.uuid4())) + payload = dict(self.auth_params) + payload.setdefault('application_id', self.application_id) + payload.setdefault('iat', iat) + payload.setdefault('exp', iat + 60) + payload.setdefault('jti', str(uuid.uuid4())) - token = jwt.encode(payload, self.private_key, algorithm='RS256') + token = jwt.encode(payload, self.private_key, algorithm='RS256') - return dict(self.headers, Authorization=b'Bearer ' + token) + return dict(self.headers, Authorization=b'Bearer ' + token) diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 00000000..4994a700 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,2 @@ +sphinx==1.4.6 +sphinx_rtd_theme==0.1.9 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..2b682789 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[pycodestyle] +max-line-length=120 diff --git a/setup.py b/setup.py index ccd80bdb..35bd4659 100644 --- a/setup.py +++ b/setup.py @@ -2,19 +2,27 @@ from setuptools import setup - with open('nexmo/__init__.py', 'r') as fd: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) - + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) setup(name='nexmo', - version=version, - description='Nexmo Client Library for Python', - long_description='This is the Python client library for Nexmo\'s API. To use it you\'ll need a Nexmo account. Sign up `for free at nexmo.com `_.', - url='http://github.com/Nexmo/nexmo-python', - author='Tim Craft', - author_email='mail@timcraft.com', - license='MIT', - packages=['nexmo'], - platforms=['any'], - install_requires=['requests', 'PyJWT', 'cryptography']) + version=version, + description='Nexmo Client Library for Python', + long_description='This is the Python client library for Nexmo\'s API. To use it you\'ll need a Nexmo account. Sign up `for free at nexmo.com `_.', + url='http://github.com/Nexmo/nexmo-python', + author='Tim Craft', + author_email='mail@timcraft.com', + license='MIT', + packages=['nexmo'], + platforms=['any'], + install_requires=['requests', 'PyJWT', 'cryptography'], + classifiers=[ + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ]) diff --git a/test_nexmo.py b/test_nexmo.py index d96c4ae1..f0079a98 100644 --- a/test_nexmo.py +++ b/test_nexmo.py @@ -1,584 +1,700 @@ try: - from urllib.parse import urlparse + from urllib.parse import urlparse except ImportError: - from urlparse import urlparse + from urlparse import urlparse try: - from urllib.parse import quote_plus + from urllib.parse import quote_plus except ImportError: - from urllib import quote_plus + from urllib import quote_plus import unittest, nexmo, responses, platform, jwt, time def request_body(): - return responses.calls[0].request.body + return responses.calls[0].request.body def request_query(): - return urlparse(responses.calls[0].request.url).query + return urlparse(responses.calls[0].request.url).query def request_user_agent(): - return responses.calls[0].request.headers['User-Agent'] + return responses.calls[0].request.headers['User-Agent'] def request_authorization(): - return responses.calls[0].request.headers['Authorization'].decode('utf-8') + return responses.calls[0].request.headers['Authorization'].decode('utf-8') def request_content_type(): - return responses.calls[0].request.headers['Content-Type'] + return responses.calls[0].request.headers['Content-Type'] + + +def read_file(path): + with open(path) as input_file: + return input_file.read() class NexmoClientTestCase(unittest.TestCase): - def setUp(self): - self.api_key = 'nexmo-api-key' - self.api_secret = 'nexmo-api-secret' - self.application_id = 'nexmo-application-id' - self.private_key = open('test/private_key.txt').read() - self.public_key = open('test/public_key.txt').read() - self.user_agent = 'nexmo-python/{0}/{1}'.format(nexmo.__version__, platform.python_version()) - self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, application_id=self.application_id, private_key=self.private_key) + def setUp(self): + self.api_key = 'nexmo-api-key' + self.api_secret = 'nexmo-api-secret' + self.application_id = 'nexmo-application-id' + self.private_key = read_file('test/private_key.txt') + self.public_key = read_file('test/public_key.txt') + self.user_agent = 'nexmo-python/{0}/{1}'.format(nexmo.__version__, platform.python_version()) + self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, application_id=self.application_id, + private_key=self.private_key) + + if not hasattr(self, 'assertRegex'): + self.assertRegex = self.assertRegexpMatches + + if not hasattr(self, 'assertRaisesRegex'): + self.assertRaisesRegex = self.assertRaisesRegexp + + def stub(self, method, url): + responses.add(method, url, body='{"key":"value"}', status=200, content_type='application/json') + + @responses.activate + def test_send_message(self): + self.stub(responses.POST, 'https://rest.nexmo.com/sms/json') + + params = {'from': 'Python', 'to': '447525856424', 'text': 'Hey!'} + + self.assertIsInstance(self.client.send_message(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('from=Python', request_body()) + self.assertIn('to=447525856424', request_body()) + self.assertIn('text=Hey%21', request_body()) + + @responses.activate + def test_get_balance(self): + self.stub(responses.GET, 'https://rest.nexmo.com/account/get-balance') + + self.assertIsInstance(self.client.get_balance(), dict) + self.assertEqual(request_user_agent(), self.user_agent) + + @responses.activate + def test_get_country_pricing(self): + self.stub(responses.GET, 'https://rest.nexmo.com/account/get-pricing/outbound') + + self.assertIsInstance(self.client.get_country_pricing('GB'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('country=GB', request_query()) + + @responses.activate + def test_get_prefix_pricing(self): + self.stub(responses.GET, 'https://rest.nexmo.com/account/get-prefix-pricing/outbound') + + self.assertIsInstance(self.client.get_prefix_pricing(44), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('prefix=44', request_query()) + + @responses.activate + def test_get_sms_pricing(self): + self.stub(responses.GET, 'https://rest.nexmo.com/account/get-phone-pricing/outbound/sms') + + self.assertIsInstance(self.client.get_sms_pricing('447525856424'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('phone=447525856424', request_query()) + + @responses.activate + def test_get_voice_pricing(self): + self.stub(responses.GET, 'https://rest.nexmo.com/account/get-phone-pricing/outbound/voice') + + self.assertIsInstance(self.client.get_voice_pricing('447525856424'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('phone=447525856424', request_query()) + + @responses.activate + def test_update_settings(self): + self.stub(responses.POST, 'https://rest.nexmo.com/account/settings') + + params = {'moCallBackUrl': 'http://example.com/callback'} + + self.assertIsInstance(self.client.update_settings(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('moCallBackUrl=http%3A%2F%2Fexample.com%2Fcallback', request_body()) + + @responses.activate + def test_topup(self): + self.stub(responses.POST, 'https://rest.nexmo.com/account/top-up') + + params = {'trx': '00X123456Y7890123Z'} + + self.assertIsInstance(self.client.topup(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('trx=00X123456Y7890123Z', request_body()) + + @responses.activate + def test_get_account_numbers(self): + self.stub(responses.GET, 'https://rest.nexmo.com/account/numbers') + + self.assertIsInstance(self.client.get_account_numbers(size=25), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('size=25', request_query()) + + @responses.activate + def test_get_available_numbers(self): + self.stub(responses.GET, 'https://rest.nexmo.com/number/search') + + self.assertIsInstance(self.client.get_available_numbers('CA', size=25), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('country=CA', request_query()) + self.assertIn('size=25', request_query()) + + @responses.activate + def test_buy_number(self): + self.stub(responses.POST, 'https://rest.nexmo.com/number/buy') + + params = {'country': 'US', 'msisdn': 'number'} + + self.assertIsInstance(self.client.buy_number(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('country=US', request_body()) + self.assertIn('msisdn=number', request_body()) + + @responses.activate + def test_cancel_number(self): + self.stub(responses.POST, 'https://rest.nexmo.com/number/cancel') + + params = {'country': 'US', 'msisdn': 'number'} - if not hasattr(self, 'assertRaisesRegex'): - self.assertRaisesRegex = self.assertRaisesRegexp + self.assertIsInstance(self.client.cancel_number(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('country=US', request_body()) + self.assertIn('msisdn=number', request_body()) - def stub(self, method, url): - responses.add(method, url, body='{"key":"value"}', status=200, content_type='application/json') + @responses.activate + def test_update_number(self): + self.stub(responses.POST, 'https://rest.nexmo.com/number/update') - @responses.activate - def test_send_message(self): - self.stub(responses.POST, 'https://rest.nexmo.com/sms/json') + params = {'country': 'US', 'msisdn': 'number', 'moHttpUrl': 'callback'} - params = {'from': 'Python', 'to': '447525856424', 'text': 'Hey!'} + self.assertIsInstance(self.client.update_number(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('country=US', request_body()) + self.assertIn('msisdn=number', request_body()) + self.assertIn('moHttpUrl=callback', request_body()) - self.assertIsInstance(self.client.send_message(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('from=Python', request_body()) - self.assertIn('to=447525856424', request_body()) - self.assertIn('text=Hey%21', request_body()) + @responses.activate + def test_get_message(self): + self.stub(responses.GET, 'https://rest.nexmo.com/search/message') - @responses.activate - def test_get_balance(self): - self.stub(responses.GET, 'https://rest.nexmo.com/account/get-balance') + self.assertIsInstance(self.client.get_message('00A0B0C0'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('id=00A0B0C0', request_query()) - self.assertIsInstance(self.client.get_balance(), dict) - self.assertEqual(request_user_agent(), self.user_agent) + @responses.activate + def test_get_message_rejections(self): + self.stub(responses.GET, 'https://rest.nexmo.com/search/rejections') - @responses.activate - def test_get_country_pricing(self): - self.stub(responses.GET, 'https://rest.nexmo.com/account/get-pricing/outbound') + self.assertIsInstance(self.client.get_message_rejections(date='YYYY-MM-DD'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('date=YYYY-MM-DD', request_query()) - self.assertIsInstance(self.client.get_country_pricing('GB'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('country=GB', request_query()) + @responses.activate + def test_search_messages(self): + self.stub(responses.GET, 'https://rest.nexmo.com/search/messages') - @responses.activate - def test_get_prefix_pricing(self): - self.stub(responses.GET, 'https://rest.nexmo.com/account/get-prefix-pricing/outbound') + self.assertIsInstance(self.client.search_messages(to='1234567890', date='YYYY-MM-DD'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('date=YYYY-MM-DD', request_query()) + self.assertIn('to=1234567890', request_query()) - self.assertIsInstance(self.client.get_prefix_pricing(44), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('prefix=44', request_query()) + @responses.activate + def test_search_messages_by_ids(self): + self.stub(responses.GET, 'https://rest.nexmo.com/search/messages') - @responses.activate - def test_get_sms_pricing(self): - self.stub(responses.GET, 'https://rest.nexmo.com/account/get-phone-pricing/outbound/sms') + self.assertIsInstance(self.client.search_messages(ids=['00A0B0C0', '00A0B0C1', '00A0B0C2']), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('ids=00A0B0C0', request_query()) + self.assertIn('ids=00A0B0C1', request_query()) + self.assertIn('ids=00A0B0C2', request_query()) - self.assertIsInstance(self.client.get_sms_pricing('447525856424'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('phone=447525856424', request_query()) + @responses.activate + def test_send_ussd_push_message(self): + self.stub(responses.POST, 'https://rest.nexmo.com/ussd/json') - @responses.activate - def test_get_voice_pricing(self): - self.stub(responses.GET, 'https://rest.nexmo.com/account/get-phone-pricing/outbound/voice') + params = {'from': 'MyCompany20', 'to': '447525856424', 'text': 'Hello'} - self.assertIsInstance(self.client.get_voice_pricing('447525856424'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('phone=447525856424', request_query()) + self.assertIsInstance(self.client.send_ussd_push_message(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('from=MyCompany20', request_body()) + self.assertIn('to=447525856424', request_body()) + self.assertIn('text=Hello', request_body()) - @responses.activate - def test_update_settings(self): - self.stub(responses.POST, 'https://rest.nexmo.com/account/settings') + @responses.activate + def test_send_ussd_prompt_message(self): + self.stub(responses.POST, 'https://rest.nexmo.com/ussd-prompt/json') - params = {'moCallBackUrl': 'http://example.com/callback'} + params = {'from': 'long-virtual-number', 'to': '447525856424', 'text': 'Hello'} - self.assertIsInstance(self.client.update_settings(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('moCallBackUrl=http%3A%2F%2Fexample.com%2Fcallback', request_body()) + self.assertIsInstance(self.client.send_ussd_prompt_message(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('from=long-virtual-number', request_body()) + self.assertIn('to=447525856424', request_body()) + self.assertIn('text=Hello', request_body()) - @responses.activate - def test_topup(self): - self.stub(responses.POST, 'https://rest.nexmo.com/account/top-up') + @responses.activate + def test_send_2fa_message(self): + self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/2fa/json') - params = {'trx': '00X123456Y7890123Z'} + params = {'to': '16365553226', 'pin': '1234'} - self.assertIsInstance(self.client.topup(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('trx=00X123456Y7890123Z', request_body()) + self.assertIsInstance(self.client.send_2fa_message(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('to=16365553226', request_body()) + self.assertIn('pin=1234', request_body()) - @responses.activate - def test_get_account_numbers(self): - self.stub(responses.GET, 'https://rest.nexmo.com/account/numbers') + @responses.activate + def test_send_event_alert_message(self): + self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/alert/json') - self.assertIsInstance(self.client.get_account_numbers(size=25), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('size=25', request_query()) + params = {'to': '16365553226', 'server': 'host', 'link': 'http://example.com/'} - @responses.activate - def test_get_available_numbers(self): - self.stub(responses.GET, 'https://rest.nexmo.com/number/search') + self.assertIsInstance(self.client.send_event_alert_message(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('to=16365553226', request_body()) + self.assertIn('server=host', request_body()) + self.assertIn('link=http%3A%2F%2Fexample.com%2F', request_body()) - self.assertIsInstance(self.client.get_available_numbers('CA', size=25), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('country=CA', request_query()) - self.assertIn('size=25', request_query()) + @responses.activate + def test_send_marketing_message(self): + self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/marketing/json') - @responses.activate - def test_buy_number(self): - self.stub(responses.POST, 'https://rest.nexmo.com/number/buy') + params = {'from': 'short-code', 'to': '16365553226', 'keyword': 'NEXMO', 'text': 'Hello'} - params = {'country': 'US', 'msisdn': 'number'} + self.assertIsInstance(self.client.send_marketing_message(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('from=short-code', request_body()) + self.assertIn('to=16365553226', request_body()) + self.assertIn('keyword=NEXMO', request_body()) + self.assertIn('text=Hello', request_body()) - self.assertIsInstance(self.client.buy_number(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('country=US', request_body()) - self.assertIn('msisdn=number', request_body()) + @responses.activate + def test_get_event_alert_numbers(self): + self.stub(responses.GET, 'https://rest.nexmo.com/sc/us/alert/opt-in/query/json') - @responses.activate - def test_cancel_number(self): - self.stub(responses.POST, 'https://rest.nexmo.com/number/cancel') + self.assertIsInstance(self.client.get_event_alert_numbers(), dict) + self.assertEqual(request_user_agent(), self.user_agent) - params = {'country': 'US', 'msisdn': 'number'} + @responses.activate + def test_resubscribe_event_alert_number(self): + self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/alert/opt-in/manage/json') - self.assertIsInstance(self.client.cancel_number(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('country=US', request_body()) - self.assertIn('msisdn=number', request_body()) + params = {'msisdn': '441632960960'} - @responses.activate - def test_update_number(self): - self.stub(responses.POST, 'https://rest.nexmo.com/number/update') + self.assertIsInstance(self.client.resubscribe_event_alert_number(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('msisdn=441632960960', request_body()) - params = {'country': 'US', 'msisdn': 'number', 'moHttpUrl': 'callback'} + @responses.activate + def test_initiate_call(self): + self.stub(responses.POST, 'https://rest.nexmo.com/call/json') - self.assertIsInstance(self.client.update_number(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('country=US', request_body()) - self.assertIn('msisdn=number', request_body()) - self.assertIn('moHttpUrl=callback', request_body()) + params = {'to': '16365553226', 'answer_url': 'http://example.com/answer'} - @responses.activate - def test_get_message(self): - self.stub(responses.GET, 'https://rest.nexmo.com/search/message') + self.assertIsInstance(self.client.initiate_call(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('to=16365553226', request_body()) + self.assertIn('answer_url=http%3A%2F%2Fexample.com%2Fanswer', request_body()) - self.assertIsInstance(self.client.get_message('00A0B0C0'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('id=00A0B0C0', request_query()) + @responses.activate + def test_initiate_tts_call(self): + self.stub(responses.POST, 'https://api.nexmo.com/tts/json') - @responses.activate - def test_get_message_rejections(self): - self.stub(responses.GET, 'https://rest.nexmo.com/search/rejections') + params = {'to': '16365553226', 'text': 'Hello'} - self.assertIsInstance(self.client.get_message_rejections(date='YYYY-MM-DD'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('date=YYYY-MM-DD', request_query()) + self.assertIsInstance(self.client.initiate_tts_call(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('to=16365553226', request_body()) + self.assertIn('text=Hello', request_body()) - @responses.activate - def test_search_messages(self): - self.stub(responses.GET, 'https://rest.nexmo.com/search/messages') + @responses.activate + def test_initiate_tts_prompt_call(self): + self.stub(responses.POST, 'https://api.nexmo.com/tts-prompt/json') - self.assertIsInstance(self.client.search_messages(to='1234567890', date='YYYY-MM-DD'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('date=YYYY-MM-DD', request_query()) - self.assertIn('to=1234567890', request_query()) + params = {'to': '16365553226', 'text': 'Hello', 'max_digits': 4, 'bye_text': 'Goodbye'} - @responses.activate - def test_search_messages_by_ids(self): - self.stub(responses.GET, 'https://rest.nexmo.com/search/messages') + self.assertIsInstance(self.client.initiate_tts_prompt_call(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('to=16365553226', request_body()) + self.assertIn('text=Hello', request_body()) + self.assertIn('max_digits=4', request_body()) + self.assertIn('bye_text=Goodbye', request_body()) - self.assertIsInstance(self.client.search_messages(ids=['00A0B0C0', '00A0B0C1', '00A0B0C2']), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('ids=00A0B0C0', request_query()) - self.assertIn('ids=00A0B0C1', request_query()) - self.assertIn('ids=00A0B0C2', request_query()) + @responses.activate + def test_start_verification(self): + self.stub(responses.POST, 'https://api.nexmo.com/verify/json') - @responses.activate - def test_send_ussd_push_message(self): - self.stub(responses.POST, 'https://rest.nexmo.com/ussd/json') + params = {'number': '447525856424', 'brand': 'MyApp'} - params = {'from': 'MyCompany20', 'to': '447525856424', 'text': 'Hello'} + self.assertIsInstance(self.client.start_verification(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('number=447525856424', request_body()) + self.assertIn('brand=MyApp', request_body()) - self.assertIsInstance(self.client.send_ussd_push_message(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('from=MyCompany20', request_body()) - self.assertIn('to=447525856424', request_body()) - self.assertIn('text=Hello', request_body()) + @responses.activate + def test_send_verification_request(self): + self.stub(responses.POST, 'https://api.nexmo.com/verify/json') - @responses.activate - def test_send_ussd_prompt_message(self): - self.stub(responses.POST, 'https://rest.nexmo.com/ussd-prompt/json') + params = {'number': '447525856424', 'brand': 'MyApp'} - params = {'from': 'long-virtual-number', 'to': '447525856424', 'text': 'Hello'} + self.assertIsInstance(self.client.send_verification_request(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('number=447525856424', request_body()) + self.assertIn('brand=MyApp', request_body()) - self.assertIsInstance(self.client.send_ussd_prompt_message(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('from=long-virtual-number', request_body()) - self.assertIn('to=447525856424', request_body()) - self.assertIn('text=Hello', request_body()) + @responses.activate + def test_check_verification(self): + self.stub(responses.POST, 'https://api.nexmo.com/verify/check/json') - @responses.activate - def test_send_2fa_message(self): - self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/2fa/json') + self.assertIsInstance(self.client.check_verification('8g88g88eg8g8gg9g90', code='123445'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('code=123445', request_body()) + self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) - params = {'to': '16365553226', 'pin': '1234'} + @responses.activate + def test_check_verification_request(self): + self.stub(responses.POST, 'https://api.nexmo.com/verify/check/json') - self.assertIsInstance(self.client.send_2fa_message(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('to=16365553226', request_body()) - self.assertIn('pin=1234', request_body()) + params = {'code': '123445', 'request_id': '8g88g88eg8g8gg9g90'} - @responses.activate - def test_send_event_alert_message(self): - self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/alert/json') + self.assertIsInstance(self.client.check_verification_request(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('code=123445', request_body()) + self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) - params = {'to': '16365553226', 'server': 'host', 'link': 'http://example.com/'} + @responses.activate + def test_get_verification(self): + self.stub(responses.GET, 'https://api.nexmo.com/verify/search/json') - self.assertIsInstance(self.client.send_event_alert_message(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('to=16365553226', request_body()) - self.assertIn('server=host', request_body()) - self.assertIn('link=http%3A%2F%2Fexample.com%2F', request_body()) + self.assertIsInstance(self.client.get_verification('xxx'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('request_id=xxx', request_query()) - @responses.activate - def test_send_marketing_message(self): - self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/marketing/json') + @responses.activate + def test_get_verification_request(self): + self.stub(responses.GET, 'https://api.nexmo.com/verify/search/json') - params = {'from': 'short-code', 'to': '16365553226', 'keyword': 'NEXMO', 'text': 'Hello'} + self.assertIsInstance(self.client.get_verification_request('xxx'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('request_id=xxx', request_query()) - self.assertIsInstance(self.client.send_marketing_message(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('from=short-code', request_body()) - self.assertIn('to=16365553226', request_body()) - self.assertIn('keyword=NEXMO', request_body()) - self.assertIn('text=Hello', request_body()) + @responses.activate + def test_cancel_verification(self): + self.stub(responses.POST, 'https://api.nexmo.com/verify/control/json') - @responses.activate - def test_get_event_alert_numbers(self): - self.stub(responses.GET, 'https://rest.nexmo.com/sc/us/alert/opt-in/query/json') + self.assertIsInstance(self.client.cancel_verification('8g88g88eg8g8gg9g90'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('cmd=cancel', request_body()) + self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) - self.assertIsInstance(self.client.get_event_alert_numbers(), dict) - self.assertEqual(request_user_agent(), self.user_agent) + @responses.activate + def test_trigger_next_verification_event(self): + self.stub(responses.POST, 'https://api.nexmo.com/verify/control/json') - @responses.activate - def test_resubscribe_event_alert_number(self): - self.stub(responses.POST, 'https://rest.nexmo.com/sc/us/alert/opt-in/manage/json') + self.assertIsInstance(self.client.trigger_next_verification_event('8g88g88eg8g8gg9g90'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('cmd=trigger_next_event', request_body()) + self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) - params = {'msisdn': '441632960960'} + @responses.activate + def test_control_verification_request(self): + self.stub(responses.POST, 'https://api.nexmo.com/verify/control/json') - self.assertIsInstance(self.client.resubscribe_event_alert_number(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('msisdn=441632960960', request_body()) + params = {'cmd': 'cancel', 'request_id': '8g88g88eg8g8gg9g90'} - @responses.activate - def test_initiate_call(self): - self.stub(responses.POST, 'https://rest.nexmo.com/call/json') + self.assertIsInstance(self.client.control_verification_request(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('cmd=cancel', request_body()) + self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) - params = {'to': '16365553226', 'answer_url': 'http://example.com/answer'} + @responses.activate + def test_get_basic_number_insight(self): + self.stub(responses.GET, 'https://api.nexmo.com/ni/basic/json') - self.assertIsInstance(self.client.initiate_call(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('to=16365553226', request_body()) - self.assertIn('answer_url=http%3A%2F%2Fexample.com%2Fanswer', request_body()) + self.assertIsInstance(self.client.get_basic_number_insight(number='447525856424'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('number=447525856424', request_query()) - @responses.activate - def test_initiate_tts_call(self): - self.stub(responses.POST, 'https://api.nexmo.com/tts/json') + @responses.activate + def test_get_standard_number_insight(self): + self.stub(responses.GET, 'https://api.nexmo.com/ni/standard/json') - params = {'to': '16365553226', 'text': 'Hello'} + self.assertIsInstance(self.client.get_standard_number_insight(number='447525856424'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('number=447525856424', request_query()) - self.assertIsInstance(self.client.initiate_tts_call(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('to=16365553226', request_body()) - self.assertIn('text=Hello', request_body()) + @responses.activate + def test_get_number_insight(self): + self.stub(responses.GET, 'https://api.nexmo.com/number/lookup/json') - @responses.activate - def test_initiate_tts_prompt_call(self): - self.stub(responses.POST, 'https://api.nexmo.com/tts-prompt/json') + self.assertIsInstance(self.client.get_number_insight(number='447525856424'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('number=447525856424', request_query()) - params = {'to': '16365553226', 'text': 'Hello', 'max_digits': 4, 'bye_text': 'Goodbye'} + @responses.activate + def test_get_advanced_number_insight(self): + self.stub(responses.GET, 'https://api.nexmo.com/ni/advanced/json') - self.assertIsInstance(self.client.initiate_tts_prompt_call(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('to=16365553226', request_body()) - self.assertIn('text=Hello', request_body()) - self.assertIn('max_digits=4', request_body()) - self.assertIn('bye_text=Goodbye', request_body()) + self.assertIsInstance(self.client.get_advanced_number_insight(number='447525856424'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('number=447525856424', request_query()) - @responses.activate - def test_start_verification(self): - self.stub(responses.POST, 'https://api.nexmo.com/verify/json') + @responses.activate + def test_request_number_insight(self): + self.stub(responses.POST, 'https://rest.nexmo.com/ni/json') - params = {'number': '447525856424', 'brand': 'MyApp'} + params = {'number': '447525856424', 'callback': 'https://example.com'} - self.assertIsInstance(self.client.start_verification(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('number=447525856424', request_body()) - self.assertIn('brand=MyApp', request_body()) + self.assertIsInstance(self.client.request_number_insight(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('number=447525856424', request_body()) + self.assertIn('callback=https%3A%2F%2Fexample.com', request_body()) - @responses.activate - def test_send_verification_request(self): - self.stub(responses.POST, 'https://api.nexmo.com/verify/json') + @responses.activate + def test_get_applications(self): + self.stub(responses.GET, 'https://api.nexmo.com/v1/applications') - params = {'number': '447525856424', 'brand': 'MyApp'} + self.assertIsInstance(self.client.get_applications(), dict) + self.assertEqual(request_user_agent(), self.user_agent) - self.assertIsInstance(self.client.send_verification_request(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('number=447525856424', request_body()) - self.assertIn('brand=MyApp', request_body()) + @responses.activate + def test_get_application(self): + self.stub(responses.GET, 'https://api.nexmo.com/v1/applications/xx-xx-xx-xx') - @responses.activate - def test_check_verification(self): - self.stub(responses.POST, 'https://api.nexmo.com/verify/check/json') + self.assertIsInstance(self.client.get_application('xx-xx-xx-xx'), dict) + self.assertEqual(request_user_agent(), self.user_agent) - self.assertIsInstance(self.client.check_verification('8g88g88eg8g8gg9g90', code='123445'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('code=123445', request_body()) - self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) + @responses.activate + def test_create_application(self): + self.stub(responses.POST, 'https://api.nexmo.com/v1/applications') - @responses.activate - def test_check_verification_request(self): - self.stub(responses.POST, 'https://api.nexmo.com/verify/check/json') + params = {'name': 'Example App', 'type': 'voice'} - params = {'code': '123445', 'request_id': '8g88g88eg8g8gg9g90'} + self.assertIsInstance(self.client.create_application(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertIn('name=Example+App', request_body()) + self.assertIn('type=voice', request_body()) - self.assertIsInstance(self.client.check_verification_request(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('code=123445', request_body()) - self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) + @responses.activate + def test_update_application(self): + self.stub(responses.PUT, 'https://api.nexmo.com/v1/applications/xx-xx-xx-xx') - @responses.activate - def test_get_verification(self): - self.stub(responses.GET, 'https://api.nexmo.com/verify/search/json') + params = {'answer_url': 'https://example.com/ncco'} - self.assertIsInstance(self.client.get_verification('xxx'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('request_id=xxx', request_query()) + self.assertIsInstance(self.client.update_application('xx-xx-xx-xx', params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertEqual(request_content_type(), 'application/json') + self.assertIn(b'"answer_url": "https://example.com/ncco"', request_body()) - @responses.activate - def test_get_verification_request(self): - self.stub(responses.GET, 'https://api.nexmo.com/verify/search/json') + @responses.activate + def test_delete_application(self): + responses.add(responses.DELETE, 'https://api.nexmo.com/v1/applications/xx-xx-xx-xx', status=204) - self.assertIsInstance(self.client.get_verification_request('xxx'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('request_id=xxx', request_query()) + self.assertEqual(None, self.client.delete_application('xx-xx-xx-xx')) + self.assertEqual(request_user_agent(), self.user_agent) - @responses.activate - def test_cancel_verification(self): - self.stub(responses.POST, 'https://api.nexmo.com/verify/control/json') + @responses.activate + def test_create_call(self): + self.stub(responses.POST, 'https://api.nexmo.com/v1/calls') - self.assertIsInstance(self.client.cancel_verification('8g88g88eg8g8gg9g90'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('cmd=cancel', request_body()) - self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) + params = { + 'to': [{'type': 'phone', 'number': '14843331234'}], + 'from': {'type': 'phone', 'number': '14843335555'}, + 'answer_url': ['https://example.com/answer'] + } - @responses.activate - def test_trigger_next_verification_event(self): - self.stub(responses.POST, 'https://api.nexmo.com/verify/control/json') + self.assertIsInstance(self.client.create_call(params), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertEqual(request_content_type(), 'application/json') - self.assertIsInstance(self.client.trigger_next_verification_event('8g88g88eg8g8gg9g90'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('cmd=trigger_next_event', request_body()) - self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) + @responses.activate + def test_get_calls(self): + self.stub(responses.GET, 'https://api.nexmo.com/v1/calls') - @responses.activate - def test_control_verification_request(self): - self.stub(responses.POST, 'https://api.nexmo.com/verify/control/json') + self.assertIsInstance(self.client.get_calls(), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertRegex(request_authorization(), r'\ABearer ') - params = {'cmd': 'cancel', 'request_id': '8g88g88eg8g8gg9g90'} + @responses.activate + def test_get_call(self): + self.stub(responses.GET, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') - self.assertIsInstance(self.client.control_verification_request(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('cmd=cancel', request_body()) - self.assertIn('request_id=8g88g88eg8g8gg9g90', request_body()) + self.assertIsInstance(self.client.get_call('xx-xx-xx-xx'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertRegex(request_authorization(), r'\ABearer ') - @responses.activate - def test_get_basic_number_insight(self): - self.stub(responses.GET, 'https://api.nexmo.com/number/format/json') + @responses.activate + def test_update_call(self): + self.stub(responses.PUT, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') - self.assertIsInstance(self.client.get_basic_number_insight(number='447525856424'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('number=447525856424', request_query()) + self.assertIsInstance(self.client.update_call('xx-xx-xx-xx', action='hangup'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertEqual(request_content_type(), 'application/json') + self.assertEqual(request_body(), b'{"action": "hangup"}') - @responses.activate - def test_get_number_insight(self): - self.stub(responses.GET, 'https://api.nexmo.com/number/lookup/json') + @responses.activate + def test_send_audio(self): + self.stub(responses.PUT, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx/stream') - self.assertIsInstance(self.client.get_number_insight(number='447525856424'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('number=447525856424', request_query()) + self.assertIsInstance(self.client.send_audio('xx-xx-xx-xx', stream_url='http://example.com/audio.mp3'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertEqual(request_content_type(), 'application/json') + self.assertEqual(request_body(), b'{"stream_url": "http://example.com/audio.mp3"}') - @responses.activate - def test_request_number_insight(self): - self.stub(responses.POST, 'https://rest.nexmo.com/ni/json') + @responses.activate + def test_stop_audio(self): + self.stub(responses.DELETE, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx/stream') - params = {'number': '447525856424', 'callback': 'https://example.com'} + self.assertIsInstance(self.client.stop_audio('xx-xx-xx-xx'), dict) + self.assertEqual(request_user_agent(), self.user_agent) - self.assertIsInstance(self.client.request_number_insight(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('number=447525856424', request_body()) - self.assertIn('callback=https%3A%2F%2Fexample.com', request_body()) + @responses.activate + def test_send_speech(self): + self.stub(responses.PUT, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx/talk') - @responses.activate - def test_get_applications(self): - self.stub(responses.GET, 'https://api.nexmo.com/v1/applications') + self.assertIsInstance(self.client.send_speech('xx-xx-xx-xx', text='Hello'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertEqual(request_content_type(), 'application/json') + self.assertEqual(request_body(), b'{"text": "Hello"}') - self.assertIsInstance(self.client.get_applications(), dict) - self.assertEqual(request_user_agent(), self.user_agent) + @responses.activate + def test_stop_speech(self): + self.stub(responses.DELETE, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx/talk') - @responses.activate - def test_get_application(self): - self.stub(responses.GET, 'https://api.nexmo.com/v1/applications/xx-xx-xx-xx') + self.assertIsInstance(self.client.stop_speech('xx-xx-xx-xx'), dict) + self.assertEqual(request_user_agent(), self.user_agent) - self.assertIsInstance(self.client.get_application('xx-xx-xx-xx'), dict) - self.assertEqual(request_user_agent(), self.user_agent) + @responses.activate + def test_send_dtmf(self): + self.stub(responses.PUT, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx/dtmf') - @responses.activate - def test_create_application(self): - self.stub(responses.POST, 'https://api.nexmo.com/v1/applications') + self.assertIsInstance(self.client.send_dtmf('xx-xx-xx-xx', digits='1234'), dict) + self.assertEqual(request_user_agent(), self.user_agent) + self.assertEqual(request_content_type(), 'application/json') + self.assertEqual(request_body(), b'{"digits": "1234"}') - params = {'name': 'Example App', 'type': 'voice'} + @responses.activate + def test_user_provided_authorization(self): + self.stub(responses.GET, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') - self.assertIsInstance(self.client.create_application(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertIn('name=Example+App', request_body()) - self.assertIn('type=voice', request_body()) + application_id = 'different-nexmo-application-id' + nbf = int(time.time()) + exp = nbf + 3600 - @responses.activate - def test_update_application(self): - self.stub(responses.PUT, 'https://api.nexmo.com/v1/applications/xx-xx-xx-xx') + self.client.auth(application_id=application_id, nbf=nbf, exp=exp) + self.client.get_call('xx-xx-xx-xx') - params = {'answer_url': 'https://example.com/ncco'} + token = request_authorization().split()[1] - self.assertIsInstance(self.client.update_application('xx-xx-xx-xx', params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertEqual(request_content_type(), 'application/json') - self.assertIn(b'"answer_url": "https://example.com/ncco"', request_body()) + token = jwt.decode(token, self.public_key, algorithm='RS256') - @responses.activate - def test_delete_application(self): - responses.add(responses.DELETE, 'https://api.nexmo.com/v1/applications/xx-xx-xx-xx', status=204) + self.assertEqual(token['application_id'], application_id) + self.assertEqual(token['nbf'], nbf) + self.assertEqual(token['exp'], exp) - self.assertEqual(None, self.client.delete_application('xx-xx-xx-xx')) - self.assertEqual(request_user_agent(), self.user_agent) + @responses.activate + def test_authorization_with_private_key_path(self): + self.stub(responses.GET, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') - @responses.activate - def test_create_call(self): - self.stub(responses.POST, 'https://api.nexmo.com/v1/calls') + private_key = 'test/private_key.txt' - params = { - 'to': [{'type': 'phone', 'number': '14843331234'}], - 'from': {'type': 'phone', 'number': '14843335555'}, - 'answer_url': ['https://example.com/answer'] - } + self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, application_id=self.application_id, + private_key=private_key) + self.client.get_call('xx-xx-xx-xx') - self.assertIsInstance(self.client.create_call(params), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertEqual(request_content_type(), 'application/json') + token = request_authorization().split()[1] - @responses.activate - def test_get_calls(self): - self.stub(responses.GET, 'https://api.nexmo.com/v1/calls') + token = jwt.decode(token, self.public_key, algorithm='RS256') - self.assertIsInstance(self.client.get_calls(), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertRegexpMatches(request_authorization(), r'\ABearer ') + self.assertEqual(token['application_id'], self.application_id) - @responses.activate - def test_get_call(self): - self.stub(responses.GET, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') + @responses.activate + def test_authorization_with_private_key_object(self): + self.stub(responses.GET, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') - self.assertIsInstance(self.client.get_call('xx-xx-xx-xx'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertRegexpMatches(request_authorization(), r'\ABearer ') + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization - @responses.activate - def test_update_call(self): - self.stub(responses.PUT, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') + private_key = serialization.load_pem_private_key(self.private_key.encode('utf-8'), password=None, + backend=default_backend()) - self.assertIsInstance(self.client.update_call('xx-xx-xx-xx', action='hangup'), dict) - self.assertEqual(request_user_agent(), self.user_agent) - self.assertEqual(request_content_type(), 'application/json') - self.assertEqual(request_body(), b'{"action": "hangup"}') + self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, application_id=self.application_id, + private_key=private_key) + self.client.get_call('xx-xx-xx-xx') - @responses.activate - def test_user_provided_authorization(self): - self.stub(responses.GET, 'https://api.nexmo.com/v1/calls/xx-xx-xx-xx') + token = request_authorization().split()[1] - application_id = 'different-nexmo-application-id' - nbf = int(time.time()) - exp = nbf + 3600 + token = jwt.decode(token, self.public_key, algorithm='RS256') - self.client.auth(application_id=application_id, nbf=nbf, exp=exp) - self.client.get_call('xx-xx-xx-xx') + self.assertEqual(token['application_id'], self.application_id) - token = request_authorization().split()[1] + @responses.activate + def test_authentication_error(self): + responses.add(responses.POST, 'https://rest.nexmo.com/sms/json', status=401) - token = jwt.decode(token, self.public_key, algorithm='RS256') + self.assertRaises(nexmo.AuthenticationError, self.client.send_message, {}) - self.assertEqual(token['application_id'], application_id) - self.assertEqual(token['nbf'], nbf) - self.assertEqual(token['exp'], exp) + @responses.activate + def test_client_error(self): + responses.add(responses.POST, 'https://rest.nexmo.com/sms/json', status=400) - @responses.activate - def test_authentication_error(self): - responses.add(responses.POST, 'https://rest.nexmo.com/sms/json', status=401) + message = '400 response from rest.nexmo.com' - self.assertRaises(nexmo.AuthenticationError, self.client.send_message, {}) + self.assertRaisesRegex(nexmo.ClientError, message, self.client.send_message, {}) - @responses.activate - def test_client_error(self): - responses.add(responses.POST, 'https://rest.nexmo.com/sms/json', status=400) + @responses.activate + def test_server_error(self): + responses.add(responses.POST, 'https://rest.nexmo.com/sms/json', status=500) - message = '400 response from rest.nexmo.com' + message = '500 response from rest.nexmo.com' - self.assertRaisesRegex(nexmo.ClientError, message, self.client.send_message, {}) + self.assertRaisesRegex(nexmo.ServerError, message, self.client.send_message, {}) - @responses.activate - def test_server_error(self): - responses.add(responses.POST, 'https://rest.nexmo.com/sms/json', status=500) + @responses.activate + def test_application_info_options(self): + app_name, app_version = 'ExampleApp', 'X.Y.Z' - message = '500 response from rest.nexmo.com' + self.stub(responses.GET, 'https://rest.nexmo.com/account/get-balance') - self.assertRaisesRegex(nexmo.ServerError, message, self.client.send_message, {}) + self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, app_name=app_name, app_version=app_version) + self.user_agent = '/'.join( + ['nexmo-python', nexmo.__version__, platform.python_version(), app_name, app_version]) - @responses.activate - def test_application_info_options(self): - app_name, app_version = 'ExampleApp', 'X.Y.Z' + self.assertIsInstance(self.client.get_balance(), dict) + self.assertEqual(request_user_agent(), self.user_agent) - self.stub(responses.GET, 'https://rest.nexmo.com/account/get-balance') + def test_check_signature(self): + params = {'a': '1', 'b': '2', 'timestamp': '1461605396', 'sig': '6af838ef94998832dbfc29020b564830'} - self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, app_name=app_name, app_version=app_version) - self.user_agent = '/'.join(['nexmo-python', nexmo.__version__, platform.python_version(), app_name, app_version]) + self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, signature_secret='secret') - self.assertIsInstance(self.client.get_balance(), dict) - self.assertEqual(request_user_agent(), self.user_agent) + self.assertTrue(self.client.check_signature(params)) - def test_check_signature(self): - params = {'a': '1', 'b': '2', 'timestamp': '1461605396', 'sig': '6af838ef94998832dbfc29020b564830'} + def test_signature(self): + params = {'a': '1', 'b': '2', 'timestamp': '1461605396'} - self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, signature_secret='secret') + self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, signature_secret='secret') - self.assertTrue(self.client.check_signature(params)) + self.assertEqual(self.client.signature(params), '6af838ef94998832dbfc29020b564830') - def test_signature(self): - params = {'a': '1', 'b': '2', 'timestamp': '1461605396'} + def test_client_doesnt_require_api_key(self): + client = nexmo.Client(application_id='myid', private_key='abc\nde') + self.assertIsNotNone(client) + self.assertIsNone(client.api_key) + self.assertIsNone(client.api_secret) - self.client = nexmo.Client(key=self.api_key, secret=self.api_secret, signature_secret='secret') + @responses.activate + def test_client_can_make_application_requests_without_api_key(self): + self.stub(responses.POST, 'https://api.nexmo.com/v1/calls') - self.assertEqual(self.client.signature(params), '6af838ef94998832dbfc29020b564830') + client = nexmo.Client(application_id='myid', private_key=self.private_key) + client.create_call("123455") if __name__ == '__main__': - unittest.main() + unittest.main()