├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── bumpversion.sh ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── rinse.client.rst ├── rinse.message.rst ├── rinse.response.rst ├── rinse.rst ├── rinse.util.rst ├── rinse.wsa.rst ├── rinse.wsdl.rst ├── rinse.wsse.rst └── rinse.xsd.rst ├── requirements.txt ├── rinse ├── __init__.py ├── client.py ├── message.py ├── res │ ├── soap-1.1.xsd │ ├── soap-1.1_envelope.xsd │ └── soap-1.2.xsd ├── response.py ├── tests │ ├── __init__.py │ ├── test_client.py │ └── utils.py ├── util.py ├── wsa.py ├── wsdl.py ├── wsse.py └── xsd.py ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | .eggs/ 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - '3.8' 5 | - '3.7' 6 | - '3.6' 7 | - '3.5' 8 | - '2.7' 9 | install: 10 | - pip install -r requirements.txt 11 | - pip install coveralls 12 | script: 13 | - coverage run --source=rinse setup.py test 14 | after_success: 15 | - coveralls 16 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.5.0 5 | ----- 6 | * Allow passing a ``timeout`` argument to the client. 7 | * Add Python 3.6, 3.7 and 3.8 to test builds. 8 | 9 | 0.4.0 10 | ----- 11 | * Use ``@cached_property`` to simplify property code. 12 | * Fix ``AttributeError`` when debugging. 13 | * Include missing XSD files in wheel distributions. 14 | * Ensure XSD files exist in distributed files via tox test suite 15 | options. 16 | 17 | 0.3.0 18 | ----- 19 | * Add 'SOAPAction' header to requests. 20 | * Expose requests.Session. 21 | * Include missing XSD files in package data. 22 | * Add Python 3.4 to test builds. 23 | 24 | 0.2.0 25 | ----- 26 | * Declared BETA status. 27 | * Homepage URL set to https://rinse.readthedocs.org/. 28 | 29 | 0.1.3 30 | ----- 31 | * Add links to README. 32 | 33 | 0.1.2 34 | ----- 35 | * Added Sphinx documentation. 36 | 37 | 0.1.1 38 | ----- 39 | * Add ElementMaker wrapper class and safe_parse_url function. 40 | * Add travis-ci.org build configuration. 41 | * Split features into submodules. 42 | 43 | 0.1.0 44 | ----- 45 | * Support for WSDL. 46 | * Simplified usage through use of SoapClient and SoapMessage classes. 47 | 48 | 0.0.5 49 | ----- 50 | * Pylint/PEP8/pychecker fixes. 51 | 52 | 0.0.4 53 | ----- 54 | * Remove reference to stale source (client.py). 55 | 56 | 0.0.3 57 | ----- 58 | * Add defused.xml to requirements. 59 | 60 | 0.0.2 61 | ----- 62 | * Validate messages against SOAP 1.1 Envelope and SOAP 1.2 XML schema. 63 | * Add support for WSSE (Security) headers. 64 | 65 | 0.0.1 66 | ----- 67 | * Generate SOAP requests and parse SOAP 1.1 responses. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tyson Clugg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.rst 3 | include *.sh 4 | include *.txt 5 | include Makefile 6 | recursive-include docs *.bat 7 | recursive-include docs *.py 8 | recursive-include docs *.rst 9 | recursive-include docs Makefile 10 | recursive-include rinse *.xsd 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := $(shell python setup.py --name) 2 | VERSION := $(shell python setup.py --version) 3 | 4 | SDIST := dist/${NAME}-${VERSION}.tar.gz 5 | WHEEL := dist/${NAME}-${VERSION}-py2.py3-none-any.whl 6 | 7 | .PHONY: all test clean clean-docs upload-docs upload-pypi dist docs 8 | 9 | all: docs dist 10 | 11 | test: 12 | tox 13 | 14 | clean: clean-docs clean-sdist clean-wheel 15 | 16 | clean-docs: 17 | $(MAKE) -C docs/ clean 18 | 19 | clean-sdist: 20 | rm -f "${SDIST}" 21 | 22 | clean-wheel: 23 | rm -f "${WHEEL}" 24 | 25 | docs: 26 | $(MAKE) -C docs/ clean html 27 | 28 | ${SDIST}: 29 | python setup.py sdist 30 | 31 | ${WHEEL}: 32 | python setup.py bdist_wheel 33 | 34 | dist: test ${SDIST} ${WHEEL} 35 | 36 | upload: upload-pypi upload-docs 37 | 38 | upload-pypi: ${SDIST} ${WHEEL} 39 | twine upload "${WHEEL}" "${SDIST}" 40 | 41 | upload-docs: docs/_build/ 42 | python setup.py upload_sphinx --upload-dir="$ setup.py 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/rinse.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/rinse.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/rinse" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/rinse" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # rinse documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Aug 29 15:57:05 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import rinse 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.ifconfig', 39 | 'sphinx.ext.viewcode', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix of source filenames. 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'rinse' 56 | copyright = '2014, Tyson Clugg' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The full version, including alpha/beta/rc tags. 63 | release = rinse.__version__ 64 | # The short X.Y version. 65 | version = '.'.join(release.split('.')[0:2]) 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | #language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | html_theme = 'default' 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | #html_theme_options = {} 116 | 117 | # Add any paths that contain custom themes here, relative to this directory. 118 | #html_theme_path = [] 119 | 120 | # The name for this set of Sphinx documents. If None, it defaults to 121 | # " v documentation". 122 | #html_title = None 123 | 124 | # A shorter title for the navigation bar. Default is the same as html_title. 125 | #html_short_title = None 126 | 127 | # The name of an image file (relative to this directory) to place at the top 128 | # of the sidebar. 129 | #html_logo = None 130 | 131 | # The name of an image file (within the static path) to use as favicon of the 132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 133 | # pixels large. 134 | #html_favicon = None 135 | 136 | # Add any paths that contain custom static files (such as style sheets) here, 137 | # relative to this directory. They are copied after the builtin static files, 138 | # so a file named "default.css" will overwrite the builtin "default.css". 139 | html_static_path = ['_static'] 140 | 141 | # Add any extra paths that contain custom files (such as robots.txt or 142 | # .htaccess) here, relative to this directory. These files are copied 143 | # directly to the root of the documentation. 144 | #html_extra_path = [] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | #html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | #html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | #html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | #html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = 'rinsedoc' 189 | 190 | 191 | # -- Options for LaTeX output --------------------------------------------- 192 | 193 | latex_elements = { 194 | # The paper size ('letterpaper' or 'a4paper'). 195 | 'papersize': 'a4paper', 196 | 197 | # The font size ('10pt', '11pt' or '12pt'). 198 | #'pointsize': '10pt', 199 | 200 | # Additional stuff for the LaTeX preamble. 201 | #'preamble': '', 202 | } 203 | 204 | # Grouping the document tree into LaTeX files. List of tuples 205 | # (source start file, target name, title, 206 | # author, documentclass [howto, manual, or own class]). 207 | latex_documents = [ 208 | ('index', 'rinse.tex', 'rinse Documentation', 209 | 'Tyson Clugg', 'manual'), 210 | ] 211 | 212 | # The name of an image file (relative to this directory) to place at the top of 213 | # the title page. 214 | #latex_logo = None 215 | 216 | # For "manual" documents, if this is true, then toplevel headings are parts, 217 | # not chapters. 218 | #latex_use_parts = False 219 | 220 | # If true, show page references after internal links. 221 | #latex_show_pagerefs = False 222 | 223 | # If true, show URL addresses after external links. 224 | #latex_show_urls = False 225 | 226 | # Documents to append as an appendix to all manuals. 227 | #latex_appendices = [] 228 | 229 | # If false, no module index is generated. 230 | #latex_domain_indices = True 231 | 232 | 233 | # -- Options for manual page output --------------------------------------- 234 | 235 | # One entry per manual page. List of tuples 236 | # (source start file, name, description, authors, manual section). 237 | man_pages = [ 238 | ('index', 'rinse', 'rinse Documentation', 239 | ['Tyson Clugg'], 1) 240 | ] 241 | 242 | # If true, show URL addresses after external links. 243 | #man_show_urls = False 244 | 245 | 246 | # -- Options for Texinfo output ------------------------------------------- 247 | 248 | # Grouping the document tree into Texinfo files. List of tuples 249 | # (source start file, target name, title, author, 250 | # dir menu entry, description, category) 251 | texinfo_documents = [ 252 | ('index', 'rinse', 'rinse Documentation', 253 | 'Tyson Clugg', 'rinse', 'One line description of project.', 254 | 'Miscellaneous'), 255 | ] 256 | 257 | # Documents to append as an appendix to all manuals. 258 | #texinfo_appendices = [] 259 | 260 | # If false, no module index is generated. 261 | #texinfo_domain_indices = True 262 | 263 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 264 | #texinfo_show_urls = 'footnote' 265 | 266 | # If true, do not generate a @detailmenu in the "Top" node's menu. 267 | #texinfo_no_detailmenu = False 268 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. rinse documentation master file, created by 2 | sphinx-quickstart on Fri Aug 29 15:57:05 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../README.rst 7 | 8 | Installation: 9 | ------------- 10 | Rinse_ is available for installation direct from PyPi_: 11 | 12 | .. code:: bash 13 | 14 | pip install rinse 15 | 16 | Links: 17 | ------ 18 | 19 | .. image:: https://readthedocs.org/projects/rinse/badge/?version=latest 20 | :target: https://readthedocs.org/projects/rinse/?badge=latest 21 | :alt: Documentation Status 22 | :align: right 23 | 24 | The latest documentation is available online at 25 | https://rinse.readthedocs.org/. 26 | 27 | Source code is available online at https://github.com/tysonclugg/rinse. 28 | 29 | You can contribute to this project on the `rinse GitHub`_ page. 30 | 31 | Contents: 32 | --------- 33 | 34 | .. toctree:: 35 | :glob: 36 | :maxdepth: 2 37 | 38 | rinse* 39 | 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | 48 | .. include:: ../CHANGELOG.rst 49 | 50 | License 51 | ======= 52 | .. include:: ../LICENSE 53 | 54 | .. _rinse GitHub: https://rinse.readthedocs.org/en/latest/ 55 | .. _pypi: https://pypi.python.org/pypi/rinse 56 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\rinse.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\rinse.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/rinse.client.rst: -------------------------------------------------------------------------------- 1 | rinse.client 2 | ============ 3 | 4 | .. automodule:: rinse.client 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.message.rst: -------------------------------------------------------------------------------- 1 | rinse.message 2 | ============= 3 | 4 | .. automodule:: rinse.message 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.response.rst: -------------------------------------------------------------------------------- 1 | rinse.response 2 | ============== 3 | 4 | .. automodule:: rinse.response 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.rst: -------------------------------------------------------------------------------- 1 | rinse 2 | ===== 3 | 4 | .. automodule:: rinse 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.util.rst: -------------------------------------------------------------------------------- 1 | rinse.util 2 | ========== 3 | 4 | .. automodule:: rinse.util 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.wsa.rst: -------------------------------------------------------------------------------- 1 | rinse.wsa 2 | ========= 3 | 4 | .. automodule:: rinse.wsa 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.wsdl.rst: -------------------------------------------------------------------------------- 1 | rinse.wsdl 2 | ========== 3 | 4 | .. automodule:: rinse.wsdl 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.wsse.rst: -------------------------------------------------------------------------------- 1 | rinse.wsse 2 | ========== 3 | 4 | .. automodule:: rinse.wsse 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/rinse.xsd.rst: -------------------------------------------------------------------------------- 1 | rinse.xsd 2 | ========= 3 | 4 | .. automodule:: rinse.xsd 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | defusedxml>=0.4.1 2 | lxml>=3.3.5 3 | requests>=2.3.0 4 | -------------------------------------------------------------------------------- /rinse/__init__.py: -------------------------------------------------------------------------------- 1 | """SOAP client.""" 2 | import os.path 3 | from pkg_resources import get_distribution, DistributionNotFound 4 | 5 | 6 | RINSE_DIR = os.path.dirname(__file__) 7 | ENVELOPE_XSD = 'soap-1.1_envelope.xsd' 8 | 9 | NS_SOAPENV = 'http://schemas.xmlsoap.org/soap/envelope/' 10 | 11 | NS_MAP = { 12 | 'soapenv': NS_SOAPENV, 13 | } 14 | 15 | try: 16 | _dist = get_distribution('rinse') 17 | if not __file__.startswith(os.path.join(_dist.location, 'rinse', '')): 18 | # not installed, but there is another version that *is* 19 | raise DistributionNotFound 20 | except DistributionNotFound: 21 | __version__ = 'development' 22 | else: 23 | __version__ = _dist.version 24 | -------------------------------------------------------------------------------- /rinse/client.py: -------------------------------------------------------------------------------- 1 | """SOAP client.""" 2 | from __future__ import print_function 3 | import requests 4 | from rinse import ENVELOPE_XSD 5 | from rinse.util import SCHEMA, cached_property 6 | from rinse.response import RinseResponse 7 | 8 | 9 | class SoapClient(object): 10 | 11 | """Rinse SOAP client.""" 12 | 13 | def __init__(self, url, debug=False, **kwargs): 14 | """Set base attributes.""" 15 | self.url = url 16 | self.debug = debug 17 | self.timeout = kwargs.pop('timeout', None) 18 | self.kwargs = kwargs 19 | self.operations = {} 20 | self.soap_schema = SCHEMA[ENVELOPE_XSD] 21 | 22 | @cached_property 23 | def _session(self): 24 | """Cached instance of requests.Session.""" 25 | return requests.Session() 26 | 27 | def __call__(self, msg, action="", build_response=RinseResponse, 28 | debug=False, **kwargs): 29 | """Post 'msg' to remote service.""" 30 | # generate HTTP request from msg 31 | request = msg.request(self.url, action).prepare() 32 | if debug or self.debug: 33 | print('{} {}'.format(request.method, self.url)) 34 | print( 35 | ''.join( 36 | '{}: {}\n'.format(name, val) 37 | for name, val 38 | in sorted(request.headers.items()) 39 | ) 40 | ) 41 | print(request.body.decode('utf-8')) 42 | 43 | # perform HTTP(s) POST 44 | resp = self._session.send(request, timeout=kwargs.get('timeout', self.timeout)) 45 | return build_response(resp) 46 | -------------------------------------------------------------------------------- /rinse/message.py: -------------------------------------------------------------------------------- 1 | """SOAP client.""" 2 | from __future__ import print_function 3 | from lxml import etree 4 | import requests 5 | from rinse import NS_SOAPENV 6 | from rinse.util import ElementMaker 7 | 8 | 9 | class SoapMessage(object): 10 | 11 | """SOAP message. 12 | 13 | >>> from rinse.message import SoapMessage 14 | >>> from lxml import etree 15 | >>> from rinse.util import printxml 16 | >>> body = etree.Element('test') 17 | >>> msg = SoapMessage(body) 18 | >>> printxml(msg.etree()) 19 | 20 | 21 | 22 | 23 | 24 | 25 | """ 26 | 27 | elementmaker_cls = ElementMaker 28 | 29 | def __init__(self, body=None): 30 | """Set base attributes.""" 31 | # XML namespace map 32 | self._nsmap = {} 33 | # cache of lxml.etree.ElementMaker instances by namespace prefix 34 | self._elementmaker_cache = {} 35 | # SOAP headers 36 | self.headers = [] 37 | # SOAP body 38 | self.body = body 39 | # HTTP headers 40 | self.http_headers = { 41 | 'Content-Type': 'text/xml;charset=UTF-8', 42 | } 43 | 44 | def __getitem__(self, key): 45 | """Dict style access to http_headers.""" 46 | return self.http_headers[key] 47 | 48 | def __setitem__(self, key, val): 49 | """Dict style access to http_headers.""" 50 | self.http_headers[key] = val 51 | 52 | def __delitem__(self, key): 53 | """Dict style access to http_headers.""" 54 | del self.http_headers[key] 55 | 56 | def elementmaker(self, prefix, url): 57 | """Register namespace and return ElementMaker bound to the namespace.""" 58 | try: 59 | old_url = self._nsmap[prefix] 60 | if url != old_url: 61 | raise ValueError( 62 | 'Namespace {!r} already defined as {!r}.'.format( 63 | prefix, 64 | old_url, 65 | ), 66 | ) 67 | except KeyError: 68 | self._nsmap[prefix] = url 69 | self._elementmaker_cache[prefix] = self.elementmaker_cls( 70 | namespace=url, nsmap=self._nsmap, 71 | ) 72 | return self._elementmaker_cache[prefix] 73 | 74 | def etree(self): 75 | """Generate a SOAP Envelope message with header and body elements.""" 76 | soapenv = self.elementmaker('soapenv', NS_SOAPENV) 77 | return soapenv.Envelope( 78 | soapenv.Header(*self.headers), 79 | soapenv.Body(self.body), 80 | ) 81 | 82 | def tostring(self, **kwargs): 83 | """Generate XML representation of self.""" 84 | return etree.tostring(self.etree(), **kwargs) 85 | 86 | def request(self, url=None, action=None): 87 | """Generate a requests.Request instance.""" 88 | headers = self.http_headers.copy() 89 | if action is not None: 90 | headers['SOAPAction'] = action 91 | return requests.Request( 92 | 'POST', 93 | url or self.url, 94 | data=self.tostring(pretty_print=True, encoding='utf-8'), 95 | headers=headers, 96 | ) 97 | 98 | def __bytes__(self): 99 | """Generate XML (bytes).""" 100 | return self.tostring() 101 | 102 | def __str__(self): 103 | """Generate XML (unicode).""" 104 | return self.tostring(encoding='unicode') 105 | -------------------------------------------------------------------------------- /rinse/res/soap-1.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | "encodingStyle" indicates any canonicalization conventions followed in the contents of the containing element. For example, the value "http://schemas.xmlsoap.org/soap/encoding/" indicates the pattern described in SOAP specification 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /rinse/res/soap-1.1_envelope.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Prose in the spec does not specify that attributes are allowed on the Body element 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | Fault reporting structure 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /rinse/res/soap-1.2.xsd: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Elements replacing the wildcard MUST be namespace qualified, but can be in the targetNamespace 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Fault reporting structure 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 106 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 126 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /rinse/response.py: -------------------------------------------------------------------------------- 1 | """SOAP client.""" 2 | import collections 3 | from rinse import NS_MAP 4 | from rinse.util import safe_parse_string 5 | 6 | 7 | class Response(object): 8 | 9 | """Rinse Response object.""" 10 | 11 | def __init__(self, response): 12 | """Response init.""" 13 | self._response = response 14 | # parse response 15 | self._doc = safe_parse_string(response.content) 16 | self._body = self._doc.xpath( 17 | '/soapenv:Envelope/soapenv:Body', namespaces=NS_MAP, 18 | )[0] 19 | 20 | def __str__(self): 21 | """String representation of Response is the HTTP body content.""" 22 | return self._response.content.decode('utf-8') 23 | 24 | 25 | RinseResponse = collections.namedtuple('RinseResponse', ['response', 'doc']) 26 | -------------------------------------------------------------------------------- /rinse/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for rinse package.""" 2 | import doctest 3 | import rinse 4 | import rinse.client 5 | import rinse.message 6 | import rinse.response 7 | import rinse.util 8 | import rinse.wsa 9 | import rinse.wsdl 10 | import rinse.wsse 11 | import rinse.xsd 12 | 13 | 14 | def load_tests(loader, tests, ignore): 15 | """Load rinse.wsse test suite.""" 16 | tests.addTests([ 17 | doctest.DocTestSuite(rinse), 18 | doctest.DocTestSuite(rinse.client), 19 | doctest.DocTestSuite(rinse.message), 20 | doctest.DocTestSuite(rinse.response), 21 | doctest.DocTestSuite(rinse.util), 22 | doctest.DocTestSuite(rinse.wsa), 23 | doctest.DocTestSuite(rinse.wsdl), 24 | doctest.DocTestSuite(rinse.wsse), 25 | doctest.DocTestSuite(rinse.xsd), 26 | ]) 27 | return tests 28 | -------------------------------------------------------------------------------- /rinse/tests/test_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Unit tests for rinse.client module.""" 4 | 5 | import unittest 6 | 7 | from lxml import etree 8 | from mock import MagicMock, patch 9 | from rinse.client import SoapClient 10 | from rinse.message import SoapMessage 11 | 12 | from .utils import captured_stdout 13 | 14 | 15 | class TestSoapMessage(unittest.TestCase): 16 | def test_soap_action(self): 17 | """Test that SOAP-Action HTTP header is set correctly.""" 18 | msg = SoapMessage(etree.Element('test')) 19 | req = msg.request('http://example.com', 'testaction') 20 | self.assertEqual(req.headers['SOAPAction'], 'testaction') 21 | 22 | def test_no_soap_action(self): 23 | """Test that SOAP-Action HTTP header is absent when no action given. 24 | """ 25 | msg = SoapMessage(etree.Element('test')) 26 | req = msg.request('http://example.com') 27 | self.assertTrue('SOAPAction' not in req.headers) 28 | 29 | def test_soap_action_is_none(self): 30 | """Test that SOAP-Action HTTP header is absent when no action is None. 31 | """ 32 | msg = SoapMessage(etree.Element('test')) 33 | req = msg.request('http://example.com', None) 34 | self.assertTrue('SOAPAction' not in req.headers) 35 | 36 | 37 | class TestRinseClient(unittest.TestCase): 38 | def test_soap_action(self): 39 | """Test that SOAP action is passed on to SoapMessage.request().""" 40 | msg = SoapMessage(etree.Element('test')) 41 | msg.request = MagicMock() 42 | with patch('requests.Session'): 43 | client = SoapClient('http://example.com') 44 | client(msg, 'testaction', build_response=lambda r: r) 45 | msg.request.assert_called_once_with('http://example.com', 46 | 'testaction') 47 | 48 | def test_soap_action_debug(self): 49 | msg = SoapMessage(etree.Element('test')) 50 | client = SoapClient('http://example.com', debug=True) 51 | client._session = MagicMock() 52 | with captured_stdout() as stdout: 53 | client(msg, 'testaction', build_response=lambda r: r) 54 | self.assertEqual( 55 | stdout.getvalue(), 56 | 'POST http://example.com\n' 57 | 'Content-Length: 164\n' 58 | 'Content-Type: text/xml;charset=UTF-8\n' 59 | 'SOAPAction: testaction\n' 60 | '\n' 61 | '\n' 62 | ' \n' 63 | ' \n' 64 | ' \n' 65 | ' \n' 66 | '\n' 67 | '\n' 68 | ) 69 | 70 | def test_no_soap_action(self): 71 | """Test that empty SOAP action is passed to SoapMessage.request() 72 | when no action given.""" 73 | msg = SoapMessage(etree.Element('test')) 74 | msg.request = MagicMock() 75 | with patch('requests.Session'): 76 | client = SoapClient('http://example.com') 77 | client(msg, build_response=lambda r: r) 78 | msg.request.assert_called_once_with('http://example.com', '') 79 | 80 | def test_timeout(self): 81 | msg = SoapMessage(etree.Element('test')) 82 | msg.request = MagicMock() 83 | client = SoapClient('http://example.com', timeout=1) 84 | self.assertEqual(client.timeout, 1) 85 | 86 | with patch('requests.Session'): 87 | client(msg, 'testaction', build_response=lambda r: r) 88 | self.assertEqual(client._session.send.call_args[1]['timeout'], 1) 89 | 90 | with patch('requests.Session'): 91 | client(msg, 'testaction', build_response=lambda r: r, timeout=2) 92 | self.assertEqual(client._session.send.call_args[1]['timeout'], 2) 93 | 94 | with patch('requests.Session'): 95 | client(msg, 'testaction', build_response=lambda r: r, timeout=None) 96 | self.assertEqual(client._session.send.call_args[1]['timeout'], None) 97 | 98 | 99 | if __name__ == '__main__': 100 | unittest.main() 101 | -------------------------------------------------------------------------------- /rinse/tests/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import contextmanager 3 | 4 | import six 5 | 6 | 7 | @contextmanager 8 | def captured_output(stream_name): 9 | """ 10 | Return a context manager used by captured_stdout (and optionally others) 11 | that temporarily replaces the sys stream *stream_name* with a StringIO. 12 | 13 | Note: This function and the following ``captured_std*`` are copied 14 | from Django's ``test.utils`` module. 15 | """ 16 | orig_stdout = getattr(sys, stream_name) 17 | setattr(sys, stream_name, six.StringIO()) 18 | try: 19 | yield getattr(sys, stream_name) 20 | finally: 21 | setattr(sys, stream_name, orig_stdout) 22 | 23 | 24 | def captured_stdout(): 25 | """ 26 | Capture the output of sys.stdout: 27 | 28 | with captured_stdout() as stdout: 29 | print("hello") 30 | self.assertEqual(stdout.getvalue(), "hello\n") 31 | """ 32 | return captured_output("stdout") 33 | -------------------------------------------------------------------------------- /rinse/util.py: -------------------------------------------------------------------------------- 1 | """rinse SOAP client utility functions.""" 2 | from __future__ import print_function 3 | import collections 4 | import os.path 5 | import pprint 6 | import textwrap 7 | 8 | import defusedxml.lxml 9 | import lxml.builder 10 | from lxml import etree 11 | 12 | RINSE_DIR = os.path.dirname(__file__) 13 | ENVELOPE_XSD = 'soap-1.1_envelope.xsd' 14 | 15 | NS_SOAPENV = 'http://schemas.xmlsoap.org/soap/envelope/' 16 | 17 | NS_MAP = { 18 | 'soapenv': NS_SOAPENV, 19 | } 20 | 21 | 22 | def element_as_tree(element): 23 | """Convert an element from within an ElementTree to its own tree.""" 24 | # XXX: this is a crude hack, but it works - got any better ideas? 25 | return safe_parse_string( 26 | etree.tostring( 27 | etree.ElementTree(element), 28 | ), 29 | ) 30 | 31 | 32 | def safe_parse_string(raw_xml, **kwargs): 33 | """Safely parse raw XML content into an element tree.""" 34 | return defusedxml.lxml.fromstring(raw_xml, **kwargs) 35 | 36 | 37 | def safe_parse_path(xml_path, **kwargs): 38 | """Safely parse XML content from path into an element tree.""" 39 | return defusedxml.lxml.parse(xml_path, **kwargs) 40 | 41 | 42 | def safe_parse_url(xml_url, **kwargs): 43 | """Safely parse XML content from path into an element tree.""" 44 | return defusedxml.lxml.parse(xml_url, **kwargs) 45 | 46 | 47 | class ElementMaker(lxml.builder.ElementMaker): 48 | 49 | """Wrapper around lxml ElementMaker that casts ints as strings.""" 50 | 51 | def __getattr__(self, name): 52 | """Return a lambda that parses int args as strings.""" 53 | _cls = super(ElementMaker, self).__getattr__(name) 54 | 55 | def __cls_wraper(*args, **kwargs): 56 | """Wrapper around Element class.""" 57 | return _cls( 58 | *[ 59 | str(arg) if isinstance(arg, int) else arg 60 | for arg 61 | in args 62 | ], 63 | **kwargs 64 | ) 65 | return __cls_wraper 66 | 67 | 68 | RinseResponse = collections.namedtuple('RinseResponse', ['response', 'doc']) 69 | 70 | 71 | class SchemaCache(collections.defaultdict): 72 | 73 | """Cache of lxml.etree.XMLSchema instances, keyed by XSD basename.""" 74 | 75 | def get(self, xsd, xpath=None, namespaces=None): 76 | """Generate XMLSchema instances as specified.""" 77 | if xsd.startswith('/'): 78 | pass # absolute path 79 | elif ':' in xsd: 80 | pass # URL - defused should help protect us. 81 | else: 82 | # assume XSD is in res/ subdir of rinse project. 83 | xsd = os.path.join(RINSE_DIR, 'res', xsd) 84 | doc = safe_parse_path(xsd) 85 | if xpath: 86 | doc = doc.xpath(xpath, namespaces=namespaces)[0] 87 | self[xsd] = schema = etree.XMLSchema(doc) 88 | return schema 89 | 90 | def __missing__(self, xsd): 91 | """Generate XMLSchema instances on demand.""" 92 | return self.get(xsd) 93 | 94 | 95 | SCHEMA = SchemaCache() 96 | 97 | 98 | class ElementMakerCache(collections.defaultdict): 99 | 100 | """Cache of ElementMaker instances for the given nsmap.""" 101 | 102 | def __init__(self, nsmap): 103 | """Keep reference to the nsmap we're given.""" 104 | super(ElementMakerCache, self).__init__() 105 | self.nsmap = { 106 | name: url 107 | for name, url 108 | in list(NS_MAP.items()) + list(nsmap.items()) 109 | } 110 | 111 | def __missing__(self, ns_prefix): 112 | """Generate ElementMaker instances as required.""" 113 | return ElementMaker( 114 | namespace=self.nsmap[ns_prefix], 115 | nsmap=self.nsmap, 116 | ) 117 | 118 | 119 | PRETTY_PARSER = etree.XMLParser( 120 | remove_blank_text=True, 121 | ) 122 | PRETTY_TEXT_WRAPPER = textwrap.TextWrapper( 123 | width=78, 124 | initial_indent='', 125 | subsequent_indent=' ', 126 | replace_whitespace=False, 127 | drop_whitespace=False, 128 | break_long_words=False, 129 | break_on_hyphens=False, 130 | ) 131 | 132 | 133 | def printxml(doc): 134 | """Pretty print an lxml document tree. 135 | 136 | The XML printed may not be exactly equivalent to the doc provided, as blank 137 | text within elements will be stripped to allow etree.tostring() to work with 138 | the 'pretty_print' option set. 139 | """ 140 | pretty_tree = safe_parse_string( 141 | etree.tostring(doc), parser=PRETTY_PARSER, 142 | ) 143 | pretty_xml = etree.tostring( 144 | pretty_tree, pretty_print=True, encoding='unicode', 145 | ).replace('\t', ' ').rstrip('\n') 146 | 147 | for line in pretty_xml.split('\n'): 148 | line = PRETTY_TEXT_WRAPPER.fill(line.rstrip('\n')).rstrip('\n') 149 | for subline in line.split('\n'): 150 | if not subline.strip(): 151 | continue 152 | print(subline) 153 | 154 | 155 | def recursive_dict(element): 156 | """Map an XML tree into a dict of dicts.""" 157 | if isinstance(element, (tuple, list)): 158 | return tuple( 159 | recursive_dict(child) 160 | for child 161 | in element 162 | ) 163 | return ( 164 | '{}{}'.format( 165 | element.tag, 166 | pprint.pformat(element.attrib, compact=True, width=10000), 167 | ), 168 | dict( 169 | map(recursive_dict, element) 170 | ) or element.text 171 | ) 172 | 173 | 174 | class cached_property(object): 175 | def __init__(self, func): 176 | self.func = func 177 | self.__doc__ = getattr(func, '__doc__') 178 | 179 | def __get__(self, instance, owner): 180 | if instance is None: 181 | return self 182 | result = instance.__dict__[self.func.__name__] = self.func(instance) 183 | return result 184 | -------------------------------------------------------------------------------- /rinse/wsa.py: -------------------------------------------------------------------------------- 1 | """WSA (Addressing) support for rinse SOAP client.""" 2 | 3 | NS_WSA = 'http://www.w3.org/2005/08/addressing' 4 | URI_ANONYMOUS = \ 5 | 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous' 6 | URI_UNSPECIFIED = \ 7 | 'http://schemas.xmlsoap.org/ws/2004/08/addressing/id/unspecified' 8 | 9 | 10 | def append_wsa_headers( 11 | msg, to, action, 12 | message_id=None, 13 | relates_to=None, 14 | relationship_type=None, 15 | reply_to=None, 16 | from_endpoint=None, 17 | fault_to=None, 18 | ): 19 | """Add WSA (addressing) headers. 20 | 21 | >>> from rinse.wsa import append_wsa_headers 22 | >>> import lxml.usedoctest 23 | >>> from rinse.message import SoapMessage 24 | >>> from rinse.util import printxml 25 | >>> msg = SoapMessage() 26 | >>> f123 = msg.elementmaker( 27 | ... 'f123', 28 | ... 'http://www.fabrikam123.example/svc53', 29 | ... ) 30 | >>> msg.body = f123.Delete(f123.maxCount('42')) 31 | >>> append_wsa_headers( 32 | ... msg, 33 | ... 'mailto:joe@fabrikam123.example', 34 | ... 'http://fabrikam123.example/mail/Delete', 35 | ... message_id='uuid:aaaabbbb-cccc-dddd-eeee-ffffffffffff', 36 | ... reply_to='http://business456.example/client1', 37 | ... ) 38 | >>> print(msg['SOAPAction']) 39 | "http://fabrikam123.example/mail/Delete" 40 | >>> printxml(msg.etree()) 41 | 44 | 45 | uuid:aaaabbbb-cccc-dddd-eeee-ffffffffffff 46 | mailto:joe@fabrikam123.example 47 | http://fabrikam123.example/mail/Delete 48 | 49 | http://business456.example/client1 50 | 51 | 52 | 53 | 54 | 42 55 | 56 | 57 | 58 | """ 59 | # perform simple validation on WSA headers 60 | if reply_to and not message_id: 61 | raise ValueError( 62 | 'wsa:ReplyTo set so wsa:MessageID MUST be present.', 63 | ) 64 | if fault_to and not message_id: 65 | raise ValueError( 66 | 'wsa:FaultTo set so wsa:MessageID MUST be present.', 67 | ) 68 | if relationship_type and not relates_to: 69 | raise ValueError( 70 | '/wsa:RelatesTo/@RelationshipType set ' 71 | 'so wsa:RelatesTo MUST be present.', 72 | ) 73 | 74 | # add 'SOAPAction' HTTP request header 75 | msg['SOAPAction'] = '"{}"'.format(action) 76 | 77 | # add WSA elements to SOAP headers 78 | relation_attrs = {} 79 | if relationship_type: 80 | relation_attrs['RelationshipType'] = relationship_type 81 | wsa = msg.elementmaker('wsa', NS_WSA) 82 | msg.headers.extend( 83 | header 84 | for header 85 | in [ 86 | wsa.MessageID(message_id) if message_id else None, 87 | wsa.RelatesTo(relates_to, **relation_attrs) if relates_to else None, 88 | wsa.To(to), 89 | wsa.Action(action), 90 | wsa.From(from_endpoint) if from_endpoint else None, 91 | wsa.ReplyTo(wsa.Address(reply_to)) if reply_to else None, 92 | wsa.FaultTo(wsa.Address(fault_to)) if fault_to else None, 93 | ] 94 | if header is not None 95 | ) 96 | -------------------------------------------------------------------------------- /rinse/wsdl.py: -------------------------------------------------------------------------------- 1 | """Rinse SOAP library: module providing WSDL functions.""" 2 | from lxml import etree 3 | from rinse.util import ( 4 | safe_parse_path, safe_parse_url, element_as_tree, cached_property, 5 | ) 6 | from rinse.xsd import XSDValidator, NS_XSD 7 | 8 | NS_WSDL = 'http://schemas.xmlsoap.org/wsdl/' 9 | NS_MAP = { 10 | 'wsdl': NS_WSDL, 11 | 'xsd': NS_XSD, 12 | } 13 | 14 | 15 | class WSDL(object): 16 | 17 | """WSDL object.""" 18 | 19 | _schema = None 20 | _xsd_validator = None 21 | 22 | @classmethod 23 | def from_file(cls, wsdl_path): 24 | """Make a WSDL instance from a file path.""" 25 | return cls(safe_parse_path(wsdl_path)) 26 | 27 | @classmethod 28 | def from_url(cls, wsdl_path): 29 | """Make a WSDL instance from a URL.""" 30 | return cls(safe_parse_url(wsdl_path)) 31 | 32 | def __init__(self, wsdl_root): 33 | """WSDL init.""" 34 | self.root = wsdl_root 35 | 36 | @cached_property 37 | def schema(self): 38 | """Return schema element (used for XSD validation).""" 39 | schema_el = self.root.xpath( 40 | '/wsdl:definitions/wsdl:types/xsd:schema', namespaces=NS_MAP, 41 | )[0] 42 | return element_as_tree(schema_el) 43 | 44 | @cached_property 45 | def xsd_validator(self): 46 | """Extract XML Schema Definition (XSD) element tree.""" 47 | return XSDValidator(self.schema) 48 | 49 | def is_valid(self, soapmsg): 50 | """Return True if SOAP message body validates against WSDL schema.""" 51 | return self.xsd_validator.is_valid(soapmsg.body) 52 | 53 | def validate(self, soapmsg): 54 | """Raise exception if SOAP message body is invalid.""" 55 | return self.xsd_validator.validate(soapmsg.body) 56 | -------------------------------------------------------------------------------- /rinse/wsse.py: -------------------------------------------------------------------------------- 1 | """SOAP client.""" 2 | 3 | NS_WSSE = 'http://docs.oasis-open.org/wss/2004/01/' \ 4 | 'oasis-200401-wss-wssecurity-secext-1.0.xsd' 5 | 6 | 7 | def append_wsse_headers(msg, username, password): 8 | """Add WSSE (security) headers. 9 | 10 | >>> from rinse.wsse import append_wsse_headers 11 | >>> import lxml.usedoctest 12 | >>> from rinse.message import SoapMessage 13 | >>> from rinse.util import printxml 14 | >>> msg = SoapMessage() 15 | >>> f123 = msg.elementmaker( 16 | ... 'f123', 17 | ... 'http://www.fabrikam123.example/svc53', 18 | ... ) 19 | >>> msg.body = f123.Delete(f123.maxCount('42')) 20 | >>> append_wsse_headers(msg, 'alice', '$uper-5ecret') 21 | >>> printxml(msg.etree()) 22 | 25 | 26 | 27 | 28 | alice 29 | $uper-5ecret 30 | 31 | 32 | 33 | 34 | 35 | 42 36 | 37 | 38 | 39 | """ 40 | # add WSSE headers 41 | wsse = msg.elementmaker('wsse', NS_WSSE) 42 | msg.headers.append( 43 | wsse.Security( 44 | wsse.UsernameToken( 45 | wsse.Username(username), 46 | wsse.Password(password), 47 | ), 48 | ), 49 | ) 50 | -------------------------------------------------------------------------------- /rinse/xsd.py: -------------------------------------------------------------------------------- 1 | """Rinse SOAP library: XML Schema Definition (XSD) functions.""" 2 | from lxml import etree 3 | from rinse.util import element_as_tree 4 | 5 | NS_XSD = 'http://www.w3.org/2001/XMLSchema' 6 | NS = { 7 | 'xsd': NS_XSD, 8 | } 9 | 10 | 11 | class XSDValidator(object): 12 | 13 | """XSD Schema Validation.""" 14 | 15 | _parser = None 16 | 17 | def __init__(self, schema_root): 18 | """XSDValidator init.""" 19 | self.root = element_as_tree(schema_root) 20 | self.schema = etree.XMLSchema(self.root) 21 | 22 | def validate(self, doc): 23 | """Validate doc against schema, raises exception if doc is invalid.""" 24 | self.schema.assertValid(doc) 25 | 26 | def is_valid(self, doc): 27 | """Validate doc against schema, return True if doc is valid.""" 28 | return self.schema.validate(doc) 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | CLASSIFIERS = [ 5 | # Beta status until 1.0 is released 6 | "Development Status :: 4 - Beta", 7 | 8 | # Who and what the project is for 9 | "Intended Audience :: Developers", 10 | "Topic :: Communications", 11 | "Topic :: Internet", 12 | "Topic :: Internet :: WWW/HTTP", 13 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 14 | "Topic :: Software Development :: Libraries", 15 | "Topic :: Text Processing :: Markup :: XML", 16 | 17 | # License classifiers 18 | "License :: OSI Approved :: MIT License", 19 | "License :: DFSG approved", 20 | "License :: OSI Approved", 21 | 22 | # Generally, we support the following. 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 2", 25 | "Programming Language :: Python :: 3", 26 | "Framework :: Django", 27 | 28 | # Specifically, we support the following releases. 29 | "Programming Language :: Python :: 2.7", 30 | "Programming Language :: Python :: 3.3", 31 | "Programming Language :: Python :: 3.4", 32 | "Framework :: Django :: 1.7", 33 | "Framework :: Django :: 1.8", 34 | ] 35 | 36 | setup( 37 | name='rinse', 38 | version='0.5.0', 39 | description='SOAP client built with lxml and requests.', 40 | long_description=open('README.rst').read(), 41 | author='Tyson Clugg', 42 | author_email='tyson@clugg.net', 43 | url='https://rinse.readthedocs.org/en/latest/', 44 | license='MIT', 45 | packages=find_packages(), 46 | include_package_data=True, 47 | zip_safe=False, 48 | test_suite='rinse.tests', 49 | install_requires=[ 50 | 'defusedxml', 51 | 'lxml', 52 | 'requests', 53 | ], 54 | tests_require=['mock', 'six'], 55 | classifiers=CLASSIFIERS, 56 | ) 57 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | mock 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | # require tox>=2.1.1 or refuse to run the tests. 8 | minversion=2.1.1 9 | 10 | # return success even if some of the specified environments are missing 11 | skip_missing_interpreters=True 12 | 13 | # "envlist" is a comma separated list of environments, each environment name 14 | # contains factors separated by hyphens. For example, "py27-unittest" has 2 15 | # factors: "py27" and "unittest". Other settings such as "setenv" accept the 16 | # factor names as a prefixes (eg: "unittest: ...") so that prefixed settings 17 | # only apply if the environment being run contains that factor. 18 | 19 | envlist = 20 | py27-test, 21 | py35-test, 22 | py36-test, 23 | py37-test, 24 | py38-test, 25 | 26 | [testenv] 27 | recreate=True 28 | usedevelop=False 29 | passenv= 30 | BUILD_NUMBER 31 | BUILD_URL 32 | XDG_CACHE_HOME 33 | 34 | # continue running commands even if previous commands have failed 35 | ignore_errors = True 36 | 37 | commands = 38 | coverage run --source={toxinidir}/rinse {toxinidir}/setup.py test 39 | coverage report 40 | 41 | deps = 42 | -r{toxinidir}/test-requirements.txt 43 | coverage 44 | --------------------------------------------------------------------------------