Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: Ib3976cdfab839242694ce603451ef38407650721
This commit is contained in:
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,29 +0,0 @@
|
|||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.egg-info
|
|
||||||
*.swp
|
|
||||||
*.orig
|
|
||||||
~*
|
|
||||||
*~
|
|
||||||
.coverage*
|
|
||||||
coverage*.xml
|
|
||||||
.noseids
|
|
||||||
.tox
|
|
||||||
.test_sphinxext
|
|
||||||
nosetests*.xml
|
|
||||||
|
|
||||||
build
|
|
||||||
dist
|
|
||||||
|
|
||||||
doc/_build
|
|
||||||
d2to1-*.egg
|
|
||||||
|
|
||||||
WSME.egg-info/
|
|
||||||
|
|
||||||
# Files created by pbr
|
|
||||||
AUTHORS
|
|
||||||
ChangeLog
|
|
||||||
pbr*.egg
|
|
||||||
|
|
||||||
# Cross-test logs
|
|
||||||
cross-test-*.log
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[gerrit]
|
|
||||||
host=review.openstack.org
|
|
||||||
port=29418
|
|
||||||
project=openstack/wsme.git
|
|
||||||
14
.hgtags
14
.hgtags
@@ -1,14 +0,0 @@
|
|||||||
2bd203a084dcc785257b35e7231b2021722f60de 0.1.0a1
|
|
||||||
0eae00db9384d52cc4a82c09ab207d631ecb82e4 0.1.0a2
|
|
||||||
86466da44f44a97b379c7b8e94c371526be0eb9f 0.1.0a3
|
|
||||||
b38c56a2b9130d8fade7be22c8ac66a45fa77a6e 0.1.0a4
|
|
||||||
b0019e486c807bafe412ebaa6eb9bd9ab656c81c 0.1.0
|
|
||||||
c17de432c1857cfa059816d0db332bcdabea0c82 0.1.1
|
|
||||||
cfb5efc624f55710c987c7795501f3dd44a01078 0.2.0
|
|
||||||
ebe2c6f228ad4a365fbda9418f3e113d542390f0 0.3b1
|
|
||||||
d5eab01bf49192df2e0f24c78ca4936073e45b19 0.3b2
|
|
||||||
603c8586b076f5cf9b70b6cd82578dba7226e0c7 0.3
|
|
||||||
5ad01afed8779bb5a384802a2ec7d6ed0186c7d5 0.4b1
|
|
||||||
f06e004ca8e4013bf94df0cdade23b01742b0ec0 0.4
|
|
||||||
359199eb4e0999b5920eadfa40038013cd360df6 0.5b1
|
|
||||||
d3e5eee0b150048762169ff20ee25b43aa0369fa 0.5b2
|
|
||||||
19
LICENSE
19
LICENSE
@@ -1,19 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
14
README
Normal file
14
README
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
This project is no longer maintained.
|
||||||
|
|
||||||
|
The contents of this repository are still available in the Git
|
||||||
|
source code management system. To see the contents of this
|
||||||
|
repository before it reached its end of life, please check out the
|
||||||
|
previous commit with "git checkout HEAD^1".
|
||||||
|
|
||||||
|
For ongoing work on maintaining OpenStack packages in the Debian
|
||||||
|
distribution, please see the Debian OpenStack packaging team at
|
||||||
|
https://wiki.debian.org/OpenStack/.
|
||||||
|
|
||||||
|
For any further questions, please email
|
||||||
|
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||||
|
Freenode.
|
||||||
112
README.rst
112
README.rst
@@ -1,112 +0,0 @@
|
|||||||
Web Services Made Easy
|
|
||||||
======================
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
------------
|
|
||||||
|
|
||||||
Web Services Made Easy (WSME) simplifies the writing of REST web services
|
|
||||||
by providing simple yet powerful typing, removing the need to directly
|
|
||||||
manipulate the request and the response objects.
|
|
||||||
|
|
||||||
WSME can work standalone or on top of your favorite Python web
|
|
||||||
(micro)framework, so you can use both your preferred way of routing your REST
|
|
||||||
requests and most of the features of WSME that rely on the typing system like:
|
|
||||||
|
|
||||||
- Alternate protocols, including those supporting batch-calls
|
|
||||||
- Easy documentation through a Sphinx_ extension
|
|
||||||
|
|
||||||
WSME is originally a rewrite of TGWebServices
|
|
||||||
with a focus on extensibility, framework-independance and better type handling.
|
|
||||||
|
|
||||||
How Easy ?
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
Here is a standalone wsgi example::
|
|
||||||
|
|
||||||
from wsme import WSRoot, expose
|
|
||||||
|
|
||||||
class MyService(WSRoot):
|
|
||||||
@expose(unicode, unicode) # First parameter is the return type,
|
|
||||||
# then the function argument types
|
|
||||||
def hello(self, who=u'World'):
|
|
||||||
return u"Hello {0} !".format(who)
|
|
||||||
|
|
||||||
ws = MyService(protocols=['restjson', 'restxml', 'soap'])
|
|
||||||
application = ws.wsgiapp()
|
|
||||||
|
|
||||||
With this published at the ``/ws`` path of your application, you can access
|
|
||||||
your hello function in various protocols:
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - URL
|
|
||||||
- Returns
|
|
||||||
|
|
||||||
* - ``http://<server>/ws/hello.json?who=you``
|
|
||||||
- ``"Hello you !"``
|
|
||||||
|
|
||||||
* - ``http://<server>/ws/hello.xml``
|
|
||||||
- ``<result>Hello World !</result>``
|
|
||||||
|
|
||||||
* - ``http://<server>/ws/api.wsdl``
|
|
||||||
- A WSDL description for any SOAP client.
|
|
||||||
|
|
||||||
|
|
||||||
Main features
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
- Very simple API.
|
|
||||||
- Supports user-defined simple and complex types.
|
|
||||||
- Multi-protocol : REST+Json, REST+XML, SOAP, ExtDirect and more to come.
|
|
||||||
- Extensible : easy to add more protocols or more base types.
|
|
||||||
- Framework independence : adapters are provided to easily integrate
|
|
||||||
your API in any web framework, for example a wsgi container,
|
|
||||||
Pecan_, TurboGears_, Flask_, cornice_...
|
|
||||||
- Very few runtime dependencies: webob, simplegeneric. Optionnaly lxml and
|
|
||||||
simplejson if you need better performances.
|
|
||||||
- Integration in `Sphinx`_ for making clean documentation with
|
|
||||||
``wsmeext.sphinxext``.
|
|
||||||
|
|
||||||
.. _Pecan: http://pecanpy.org/
|
|
||||||
.. _TurboGears: http://www.turbogears.org/
|
|
||||||
.. _Flask: http://flask.pocoo.org/
|
|
||||||
.. _cornice: http://pypi.python.org/pypi/cornice
|
|
||||||
|
|
||||||
Install
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install WSME
|
|
||||||
|
|
||||||
or, if you do not have pip on your system or virtualenv
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
easy_install WSME
|
|
||||||
|
|
||||||
Changes
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
- Read the `Changelog`_
|
|
||||||
|
|
||||||
Getting Help
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
- Read the `WSME Documentation`_.
|
|
||||||
- Questions about WSME should go to the `python-wsme mailinglist`_.
|
|
||||||
|
|
||||||
Contribute
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
* Documentation: http://packages.python.org/WSME/
|
|
||||||
* Source: http://git.openstack.org/cgit/openstack/wsme
|
|
||||||
* Bugs: https://bugs.launchpad.net/wsme/+bugs
|
|
||||||
* Code review: https://review.openstack.org/#/q/project:openstack/wsme,n,z
|
|
||||||
|
|
||||||
.. _Changelog: http://packages.python.org/WSME/changes.html
|
|
||||||
.. _python-wsme mailinglist: http://groups.google.com/group/python-wsme
|
|
||||||
.. _WSME Documentation: http://packages.python.org/WSME/
|
|
||||||
.. _WSME issue tracker: https://bugs.launchpad.net/wsme/+bugs
|
|
||||||
.. _Sphinx: http://sphinx.pocoo.org/
|
|
||||||
134
doc/Makefile
134
doc/Makefile
@@ -1,134 +0,0 @@
|
|||||||
# 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) .
|
|
||||||
|
|
||||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "Please use \`make <target>' where <target> 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 " 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 " text to make text files"
|
|
||||||
@echo " man to make manual pages"
|
|
||||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
|
||||||
@echo " linkcheck to check all external links for integrity"
|
|
||||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
-rm -rf $(BUILDDIR)/*
|
|
||||||
|
|
||||||
html:
|
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
|
||||||
|
|
||||||
ziphtml: html
|
|
||||||
rm -f $(BUILDDIR)/wsme-documentation.zip
|
|
||||||
cd $(BUILDDIR)/html && zip -r ../wsme-documentation.zip .
|
|
||||||
|
|
||||||
dirhtml:
|
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
|
||||||
|
|
||||||
singlehtml:
|
|
||||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
|
||||||
|
|
||||||
pickle:
|
|
||||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the pickle files."
|
|
||||||
|
|
||||||
json:
|
|
||||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the JSON files."
|
|
||||||
|
|
||||||
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."
|
|
||||||
|
|
||||||
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/WebServicesMadeEasy.qhcp"
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WebServicesMadeEasy.qhc"
|
|
||||||
|
|
||||||
devhelp:
|
|
||||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished."
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/WebServicesMadeEasy"
|
|
||||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WebServicesMadeEasy"
|
|
||||||
@echo "# devhelp"
|
|
||||||
|
|
||||||
epub:
|
|
||||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
|
||||||
|
|
||||||
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)."
|
|
||||||
|
|
||||||
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."
|
|
||||||
|
|
||||||
text:
|
|
||||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
|
||||||
|
|
||||||
man:
|
|
||||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
|
||||||
|
|
||||||
changes:
|
|
||||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
|
||||||
@echo
|
|
||||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
|
||||||
|
|
||||||
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."
|
|
||||||
|
|
||||||
doctest:
|
|
||||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
|
||||||
@echo "Testing of doctests in the sources finished, look at the " \
|
|
||||||
"results in $(BUILDDIR)/doctest/output.txt."
|
|
||||||
10
doc/_static/toggle.css
vendored
10
doc/_static/toggle.css
vendored
@@ -1,10 +0,0 @@
|
|||||||
dl.toggle dt {
|
|
||||||
background-color: #eeffcc;
|
|
||||||
border: 1px solid #ac9;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl.toggle dd {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
9
doc/_static/toggle.js
vendored
9
doc/_static/toggle.js
vendored
@@ -1,9 +0,0 @@
|
|||||||
/*global $,document*/
|
|
||||||
$(document).ready(function () {
|
|
||||||
"use strict";
|
|
||||||
$("dl.toggle > dt").click(
|
|
||||||
function (event) {
|
|
||||||
$(this).next().toggle(250);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
27
doc/_static/wsme.css
vendored
27
doc/_static/wsme.css
vendored
@@ -1,27 +0,0 @@
|
|||||||
@import "agogo.css";
|
|
||||||
|
|
||||||
table.docutils {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils th {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils thead tr {
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils td {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils tr.row-odd {
|
|
||||||
background: #EEEEEC;
|
|
||||||
}
|
|
||||||
|
|
||||||
45
doc/api.rst
45
doc/api.rst
@@ -1,45 +0,0 @@
|
|||||||
API
|
|
||||||
===
|
|
||||||
|
|
||||||
Public API
|
|
||||||
----------
|
|
||||||
|
|
||||||
:mod:`wsme` -- Essentials
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. module:: wsme
|
|
||||||
|
|
||||||
.. autoclass:: signature([return_type, [arg0_type, [arg1_type, ... ] ] ], body=None, status_code=None)
|
|
||||||
|
|
||||||
.. autoclass:: wsme.types.Base
|
|
||||||
.. autoclass:: wsattr
|
|
||||||
.. autoclass:: wsproperty
|
|
||||||
|
|
||||||
.. data:: Unset
|
|
||||||
|
|
||||||
Default value of the complex type attributes.
|
|
||||||
|
|
||||||
.. autoclass:: WSRoot
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Internals
|
|
||||||
---------
|
|
||||||
|
|
||||||
:mod:`wsme.types` -- Types
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. automodule:: wsme.types
|
|
||||||
:members: register_type
|
|
||||||
|
|
||||||
:mod:`wsme.api` -- API related api
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. automodule:: wsme.api
|
|
||||||
:members: FunctionArgument, FunctionDefinition
|
|
||||||
|
|
||||||
:mod:`wsme.rest.args` -- REST protocol argument handling
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. automodule:: wsme.rest.args
|
|
||||||
:members:
|
|
||||||
|
|
||||||
433
doc/changes.rst
433
doc/changes.rst
@@ -1,433 +0,0 @@
|
|||||||
Changes
|
|
||||||
=======
|
|
||||||
|
|
||||||
0.8.0 (2015-08-25)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Changes that may break your app:
|
|
||||||
|
|
||||||
* Returns 400 if unexpected attributes are added to complex types (#1277571).
|
|
||||||
|
|
||||||
Other changes:
|
|
||||||
|
|
||||||
* Returns 415 when Content-Type is invalid (#1419110)
|
|
||||||
* Returns 400 if a complex input type is not a json object (#1423634)
|
|
||||||
* Fix error reports with ArrayType and DictType invalid inputs (#1428185, #1428628)
|
|
||||||
* Update README
|
|
||||||
|
|
||||||
0.7.0 (2015-05-13)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Ensure UserType objects are converted to basetype
|
|
||||||
* Convert built-in types when passed as strings
|
|
||||||
* Multiple protocol accept or content-type matching
|
|
||||||
* Raise an InvalidInput if you get a ValueError from JSON data
|
|
||||||
* Remove unsupported python versions from setup.cfg
|
|
||||||
* Clean up setup.py and add requirements.txt
|
|
||||||
* Add full MIT license
|
|
||||||
* Fix i18n when formatting exception
|
|
||||||
* Cleanup up logging
|
|
||||||
* Make it possible to use the Response to return a non-default return type
|
|
||||||
* several fixes for SOAP protocol
|
|
||||||
|
|
||||||
0.6.4 (2014-11-20)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Include tests in the source distribution
|
|
||||||
|
|
||||||
0.6.3 (2014-11-19)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Disable universal wheels
|
|
||||||
|
|
||||||
0.6.2 (2014-11-18)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Flask adapter complex types now supports flask.ext.restful
|
|
||||||
* Allow disabling complex types auto-register
|
|
||||||
* Documentation edits
|
|
||||||
* Various documentation build fixes
|
|
||||||
* Fix passing Dict and Array based UserType as params
|
|
||||||
|
|
||||||
0.6.1 (2014-05-02)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fix error: variable 'kw' referenced before assignment
|
|
||||||
* Fix default handling for zero values
|
|
||||||
* Fixing spelling mistakes
|
|
||||||
* A proper check of UuidType
|
|
||||||
* pecan: cleanup, use global vars and staticmethod
|
|
||||||
* args_from_args() to work with an instance of UserType
|
|
||||||
|
|
||||||
0.6 (2014-02-06)
|
|
||||||
----------------
|
|
||||||
|
|
||||||
* Add 'readonly' parameter to wsattr
|
|
||||||
* Fix typos in documents and comments
|
|
||||||
* Support dynamic types
|
|
||||||
* Support building wheels (PEP-427)
|
|
||||||
* Fix a typo in the types documentation
|
|
||||||
* Add IntegerType and some classes for validation
|
|
||||||
* Use assertRaises() for negative tests
|
|
||||||
* Remove the duplicated error message from Enum
|
|
||||||
* Drop description from 403 flask test case
|
|
||||||
* Fix SyntaxWarning under Python 3
|
|
||||||
|
|
||||||
0.5b6 (2013-10-16)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Add improved support for HTTP response codes in cornice apps.
|
|
||||||
|
|
||||||
* Handle mandatory attributes
|
|
||||||
|
|
||||||
* Fix error code returned when None is used in an Enum
|
|
||||||
|
|
||||||
* Handle list and dict for body type in REST protocol
|
|
||||||
|
|
||||||
* Fix Sphinx for Python 3
|
|
||||||
|
|
||||||
* Add custom error code to ClientSideError
|
|
||||||
|
|
||||||
* Return a ClientSideError if unable to convert data
|
|
||||||
|
|
||||||
* Validate body when using Pecan
|
|
||||||
|
|
||||||
|
|
||||||
0.5b5 (2013-09-16)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
More packaging fixes.
|
|
||||||
|
|
||||||
0.5b4 (2013-09-11)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Fixes some release-related files for the stackforge release process.
|
|
||||||
No user-facing bug fixes or features over what 0.5b3 provides.
|
|
||||||
|
|
||||||
0.5b3 (2013-09-04)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
The project moved to stackforge. Mind the new URLs for the repository, bug
|
|
||||||
report etc (see the documentation).
|
|
||||||
|
|
||||||
* Allow non-default status code return with the pecan adapter
|
|
||||||
(Angus Salked).
|
|
||||||
|
|
||||||
* Fix returning objects with object attributes set to None on rest-json
|
|
||||||
& ExtDirect.
|
|
||||||
|
|
||||||
* Allow error details to be set on the Response object (experimental !).
|
|
||||||
|
|
||||||
* Fix: Content-Type header is not set anymore when the return type is None
|
|
||||||
on the pecan adapter.
|
|
||||||
|
|
||||||
* Support unicode message in ClientSideError (Mehdi Abaakouk).
|
|
||||||
|
|
||||||
* Use pbr instead of d2to1 (Julien Danjou).
|
|
||||||
|
|
||||||
* Python 3.3 support (Julien Danjou).
|
|
||||||
|
|
||||||
* Pecan adapter: returned status can now be set on exceptions (Vitaly
|
|
||||||
Kostenko).
|
|
||||||
|
|
||||||
* TG adapters: returned status can be set on exceptions (Ryan
|
|
||||||
Petrello).
|
|
||||||
|
|
||||||
* six >= 1.4.0 support (Julien Danjou).
|
|
||||||
|
|
||||||
* Require ordereddict from pypi for python < 2.6 (Ryan Petrello).
|
|
||||||
|
|
||||||
* Make the code PEP8 compliant (Ryan Petrello).
|
|
||||||
|
|
||||||
0.5b2 (2013-04-18)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Changed the way datas of complex types are stored. In previous versions, an
|
|
||||||
attribute was added to the type for each attribute, its name being the
|
|
||||||
attribute name prefixed with '_'.
|
|
||||||
|
|
||||||
Starting with this version, a single attribute _wsme_dataholder is added to
|
|
||||||
the instance.
|
|
||||||
|
|
||||||
The motivation behind this change is to avoid adding too many attributes to
|
|
||||||
the object.
|
|
||||||
|
|
||||||
* Add a special type 'HostRequest' that allow a function to ask for the host
|
|
||||||
framework request object in its arguments.
|
|
||||||
|
|
||||||
* Pecan adapter: Debug mode (which returns the exception tracebacks to the
|
|
||||||
client) can be enabled by the pecan application configuration.
|
|
||||||
|
|
||||||
* New adapter: wsmeext.flask, for the Flask_ framework.
|
|
||||||
|
|
||||||
.. _Flask: http://flask.pocoo.org/
|
|
||||||
|
|
||||||
* Fix: the cornice adapter was not usable.
|
|
||||||
|
|
||||||
* Fix: Submodules of wsmeext were missing in the packages.
|
|
||||||
|
|
||||||
* Fix: The demo app was still depending on the WSME-Soap package (which has
|
|
||||||
been merged into WSME in 0.5b1).
|
|
||||||
|
|
||||||
* Fix: A function with only on 'body' parameter would fail when being called.
|
|
||||||
|
|
||||||
* Fix: Missing arguments were poorly reported by the frameworks adapters.
|
|
||||||
|
|
||||||
0.5b1 (2013-01-30)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Introduce a new kind of adapters that rely on the framework routing.
|
|
||||||
Adapters are provided for Pecan, TurboGears and cornice.
|
|
||||||
|
|
||||||
* Reorganised the rest protocol implementation to ease the implementation of
|
|
||||||
adapters that rely only on the host framework routing system.
|
|
||||||
|
|
||||||
* The default rest ``@expose`` decorator does not wrap the decorated function
|
|
||||||
anymore. If needed to expose a same function several times, a parameter
|
|
||||||
``multiple_expose=True`` has been introduced.
|
|
||||||
|
|
||||||
* Remove the wsme.release module
|
|
||||||
|
|
||||||
* Fix == operator on ArrayType
|
|
||||||
|
|
||||||
* Adapted the wsme.sphinxext module to work with the function exposed by the
|
|
||||||
``wsme.pecan`` adapter.
|
|
||||||
|
|
||||||
* Allow promotion of ``int`` to ``float`` on float attributes (Doug Hellman)
|
|
||||||
|
|
||||||
* Add a ``samples_slot`` option to the ``.. autotype`` directive to
|
|
||||||
choose where the data samples whould be inserted (Doug Hellman).
|
|
||||||
|
|
||||||
* Add ``sample()`` to ArrayType and DictType (Doug Hellman).
|
|
||||||
|
|
||||||
* New syntax for object arrays as GET parameters, without brackets. Ex:
|
|
||||||
``?o.f1=a&o.f1=b&o.f2=c&o.f2=d`` is an array of two objects:
|
|
||||||
[{'f1': 'a', 'f2': 'c']}, {'f1': 'b', 'f2': 'd']}.
|
|
||||||
|
|
||||||
* @signature (and its @wsexpose frontends) has a new parameter:
|
|
||||||
``ignore_extra_args``.
|
|
||||||
|
|
||||||
* Fix boolean as input type support in the soap implementation (Craig
|
|
||||||
McDaniel).
|
|
||||||
|
|
||||||
* Fix empty/nil strings distinction in soap (Craig McDaniel).
|
|
||||||
|
|
||||||
* Improved unittests code coverage.
|
|
||||||
|
|
||||||
* Ported the soap implementation to python 3.
|
|
||||||
|
|
||||||
* Moved non-core features (adapters, sphinx extension) to the ``wsmeext`` module.
|
|
||||||
|
|
||||||
* Change the GET parameter name for passing the request body as a parameter
|
|
||||||
is now from 'body' to '__body__'
|
|
||||||
|
|
||||||
* The soap, extdirect and sqlalchemy packages have been merged into the main
|
|
||||||
package.
|
|
||||||
|
|
||||||
* Changed the documentation theme to "Cloud".
|
|
||||||
|
|
||||||
0.4 (2012-10-15)
|
|
||||||
----------------
|
|
||||||
|
|
||||||
* Automatically converts unicode strings to/from ascii bytes.
|
|
||||||
|
|
||||||
* Use d2to1 to simplify setup.py.
|
|
||||||
|
|
||||||
* Implements the SPORE specification.
|
|
||||||
|
|
||||||
* Fixed a few things in the documentation
|
|
||||||
|
|
||||||
0.4b1 (2012-09-14)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Now supports Python 3.2
|
|
||||||
|
|
||||||
* String types handling is clearer.
|
|
||||||
|
|
||||||
* New :class:`wsme.types.File` type.
|
|
||||||
|
|
||||||
* Supports cross-referenced types.
|
|
||||||
|
|
||||||
* Various bugfixes.
|
|
||||||
|
|
||||||
* Tests code coverage is now over 95%.
|
|
||||||
|
|
||||||
* RESTful protocol can now use the http method.
|
|
||||||
|
|
||||||
* UserTypes can now be given a name that will be used in the
|
|
||||||
documentation.
|
|
||||||
|
|
||||||
* Complex types can inherit :class:`wsme.types.Base`. They will
|
|
||||||
have a default constructor and be registered automatically.
|
|
||||||
|
|
||||||
* Removed the wsme.wsgi.adapt function if favor of
|
|
||||||
:meth:`wsme.WSRoot.wsgiapp`
|
|
||||||
|
|
||||||
Extensions
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
wsme-soap
|
|
||||||
* Function names now starts with a lowercase letter.
|
|
||||||
|
|
||||||
* Fixed issues with arrays (issue #3).
|
|
||||||
|
|
||||||
* Fixed empty array handling.
|
|
||||||
|
|
||||||
|
|
||||||
wsme-sqlalchemy
|
|
||||||
This new extension makes it easy to create webservices on top
|
|
||||||
of a SQLAlchemy set of mapped classes.
|
|
||||||
|
|
||||||
wsme-extdirect
|
|
||||||
* Implements server-side DataStore
|
|
||||||
(:class:`wsmeext.extdirect.datastore.DataStoreController`).
|
|
||||||
|
|
||||||
* Add Store and Model javascript definition auto-generation
|
|
||||||
|
|
||||||
* Add Store server-side based on SQLAlchemy mapped classes
|
|
||||||
(:class:`wsmeext.extdirect.sadatastore.SADataStoreController`).
|
|
||||||
|
|
||||||
0.3 (2012-04-20)
|
|
||||||
----------------
|
|
||||||
|
|
||||||
* Initial Sphinx integration.
|
|
||||||
|
|
||||||
0.3b2 (2012-03-29)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fixed issues with the TG1 adapter.
|
|
||||||
|
|
||||||
* Now handle dict and UserType types as GET/POST params.
|
|
||||||
|
|
||||||
* Better handling of application/x-www-form-urlencoded encoded POSTs
|
|
||||||
in rest protocols.
|
|
||||||
|
|
||||||
* :class:`wsattr` now takes a 'default' parameter that will be returned
|
|
||||||
instead of 'Unset' if no value has been set.
|
|
||||||
|
|
||||||
0.3b1 (2012-01-19)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Per-call database transaction handling.
|
|
||||||
|
|
||||||
* :class:`Unset` is now imported in the wsme module
|
|
||||||
|
|
||||||
* Attributes of complex types can now have a different name in
|
|
||||||
the public api and in the implementation.
|
|
||||||
|
|
||||||
* Complex arguments can now be sent as GET/POST params in the rest
|
|
||||||
protocols.
|
|
||||||
|
|
||||||
* The restjson protocol do not nest the results in an object anymore.
|
|
||||||
|
|
||||||
* Improved the documentation
|
|
||||||
|
|
||||||
* Fix array attributes validation.
|
|
||||||
|
|
||||||
* Fix date|time parsing errors.
|
|
||||||
|
|
||||||
* Fix Unset values validation.
|
|
||||||
|
|
||||||
* Fix registering of complex types inheriting form already
|
|
||||||
registered complex types.
|
|
||||||
|
|
||||||
* Fix user types, str and None values encoding/decoding.
|
|
||||||
|
|
||||||
0.2.0 (2011-10-29)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Added batch-calls abilities.
|
|
||||||
|
|
||||||
* Introduce a :class:`UnsetType` and a :data:`Unset` constant
|
|
||||||
so that non-mandatory attributes can remain unset (which is
|
|
||||||
different from null).
|
|
||||||
|
|
||||||
* Fix: If a complex type was only used as an input type, it was
|
|
||||||
not registered.
|
|
||||||
|
|
||||||
* Add support for user types.
|
|
||||||
|
|
||||||
* Add an Enum type (which is a user type).
|
|
||||||
|
|
||||||
* The 'binary' type is now a user type.
|
|
||||||
|
|
||||||
* Complex types:
|
|
||||||
|
|
||||||
- Fix inspection of complex types with inheritance.
|
|
||||||
|
|
||||||
- Fix inspection of self-referencing complex types.
|
|
||||||
|
|
||||||
- wsattr is now a python Descriptor, which makes it possible
|
|
||||||
to retrieve the attribute definition on a class while
|
|
||||||
manipulating values on the instance.
|
|
||||||
|
|
||||||
- Add strong type validation on assignment (made possible by
|
|
||||||
the use of Descriptors).
|
|
||||||
|
|
||||||
* ExtDirect:
|
|
||||||
|
|
||||||
- Implements batch calls
|
|
||||||
|
|
||||||
- Fix None values conversion
|
|
||||||
|
|
||||||
- Fix transaction result : 'action' and 'method' were missing.
|
|
||||||
|
|
||||||
0.1.1 (2011-10-20)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Changed the internal API by introducing a CallContext object.
|
|
||||||
It makes it easier to implement some protocols that have
|
|
||||||
a transaction or call id that has to be returned. It will also
|
|
||||||
make it possible to implement batch-calls in a later version.
|
|
||||||
|
|
||||||
* More test coverage.
|
|
||||||
|
|
||||||
* Fix a problem with array attribute types not being registered.
|
|
||||||
|
|
||||||
* Fix the mandatory / default detection on function arguments.
|
|
||||||
|
|
||||||
* Fix issues with the SOAP protocol implementation which should now
|
|
||||||
work properly with a suds client.
|
|
||||||
|
|
||||||
* Fix issues with the ExtDirect protocol implementation.
|
|
||||||
|
|
||||||
0.1.0 (2011-10-14)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Protocol insertion order now influence the protocol selection
|
|
||||||
|
|
||||||
* Move the soap protocol implementation in a separate lib,
|
|
||||||
WSME-Soap
|
|
||||||
|
|
||||||
* Introduce a new protocol ExtDirect in the WSME-ExtDirect lib.
|
|
||||||
|
|
||||||
0.1.0a4 (2011-10-12)
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
* Change the way framework adapters works. Now the adapter modules
|
|
||||||
have a simple adapt function that adapt a :class:`wsme.WSRoot`
|
|
||||||
instance. This way a same root can be integrated in several
|
|
||||||
framework.
|
|
||||||
|
|
||||||
* Protocol lookup now use entry points in the group ``[wsme.protocols]``.
|
|
||||||
|
|
||||||
0.1.0a3 (2011-10-11)
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
* Add specialised WSRoot classes for easy integration as a
|
|
||||||
WSGI Application (:class:`wsme.wsgi.WSRoot`) or a
|
|
||||||
TurboGears 1.x controller (:class:`wsme.tg1.WSRoot`).
|
|
||||||
|
|
||||||
* Improve the documentation.
|
|
||||||
|
|
||||||
* More unit tests and code-coverage.
|
|
||||||
|
|
||||||
0.1.0a2 (2011-10-07)
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
* Added support for arrays in all the protocols
|
|
||||||
|
|
||||||
0.1.0a1 (2011-10-04)
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Initial public release.
|
|
||||||
259
doc/conf.py
259
doc/conf.py
@@ -1,259 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Web Services Made Easy documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Sun Oct 2 20:27:45 2011.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
import sys, os
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
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.viewcode', 'wsmeext.sphinxext',
|
|
||||||
'sphinx.ext.intersphinx']
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
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'Web Services Made Easy'
|
|
||||||
copyright = u'2011, Christophe de Vienne'
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
import pkg_resources
|
|
||||||
wsmedist = pkg_resources.require('WSME')[0]
|
|
||||||
version = wsmedist.version
|
|
||||||
|
|
||||||
# The short X.Y version.
|
|
||||||
version = '.'.join(version.split('.')[:2])
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = version
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#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.
|
|
||||||
exclude_patterns = ['_build']
|
|
||||||
|
|
||||||
# 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 = []
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
#html_theme = 'agogo'
|
|
||||||
#html_theme_options = {
|
|
||||||
# "pagewidth": "60em",
|
|
||||||
# "documentwidth": "40em",
|
|
||||||
#}
|
|
||||||
|
|
||||||
#html_style = 'wsme.css'
|
|
||||||
|
|
||||||
import cloud_sptheme as csp
|
|
||||||
|
|
||||||
html_theme = 'cloud'
|
|
||||||
html_theme_path = [csp.get_theme_dir()]
|
|
||||||
|
|
||||||
html_theme_options = {
|
|
||||||
"roottarget": "index",
|
|
||||||
"googleanalytics_id": "UA-8510502-6"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
html_title = "WSME %s" % release
|
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
||||||
#html_short_title = "WSME"
|
|
||||||
|
|
||||||
# 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 (within the static path) to use as 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']
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
#html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# 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 <link> 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
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'WebServicesMadeEasydoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
|
||||||
|
|
||||||
# The paper size ('letter' or 'a4').
|
|
||||||
latex_paper_size = 'a4'
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#latex_font_size = '10pt'
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'WebServicesMadeEasy.tex', u'Web Services Made Easy Documentation',
|
|
||||||
u'Christophe de Vienne', '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
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
latex_preamble = '''
|
|
||||||
\usepackage[T2A]{fontenc}
|
|
||||||
\usepackage[utf8]{inputenc}
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#latex_appendices = []
|
|
||||||
|
|
||||||
# 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 = [
|
|
||||||
('index', 'webservicesmadeeasy', u'Web Services Made Easy Documentation',
|
|
||||||
[u'Christophe de Vienne'], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
autodoc_member_order = 'bysource'
|
|
||||||
|
|
||||||
wsme_protocols = [
|
|
||||||
'restjson', 'restxml', 'soap', 'extdirect'
|
|
||||||
]
|
|
||||||
|
|
||||||
intersphinx_mapping = {
|
|
||||||
'python': ('http://docs.python.org/', None),
|
|
||||||
'six': ('http://packages.python.org/six/', None),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
|
||||||
# confval directive taken from the sphinx doc
|
|
||||||
app.add_object_type('confval', 'confval',
|
|
||||||
objname='configuration value',
|
|
||||||
indextemplate='pair: %s; configuration value')
|
|
||||||
190
doc/document.rst
190
doc/document.rst
@@ -1,190 +0,0 @@
|
|||||||
Document your API
|
|
||||||
=================
|
|
||||||
|
|
||||||
Web services without a proper documentation are usually useless.
|
|
||||||
|
|
||||||
To make it easy to document your own API, WSME provides a Sphinx_ extension.
|
|
||||||
|
|
||||||
Install the extension
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
Here we consider that you already quick-started a sphinx project.
|
|
||||||
|
|
||||||
#. In your ``conf.py`` file, add ``'ext'`` to your extensions,
|
|
||||||
and optionally set the enabled protocols.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
extensions = ['ext']
|
|
||||||
|
|
||||||
wsme_protocols = ['restjson', 'restxml', 'extdirect']
|
|
||||||
|
|
||||||
#. Copy :download:`toggle.js <_static/toggle.js>`
|
|
||||||
and :download:`toggle.css <_static/toggle.css>`
|
|
||||||
in your _static directory.
|
|
||||||
|
|
||||||
The ``wsme`` domain
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
The extension will add a new Sphinx domain providing a few directives.
|
|
||||||
|
|
||||||
Config values
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. confval:: wsme_protocols
|
|
||||||
|
|
||||||
A list of strings that are WSME protocol names. If provided by an
|
|
||||||
additional package (for example WSME-Soap or WSME-ExtDirect), that package must
|
|
||||||
be installed.
|
|
||||||
|
|
||||||
The types and services generated documentation will include code samples
|
|
||||||
for each of these protocols.
|
|
||||||
|
|
||||||
.. confval:: wsme_root
|
|
||||||
|
|
||||||
A string that is the full name of the service root controller.
|
|
||||||
It will be used
|
|
||||||
to determinate the relative path of the other controllers when they
|
|
||||||
are autodocumented, and calculate the complete webpath of the other
|
|
||||||
controllers.
|
|
||||||
|
|
||||||
.. confval:: wsme_webpath
|
|
||||||
|
|
||||||
A string that is the webpath where the :confval:`wsme_root` is mounted.
|
|
||||||
|
|
||||||
Directives
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
.. rst:directive:: .. root:: <WSRoot full path>
|
|
||||||
|
|
||||||
Define the service root controller for this documentation source file.
|
|
||||||
To set it globally, see :confval:`wsme_root`.
|
|
||||||
|
|
||||||
A ``webpath`` option allows override of :confval:`wsme_webpath`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: rst
|
|
||||||
|
|
||||||
.. wsme:root:: myapp.controllers.MyWSRoot
|
|
||||||
:webpath: /api
|
|
||||||
|
|
||||||
.. rst:directive:: .. service:: name/space/ServiceName
|
|
||||||
|
|
||||||
Declare a service.
|
|
||||||
|
|
||||||
.. rst:directive:: .. type:: MyComplexType
|
|
||||||
|
|
||||||
Equivalent to the :rst:dir:`py:class` directive to document a complex type
|
|
||||||
|
|
||||||
.. rst:directive:: .. attribute:: aname
|
|
||||||
|
|
||||||
Equivalent to the :rst:dir:`py:attribute` directive to document a complex type
|
|
||||||
attribute. It takes an additional ``:type:`` field.
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - Source
|
|
||||||
- Result
|
|
||||||
|
|
||||||
* - .. code-block:: rst
|
|
||||||
|
|
||||||
.. wsme:root:: wsmeext.sphinxext.SampleService
|
|
||||||
:webpath: /api
|
|
||||||
|
|
||||||
.. wsme:type:: MyType
|
|
||||||
|
|
||||||
.. wsme:attribute:: test
|
|
||||||
|
|
||||||
:type: int
|
|
||||||
|
|
||||||
.. wsme:service:: name/space/SampleService
|
|
||||||
|
|
||||||
.. wsme:function:: doit
|
|
||||||
|
|
||||||
- .. wsme:root:: wsmeext.sphinxext.SampleService
|
|
||||||
:webpath: /api
|
|
||||||
|
|
||||||
.. wsme:type:: MyType
|
|
||||||
|
|
||||||
.. wsme:attribute:: test
|
|
||||||
|
|
||||||
:type: int
|
|
||||||
|
|
||||||
.. wsme:service:: name/space/SampleService
|
|
||||||
|
|
||||||
.. wsme:function:: getType
|
|
||||||
|
|
||||||
Returns a :wsme:type:`MyType <MyType>`
|
|
||||||
|
|
||||||
|
|
||||||
Autodoc directives
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Theses directives scan your code to generate the documentation from the
|
|
||||||
docstrings and your API types and controllers.
|
|
||||||
|
|
||||||
.. rst:directive:: .. autotype:: myapp.MyType
|
|
||||||
|
|
||||||
Generate the myapp.MyType documentation.
|
|
||||||
|
|
||||||
.. rst:directive:: .. autoattribute:: myapp.MyType.aname
|
|
||||||
|
|
||||||
Generate the myapp.MyType.aname documentation.
|
|
||||||
|
|
||||||
.. rst:directive:: .. autoservice:: myapp.MyService
|
|
||||||
|
|
||||||
Generate the myapp.MyService documentation.
|
|
||||||
|
|
||||||
.. rst:directive:: .. autofunction:: myapp.MyService.myfunction
|
|
||||||
|
|
||||||
Generate the myapp.MyService.myfunction documentation.
|
|
||||||
|
|
||||||
Full Example
|
|
||||||
------------
|
|
||||||
|
|
||||||
Python source
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. literalinclude:: ../wsmeext/sphinxext.py
|
|
||||||
:lines: 69-96
|
|
||||||
:language: python
|
|
||||||
|
|
||||||
Documentation source
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code-block:: rst
|
|
||||||
|
|
||||||
.. default-domain:: wsmeext
|
|
||||||
|
|
||||||
.. type:: int
|
|
||||||
|
|
||||||
An integer
|
|
||||||
|
|
||||||
.. autotype:: wsmeext.sphinxext.SampleType
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoservice:: wsmeext.sphinxext.SampleService
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Result
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
.. default-domain:: wsmeext
|
|
||||||
|
|
||||||
.. type:: int
|
|
||||||
|
|
||||||
An integer
|
|
||||||
|
|
||||||
.. autotype:: wsmeext.sphinxext.SampleType
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoservice:: wsmeext.sphinxext.SampleService
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
.. _Sphinx: http://sphinx.pocoo.org/
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
Functions
|
|
||||||
=========
|
|
||||||
|
|
||||||
WSME is based on the idea that most of the time the input and output of web
|
|
||||||
services are actually strictly typed. It uses this idea to ease the
|
|
||||||
implementation of the actual functions by handling those input/output.
|
|
||||||
It also proposes alternate protocols on top of a proper REST api.
|
|
||||||
|
|
||||||
This chapter explains in detail how to 'sign' a function with WSME.
|
|
||||||
|
|
||||||
The decorators
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Depending on the framework you are using, you will have to use either a
|
|
||||||
@\ :class:`wsme.signature` decorator or a @\ :class:`wsme.wsexpose` decorator.
|
|
||||||
|
|
||||||
@signature
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
The base @\ :class:`wsme.signature` decorator defines the return and argument types
|
|
||||||
of the function, and if needed a few more options.
|
|
||||||
|
|
||||||
The Flask and Cornice adapters both propose a specific version of it, which
|
|
||||||
also wrap the function so that it becomes suitable for the host framework.
|
|
||||||
|
|
||||||
In any case, the use of @\ :class:`wsme.signature` has the same meaning: tell WSME what is the
|
|
||||||
signature of the function.
|
|
||||||
|
|
||||||
@wsexpose
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
The native Rest implementation, and the TG and Pecan adapters add a @\ :class:`wsme.wsexpose`
|
|
||||||
decorator.
|
|
||||||
|
|
||||||
It does what @\ :class:`wsme.signature` does, *and* exposes the function in the routing system
|
|
||||||
of the host framework.
|
|
||||||
|
|
||||||
This decorator is generally used in an object-dispatch routing context.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Since both decorators play the same role, the rest of this
|
|
||||||
document will alway use @signature.
|
|
||||||
|
|
||||||
Signing a function
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Signing a function is just a matter of decorating it with @signature:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@signature(int, int, int)
|
|
||||||
def multiply(a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
In this trivial example, we tell WSME that the 'multiply' function returns an
|
|
||||||
integer, and takes two integer parameters.
|
|
||||||
|
|
||||||
WSME will match the argument types by order to determine the exact type of each
|
|
||||||
named argument. This is important since most of the web service protocols don't
|
|
||||||
provide strict argument ordering but only named parameters.
|
|
||||||
|
|
||||||
Optional arguments
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Defining an argument as optional is done by providing a default value:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@signature(int, int, int):
|
|
||||||
def increment(value, delta=1):
|
|
||||||
return value + delta
|
|
||||||
|
|
||||||
In this example, the caller may omit the 'delta' argument, and no
|
|
||||||
'MissingArgument' error will be raised.
|
|
||||||
|
|
||||||
Additionally, this argument will be documented as optional by the sphinx
|
|
||||||
extension.
|
|
||||||
|
|
||||||
Body argument
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
When defining a Rest CRUD API, we generally have a URL to which we POST data.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@signature(Author, Author)
|
|
||||||
def update_author(data):
|
|
||||||
# ...
|
|
||||||
return data
|
|
||||||
|
|
||||||
Such a function will take at least one parameter, 'data', that is a structured
|
|
||||||
type. With the default way of handling parameters, the body of the request
|
|
||||||
would look like this:
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"data":
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Pierre-Joseph"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
If you think (and you should) that it has one extra level of nesting, the 'body'
|
|
||||||
argument is here for you::
|
|
||||||
|
|
||||||
@signature(Author, body=Author)
|
|
||||||
def update_author(data):
|
|
||||||
# ...
|
|
||||||
return data
|
|
||||||
|
|
||||||
With this syntax, we can now post a simpler body:
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Pierre-Joseph"
|
|
||||||
}
|
|
||||||
|
|
||||||
Note that this does not prevent the function from having multiple parameters; it just requires
|
|
||||||
the body argument to be the last:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@signature(Author, bool, body=Author)
|
|
||||||
def update_author(force_update=False, data=None):
|
|
||||||
# ...
|
|
||||||
return data
|
|
||||||
|
|
||||||
In this case, the other arguments can be passed in the URL, in addition to the
|
|
||||||
body parameter. For example, a POST on ``/author/SOMEID?force_update=true``.
|
|
||||||
|
|
||||||
Status code
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
The default status codes returned by WSME are 200, 400 (if the client sends invalid
|
|
||||||
inputs) and 500 (for server-side errors).
|
|
||||||
|
|
||||||
Since a proper Rest API should use different return codes (201, etc), one can
|
|
||||||
use the 'status_code=' option of @signature to do so.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@signature(Author, body=Author, status_code=201)
|
|
||||||
def create_author(data):
|
|
||||||
# ...
|
|
||||||
return data
|
|
||||||
|
|
||||||
Of course this code will only be used if no error occurs.
|
|
||||||
|
|
||||||
In case the function needs to change the status code on a per-request basis, it
|
|
||||||
can return a :class:`wsme.Response` object, allowing it to override the status
|
|
||||||
code:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@signature(Author, body=Author, status_code=202)
|
|
||||||
def update_author(data):
|
|
||||||
# ...
|
|
||||||
response = Response(data)
|
|
||||||
if transaction_finished_and_successful:
|
|
||||||
response.status_code = 200
|
|
||||||
return response
|
|
||||||
|
|
||||||
Extra arguments
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The default behavior of WSME is to reject requests that give extra/unknown
|
|
||||||
arguments. In some (rare) cases, this is undesirable.
|
|
||||||
|
|
||||||
Adding 'ignore_extra_args=True' to @signature changes this behavior.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If using this option seems to solve your problem, please think twice
|
|
||||||
before using it!
|
|
||||||
|
|
||||||
Accessing the request
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Most of the time direct access to the request object should not be needed, but
|
|
||||||
in some cases it is.
|
|
||||||
|
|
||||||
On frameworks that propose a global access to the current request it is not an
|
|
||||||
issue, but on frameworks like pyramid it is not the way to go.
|
|
||||||
|
|
||||||
To handle this use case, WSME has a special type, :class:`HostRequest`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from wsme.types import HostRequest
|
|
||||||
|
|
||||||
@signature(Author, HostRequest, body=Author)
|
|
||||||
def create_author(request, newauthor):
|
|
||||||
# ...
|
|
||||||
return newauthor
|
|
||||||
|
|
||||||
In this example, the request object of the host framework will be passed as the
|
|
||||||
``request`` parameter of the create_author function.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
For now here is just a working example.
|
|
||||||
You can find it in the examples directory of the source distribution.
|
|
||||||
|
|
||||||
.. literalinclude:: ../examples/demo/demo.py
|
|
||||||
:language: python
|
|
||||||
|
|
||||||
When running this example, the following soap client can interrogate
|
|
||||||
the web services:
|
|
||||||
|
|
||||||
.. literalinclude:: ../examples/demo/client.py
|
|
||||||
:language: python
|
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
|
|
||||||
.. include:: ../README.rst
|
|
||||||
|
|
||||||
Contents
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
gettingstarted
|
|
||||||
api
|
|
||||||
types
|
|
||||||
functions
|
|
||||||
protocols
|
|
||||||
integrate
|
|
||||||
document
|
|
||||||
|
|
||||||
todo
|
|
||||||
changes
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
Integrating with a Framework
|
|
||||||
============================
|
|
||||||
|
|
||||||
General considerations
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
Using WSME within another framework providing its own REST capabilities is
|
|
||||||
generally done by using a specific decorator to declare the function signature,
|
|
||||||
in addition to the framework's own way of declaring exposed functions.
|
|
||||||
|
|
||||||
This decorator can have two different names depending on the adapter.
|
|
||||||
|
|
||||||
``@wsexpose``
|
|
||||||
This decorator will declare the function signature *and*
|
|
||||||
take care of calling the adequate decorators of the framework.
|
|
||||||
|
|
||||||
Generally this decorator is provided for frameworks that use
|
|
||||||
object-dispatch controllers, such as :ref:`adapter-pecan` and
|
|
||||||
:ref:`adapter-tg1`.
|
|
||||||
|
|
||||||
``@signature``
|
|
||||||
This decorator only sets the function signature and returns a function
|
|
||||||
that can be used by the host framework as a REST request target.
|
|
||||||
|
|
||||||
Generally this decorator is provided for frameworks that expect functions
|
|
||||||
taking a request object as a single parameter and returning a response
|
|
||||||
object. This is the case for :ref:`adapter-cornice` and
|
|
||||||
:ref:`adapter-flask`.
|
|
||||||
|
|
||||||
If you want to enable additional protocols, you will need to
|
|
||||||
mount a :class:`WSRoot` instance somewhere in the application, generally
|
|
||||||
``/ws``. This subpath will then handle the additional protocols. In a future
|
|
||||||
version, a WSGI middleware will probably play this role.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Not all the adapters are at the same level of maturity.
|
|
||||||
|
|
||||||
WSGI Application
|
|
||||||
----------------
|
|
||||||
|
|
||||||
The :func:`wsme.WSRoot.wsgiapp` function of WSRoot returns a WSGI
|
|
||||||
application.
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
The following example assumes the REST protocol will be entirely handled by
|
|
||||||
WSME, which is the case if you write a WSME standalone application.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from wsme import WSRoot, expose
|
|
||||||
|
|
||||||
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
@expose(unicode)
|
|
||||||
def helloworld(self):
|
|
||||||
return u"Hello World !"
|
|
||||||
|
|
||||||
root = MyRoot(protocols=['restjson'])
|
|
||||||
application = root.wsgiapp()
|
|
||||||
|
|
||||||
|
|
||||||
.. _adapter-cornice:
|
|
||||||
|
|
||||||
Cornice
|
|
||||||
-------
|
|
||||||
|
|
||||||
.. _cornice: http://cornice.readthedocs.org/en/latest/
|
|
||||||
|
|
||||||
*"* Cornice_ *provides helpers to build & document REST-ish Web Services with
|
|
||||||
Pyramid, with decent default behaviors. It takes care of following the HTTP
|
|
||||||
specification in an automated way where possible."*
|
|
||||||
|
|
||||||
|
|
||||||
:mod:`wsmeext.cornice` -- Cornice adapter
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. module:: wsmeext.cornice
|
|
||||||
|
|
||||||
.. function:: signature
|
|
||||||
|
|
||||||
Declare the parameters of a function and returns a function suitable for
|
|
||||||
cornice (ie that takes a request and returns a response).
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
To use WSME with Cornice you have to add a configuration option to your Pyramid application.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pyramid.config import Configurator
|
|
||||||
|
|
||||||
|
|
||||||
def make_app():
|
|
||||||
config = Configurator()
|
|
||||||
config.include("cornice")
|
|
||||||
config.include("wsmeext.cornice") # This includes WSME cornice support
|
|
||||||
# ...
|
|
||||||
return config.make_wsgi_app()
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from cornice import Service
|
|
||||||
from wsmeext.cornice import signature
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
hello = Service(name='hello', path='/', description="Simplest app")
|
|
||||||
|
|
||||||
class Info(wsme.types.Base):
|
|
||||||
message = wsme.types.text
|
|
||||||
|
|
||||||
|
|
||||||
@hello.get()
|
|
||||||
@signature(Info)
|
|
||||||
def get_info():
|
|
||||||
"""Returns Hello in JSON or XML."""
|
|
||||||
return Info(message='Hello World')
|
|
||||||
|
|
||||||
|
|
||||||
@hello.post()
|
|
||||||
@signature(None, Info)
|
|
||||||
def set_info(info):
|
|
||||||
print("Got a message: %s" % info.message)
|
|
||||||
|
|
||||||
|
|
||||||
.. _adapter-flask:
|
|
||||||
|
|
||||||
Flask
|
|
||||||
-----
|
|
||||||
|
|
||||||
*"Flask is a microframework for Python based on Werkzeug, Jinja 2 and good
|
|
||||||
intentions. And before you ask: It's BSD licensed! "*
|
|
||||||
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
Flask support is limited to function signature handling. It does not
|
|
||||||
support additional protocols. This is a temporary limitation, if you have
|
|
||||||
needs on that matter please tell us at python-wsme@googlegroups.com.
|
|
||||||
|
|
||||||
|
|
||||||
:mod:`wsmeext.flask` -- Flask adapter
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. module:: wsmeext.flask
|
|
||||||
|
|
||||||
.. function:: signature(return_type, \*arg_types, \*\*options)
|
|
||||||
|
|
||||||
See @\ :func:`signature` for parameters documentation.
|
|
||||||
|
|
||||||
Can be used on a function before routing it with flask.
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from wsmeext.flask import signature
|
|
||||||
|
|
||||||
@app.route('/multiply')
|
|
||||||
@signature(int, int, int)
|
|
||||||
def multiply(a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
.. _adapter-pecan:
|
|
||||||
|
|
||||||
Pecan
|
|
||||||
-----
|
|
||||||
|
|
||||||
*"*\ Pecan_ *was created to fill a void in the Python web-framework world –
|
|
||||||
a very lightweight framework that provides object-dispatch style routing.
|
|
||||||
Pecan does not aim to be a "full stack" framework, and therefore includes
|
|
||||||
no out of the box support for things like sessions or databases. Pecan
|
|
||||||
instead focuses on HTTP itself."*
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
A pecan application is not able to mount another WSGI application on a
|
|
||||||
subpath. For that reason, additional protocols are not supported for now,
|
|
||||||
until WSME provides a middleware that can do the same as a mounted
|
|
||||||
WSRoot.
|
|
||||||
|
|
||||||
:mod:`wsmeext.pecan` -- Pecan adapter
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. module:: wsmeext.pecan
|
|
||||||
|
|
||||||
.. function:: wsexpose(return_type, \*arg_types, \*\*options)
|
|
||||||
|
|
||||||
See @\ :func:`signature` for parameters documentation.
|
|
||||||
|
|
||||||
Can be used on any function of a pecan
|
|
||||||
`RestController <http://pecan.readthedocs.org/en/latest/rest.html>`_
|
|
||||||
instead of the expose decorator from Pecan.
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
WSME can be configured through the application configation, by adding a 'wsme'
|
|
||||||
configuration entry in ``config.py``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
wsme = {
|
|
||||||
'debug': True
|
|
||||||
}
|
|
||||||
|
|
||||||
Valid configuration variables are :
|
|
||||||
|
|
||||||
- ``'debug'``: Whether or not to include exception tracebacks in the returned
|
|
||||||
server-side errors.
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
The `example <http://pecan.readthedocs.org/en/latest/rest.html#nesting-restcontroller>`_ from the Pecan documentation becomes:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from wsmeext.pecan import wsexpose
|
|
||||||
|
|
||||||
class BooksController(RestController):
|
|
||||||
@wsexpose(Book, int, int)
|
|
||||||
def get(self, author_id, id):
|
|
||||||
# ..
|
|
||||||
|
|
||||||
@wsexpose(Book, int, int, body=Book)
|
|
||||||
def put(self, author_id, id, book):
|
|
||||||
# ..
|
|
||||||
|
|
||||||
class AuthorsController(RestController):
|
|
||||||
books = BooksController()
|
|
||||||
|
|
||||||
.. _Pecan: http://pecanpy.org/
|
|
||||||
|
|
||||||
.. _adapter-tg1:
|
|
||||||
|
|
||||||
Turbogears 1.x
|
|
||||||
--------------
|
|
||||||
|
|
||||||
The TG adapters have an api very similar to TGWebServices. Migrating from it
|
|
||||||
should be straightforward (a little howto migrate would not hurt though, and it
|
|
||||||
will be written as soon as possible).
|
|
||||||
|
|
||||||
:mod:`wsmeext.tg11` -- TG 1.1 adapter
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. module:: wsmeext.tg11
|
|
||||||
|
|
||||||
.. function:: wsexpose(return_type, \*arg_types, \*\*options)
|
|
||||||
|
|
||||||
See @\ :func:`signature` for parameters documentation.
|
|
||||||
|
|
||||||
Can be used on any function of a controller
|
|
||||||
instead of the expose decorator from TG.
|
|
||||||
|
|
||||||
.. function:: wsvalidate(\*arg_types)
|
|
||||||
|
|
||||||
Set the argument types of an exposed function. This decorator is provided
|
|
||||||
so that WSME is an almost drop-in replacement for TGWebServices. If
|
|
||||||
starting from scratch you can use \ :func:`wsexpose` only
|
|
||||||
|
|
||||||
.. function:: adapt(wsroot)
|
|
||||||
|
|
||||||
Returns a TG1 controller instance that publish a :class:`wsme.WSRoot`.
|
|
||||||
It can then be mounted on a TG1 controller.
|
|
||||||
|
|
||||||
Because the adapt function modifies the cherrypy filters of the controller
|
|
||||||
the 'webpath' of the WSRoot instance must be consistent with the path it
|
|
||||||
will be mounted on.
|
|
||||||
|
|
||||||
:mod:`wsmeext.tg15` -- TG 1.5 adapter
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. module:: wsmeext.tg15
|
|
||||||
|
|
||||||
This adapter has the exact same api as :mod:`wsmeext.tg11`.
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
In a freshly quickstarted tg1 application (let's say, wsmedemo), you can add
|
|
||||||
REST-ish functions anywhere in your controller tree. Here directly on the root,
|
|
||||||
in controllers.py:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# ...
|
|
||||||
|
|
||||||
# For tg 1.5, import from wsmeext.tg15 instead :
|
|
||||||
from wsmeext.tg11 import wsexpose, WSRoot
|
|
||||||
|
|
||||||
class Root(controllers.RootController):
|
|
||||||
# Having a WSRoot on /ws is only required to enable additional
|
|
||||||
# protocols. For REST-only services, it can be ignored.
|
|
||||||
ws = adapt(
|
|
||||||
WSRoot(webpath='/ws', protocols=['soap'])
|
|
||||||
)
|
|
||||||
|
|
||||||
@wsexpose(int, int, int)
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
.. _TurboGears: http://www.turbogears.org/
|
|
||||||
|
|
||||||
Other frameworks
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Bottle
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
No adapter is provided yet but it should not be hard to write one, by taking
|
|
||||||
example on the cornice adapter.
|
|
||||||
|
|
||||||
This example only show how to mount a WSRoot inside a bottle application.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import bottle
|
|
||||||
import wsme
|
|
||||||
|
|
||||||
class MyRoot(wsme.WSRoot):
|
|
||||||
@wsme.expose(unicode)
|
|
||||||
def helloworld(self):
|
|
||||||
return u"Hello World !"
|
|
||||||
|
|
||||||
root = MyRoot(webpath='/ws', protocols=['restjson'])
|
|
||||||
|
|
||||||
bottle.mount('/ws', root.wsgiapp())
|
|
||||||
bottle.run()
|
|
||||||
|
|
||||||
Pyramid
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
The recommended way of using WSME inside Pyramid is to use
|
|
||||||
:ref:`adapter-cornice`.
|
|
||||||
155
doc/make.bat
155
doc/make.bat
@@ -1,155 +0,0 @@
|
|||||||
@ECHO OFF
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
)
|
|
||||||
set BUILDDIR=_build
|
|
||||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
|
||||||
if NOT "%PAPER%" == "" (
|
|
||||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "" goto help
|
|
||||||
|
|
||||||
if "%1" == "help" (
|
|
||||||
:help
|
|
||||||
echo.Please use `make ^<target^>` where ^<target^> 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. 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. changes to make an overview over all changed/added/deprecated items
|
|
||||||
echo. linkcheck to check all external links for integrity
|
|
||||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "clean" (
|
|
||||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
|
||||||
del /q /s %BUILDDIR%\*
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "html" (
|
|
||||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "dirhtml" (
|
|
||||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "singlehtml" (
|
|
||||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "pickle" (
|
|
||||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the pickle files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "json" (
|
|
||||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the JSON files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "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.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "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\WebServicesMadeEasy.qhcp
|
|
||||||
echo.To view the help file:
|
|
||||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WebServicesMadeEasy.ghc
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "devhelp" (
|
|
||||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
|
||||||
echo.
|
|
||||||
echo.Build finished.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "epub" (
|
|
||||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "latex" (
|
|
||||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
|
||||||
echo.
|
|
||||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "text" (
|
|
||||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "man" (
|
|
||||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "changes" (
|
|
||||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
|
||||||
echo.
|
|
||||||
echo.The overview file is in %BUILDDIR%/changes.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "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.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "doctest" (
|
|
||||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
|
||||||
echo.
|
|
||||||
echo.Testing of doctests in the sources finished, look at the ^
|
|
||||||
results in %BUILDDIR%/doctest/output.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
:end
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
Protocols
|
|
||||||
=========
|
|
||||||
|
|
||||||
In this document the same webservice example will be used to
|
|
||||||
illustrate the different protocols. Its source code is in the
|
|
||||||
last chapter (:ref:`protocols-the-example`).
|
|
||||||
|
|
||||||
REST
|
|
||||||
----
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This chapter applies for all adapters, not just the native REST
|
|
||||||
implementation.
|
|
||||||
|
|
||||||
The two REST protocols share common characterics.
|
|
||||||
|
|
||||||
Each function corresponds to distinct webpath that starts with the
|
|
||||||
root webpath, followed by the controllers names if any, and finally
|
|
||||||
the function name.
|
|
||||||
|
|
||||||
The example's exposed functions will be mapped to the following paths:
|
|
||||||
|
|
||||||
- ``/ws/persons/create``
|
|
||||||
- ``/ws/persons/get``
|
|
||||||
- ``/ws/persons/list``
|
|
||||||
- ``/ws/persons/update``
|
|
||||||
- ``/ws/persons/destroy``
|
|
||||||
|
|
||||||
In addition to this trivial function mapping, a `method` option can
|
|
||||||
be given to the `expose` decorator. In such a case, the function
|
|
||||||
name can be omitted by the caller, and the dispatch will look at the
|
|
||||||
HTTP method used in the request to select the correct function.
|
|
||||||
|
|
||||||
The function parameters can be transmitted in two ways (if using
|
|
||||||
the HTTP method to select the function, one way or the other
|
|
||||||
may be usable) :
|
|
||||||
|
|
||||||
#. As a GET query string or POST form parameters.
|
|
||||||
|
|
||||||
Simple types are straight forward :
|
|
||||||
|
|
||||||
``/ws/person/get?id=5``
|
|
||||||
|
|
||||||
Complex types can be transmitted this way:
|
|
||||||
|
|
||||||
``/ws/person/update?p.id=1&p.name=Ross&p.hobbies[0]=Dinausaurs&p.hobbies[1]=Rachel``
|
|
||||||
|
|
||||||
#. In a Json or XML encoded POST body (see below)
|
|
||||||
|
|
||||||
The result will be returned Json or XML encoded (see below).
|
|
||||||
|
|
||||||
In case of error, a 400 or 500 status code is returned, and the
|
|
||||||
response body contains details about the error (see below).
|
|
||||||
|
|
||||||
REST+Json
|
|
||||||
---------
|
|
||||||
|
|
||||||
:name: ``'restjson'``
|
|
||||||
|
|
||||||
Implements a REST+Json protocol.
|
|
||||||
|
|
||||||
This protocol is selected if:
|
|
||||||
|
|
||||||
- The request content-type is either 'text/javascript' or 'application/json'
|
|
||||||
- The request 'Accept' header contains 'text/javascript' or 'application/json'
|
|
||||||
- A trailing '.json' is added to the path
|
|
||||||
- A 'wsmeproto=restjson' is added in the query string
|
|
||||||
|
|
||||||
Options
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
:nest_result: Nest the encoded result in a result param of an object.
|
|
||||||
For example, a result of ``2`` would be ``{'result': 2}``
|
|
||||||
|
|
||||||
Types
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| Type | Json type |
|
|
||||||
+===============+===============================+
|
|
||||||
| ``str`` | String |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``unicode`` | String |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``int`` | Number |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``float`` | Number |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``bool`` | Boolean |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``Decimal`` | String |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``date`` | String (YYYY-MM-DD) |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``time`` | String (hh:mm:ss) |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| ``datetime`` | String (YYYY-MM-DDThh:mm:ss) |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| Arrays | Array |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| None | null |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
| Complex types | Object |
|
|
||||||
+---------------+-------------------------------+
|
|
||||||
|
|
||||||
Return
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
The Json encoded result when the response code is 200, or a Json object
|
|
||||||
with error properties ('faulcode', 'faultstring' and 'debuginfo' if
|
|
||||||
available) on error.
|
|
||||||
|
|
||||||
For example, the '/ws/person/get' result looks like:
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
'id': 2
|
|
||||||
'fistname': 'Monica',
|
|
||||||
'lastname': 'Geller',
|
|
||||||
'age': 28,
|
|
||||||
'hobbies': [
|
|
||||||
'Food',
|
|
||||||
'Cleaning'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
And in case of error:
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
'faultcode': 'Client',
|
|
||||||
'faultstring': 'id is missing'
|
|
||||||
}
|
|
||||||
|
|
||||||
REST+XML
|
|
||||||
--------
|
|
||||||
|
|
||||||
:name: ``'restxml'``
|
|
||||||
|
|
||||||
This protocol is selected if
|
|
||||||
|
|
||||||
- The request content-type is 'text/xml'
|
|
||||||
- The request 'Accept' header contains 'text/xml'
|
|
||||||
- A trailing '.xml' is added to the path
|
|
||||||
- A 'wsmeproto=restxml' is added in the query string
|
|
||||||
|
|
||||||
Types
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| Type | XML example |
|
|
||||||
+===============+========================================+
|
|
||||||
| ``str`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>a string</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``unicode`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>a string</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``int`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>5</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``float`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>3.14</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``bool`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>true</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``Decimal`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>5.46</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``date`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>2010-04-27</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``time`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>12:54:18</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| ``datetime`` | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value>2010-04-27T12:54:18</value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| Arrays | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value> |
|
|
||||||
| | <item>Dinausaurs<item> |
|
|
||||||
| | <item>Rachel<item> |
|
|
||||||
| | </value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| None | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value nil="true"/> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
| Complex types | .. code-block:: xml |
|
|
||||||
| | |
|
|
||||||
| | <value> |
|
|
||||||
| | <id>1</id> |
|
|
||||||
| | <fistname>Ross</fistname> |
|
|
||||||
| | </value> |
|
|
||||||
+---------------+----------------------------------------+
|
|
||||||
|
|
||||||
Return
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
A xml tree with a top 'result' element.
|
|
||||||
|
|
||||||
.. code-block:: xml
|
|
||||||
|
|
||||||
<result>
|
|
||||||
<id>1</id>
|
|
||||||
<firstname>Ross</firstname>
|
|
||||||
<lastname>Geller</lastname>
|
|
||||||
</result>
|
|
||||||
|
|
||||||
Errors
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
A xml tree with a top 'error' element, having 'faultcode', 'faultstring'
|
|
||||||
and 'debuginfo' subelements:
|
|
||||||
|
|
||||||
.. code-block:: xml
|
|
||||||
|
|
||||||
<error>
|
|
||||||
<faultcode>Client</faultcode>
|
|
||||||
<faultstring>id is missing</faultstring>
|
|
||||||
</error>
|
|
||||||
|
|
||||||
SOAP
|
|
||||||
----
|
|
||||||
|
|
||||||
:name: ``'soap'``
|
|
||||||
|
|
||||||
Implements the SOAP protocol.
|
|
||||||
|
|
||||||
A wsdl definition of the webservice is available at the 'api.wsdl' subpath.
|
|
||||||
(``/ws/api.wsdl`` in our example).
|
|
||||||
|
|
||||||
The protocol is selected if the request matches one of the following condition:
|
|
||||||
|
|
||||||
- The Content-Type is 'application/soap+xml'
|
|
||||||
- A header 'Soapaction' is present
|
|
||||||
|
|
||||||
Options
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
:tns: Type namespace
|
|
||||||
|
|
||||||
ExtDirect
|
|
||||||
---------
|
|
||||||
|
|
||||||
:name: ``extdirect``
|
|
||||||
|
|
||||||
Implements the `Ext Direct`_ protocol.
|
|
||||||
|
|
||||||
The provider definition is made available at the ``/extdirect/api.js`` subpath.
|
|
||||||
|
|
||||||
The router url is ``/extdirect/router[/subnamespace]``.
|
|
||||||
|
|
||||||
Options
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
:namespace: Base namespace of the api. Used for the provider definition.
|
|
||||||
:params_notation: Default notation for function call parameters. Can be
|
|
||||||
overriden for individual functions by adding the
|
|
||||||
``extdirect_params_notation`` extra option to @expose.
|
|
||||||
|
|
||||||
The possible notations are :
|
|
||||||
|
|
||||||
- ``'named'`` -- The function will take only one object parameter
|
|
||||||
in which each property will be one of the parameters.
|
|
||||||
- ``'positional'`` -- The function will take as many parameters as
|
|
||||||
the function has, and their position will determine which parameter
|
|
||||||
they are.
|
|
||||||
|
|
||||||
expose extra options
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
:extdirect_params_notation: Override the params_notation for a particular
|
|
||||||
function.
|
|
||||||
|
|
||||||
.. _Ext Direct: http://www.sencha.com/products/extjs/extdirect
|
|
||||||
|
|
||||||
.. _protocols-the-example:
|
|
||||||
|
|
||||||
The example
|
|
||||||
-----------
|
|
||||||
|
|
||||||
In this document the same webservice example will be used to
|
|
||||||
illustrate the different protocols:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Person(object):
|
|
||||||
id = int
|
|
||||||
lastname = unicode
|
|
||||||
firstname = unicode
|
|
||||||
age = int
|
|
||||||
|
|
||||||
hobbies = [unicode]
|
|
||||||
|
|
||||||
def __init__(self, id=None, lastname=None, firstname=None, age=None,
|
|
||||||
hobbies=None):
|
|
||||||
if id:
|
|
||||||
self.id = id
|
|
||||||
if lastname:
|
|
||||||
self.lastname = lastname
|
|
||||||
if firstname:
|
|
||||||
self.firstname = firstname
|
|
||||||
if age:
|
|
||||||
self.age = age
|
|
||||||
if hobbies:
|
|
||||||
self.hobbies = hobbies
|
|
||||||
|
|
||||||
persons = {
|
|
||||||
1: Person(1, "Geller", "Ross", 30, ["Dinosaurs", "Rachel"]),
|
|
||||||
2: Person(2, "Geller", "Monica", 28, ["Food", "Cleaning"])
|
|
||||||
}
|
|
||||||
|
|
||||||
class PersonController(object):
|
|
||||||
@expose(Person)
|
|
||||||
@validate(int)
|
|
||||||
def get(self, id):
|
|
||||||
return persons[id]
|
|
||||||
|
|
||||||
@expose([Person])
|
|
||||||
def list(self):
|
|
||||||
return persons.values()
|
|
||||||
|
|
||||||
@expose(Person)
|
|
||||||
@validate(Person)
|
|
||||||
def update(self, p):
|
|
||||||
if p.id is Unset:
|
|
||||||
raise ClientSideError("id is missing")
|
|
||||||
persons[p.id] = p
|
|
||||||
return p
|
|
||||||
|
|
||||||
@expose(Person)
|
|
||||||
@validate(Person)
|
|
||||||
def create(self, p):
|
|
||||||
if p.id is not Unset:
|
|
||||||
raise ClientSideError("I don't want an id")
|
|
||||||
p.id = max(persons.keys()) + 1
|
|
||||||
persons[p.id] = p
|
|
||||||
return p
|
|
||||||
|
|
||||||
@expose()
|
|
||||||
@validate(int)
|
|
||||||
def destroy(self, id):
|
|
||||||
if id not in persons:
|
|
||||||
raise ClientSideError("Unknown ID")
|
|
||||||
|
|
||||||
|
|
||||||
class WS(WSRoot):
|
|
||||||
person = PersonController()
|
|
||||||
|
|
||||||
root = WS(webpath='ws')
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
sphinx
|
|
||||||
cloud_sptheme
|
|
||||||
-r ../requirements.txt
|
|
||||||
31
doc/todo.rst
31
doc/todo.rst
@@ -1,31 +0,0 @@
|
|||||||
TODO
|
|
||||||
====
|
|
||||||
|
|
||||||
WSME is a work in progress. Here is a list of things that should
|
|
||||||
be done :
|
|
||||||
|
|
||||||
- Use gevents for batch-calls
|
|
||||||
|
|
||||||
- Implement new protocols :
|
|
||||||
|
|
||||||
- json-rpc
|
|
||||||
|
|
||||||
- xml-rpc
|
|
||||||
|
|
||||||
- Implement adapters for other frameworks :
|
|
||||||
|
|
||||||
- TurboGears 2
|
|
||||||
|
|
||||||
- Pylons
|
|
||||||
|
|
||||||
- CherryPy
|
|
||||||
|
|
||||||
- Flask
|
|
||||||
|
|
||||||
- others ?
|
|
||||||
|
|
||||||
- Add unittests for adapters
|
|
||||||
|
|
||||||
- Address the authentication subject (which should be handled by
|
|
||||||
some other wsgi framework/middleware, but a little integration
|
|
||||||
could help).
|
|
||||||
246
doc/types.rst
246
doc/types.rst
@@ -1,246 +0,0 @@
|
|||||||
Types
|
|
||||||
=====
|
|
||||||
|
|
||||||
Three kinds of data types can be used as input or output by WSME.
|
|
||||||
|
|
||||||
Native types
|
|
||||||
------------
|
|
||||||
|
|
||||||
The native types are a fixed set of standard Python types that
|
|
||||||
different protocols map to their own basic types.
|
|
||||||
|
|
||||||
The native types are :
|
|
||||||
|
|
||||||
- .. wsme:type:: bytes
|
|
||||||
|
|
||||||
A pure-ascii string (:py:class:`wsme.types.bytes` which is
|
|
||||||
:py:class:`str` in Python 2 and :py:class:`bytes` in Python 3).
|
|
||||||
|
|
||||||
|
|
||||||
- .. wsme:type:: text
|
|
||||||
|
|
||||||
A unicode string (:py:class:`wsme.types.text` which is
|
|
||||||
:py:class:`unicode` in Python 2 and :py:class:`str` in Python 3).
|
|
||||||
|
|
||||||
- .. wsme:type:: int
|
|
||||||
|
|
||||||
An integer (:py:class:`int`)
|
|
||||||
|
|
||||||
- .. wsme:type:: float
|
|
||||||
|
|
||||||
A float (:py:class:`float`)
|
|
||||||
|
|
||||||
- .. wsme:type:: bool
|
|
||||||
|
|
||||||
A boolean (:py:class:`bool`)
|
|
||||||
|
|
||||||
- .. wsme:type:: Decimal
|
|
||||||
|
|
||||||
A fixed-width decimal (:py:class:`decimal.Decimal`)
|
|
||||||
|
|
||||||
- .. wsme:type:: date
|
|
||||||
|
|
||||||
A date (:py:class:`datetime.date`)
|
|
||||||
|
|
||||||
- .. wsme:type:: datetime
|
|
||||||
|
|
||||||
A date and time (:py:class:`datetime.datetime`)
|
|
||||||
|
|
||||||
- .. wsme:type:: time
|
|
||||||
|
|
||||||
A time (:py:class:`datetime.time`)
|
|
||||||
|
|
||||||
- Arrays -- This is a special case. When stating a list
|
|
||||||
datatype, always state its content type as the unique element
|
|
||||||
of a list. Example::
|
|
||||||
|
|
||||||
class SomeWebService(object):
|
|
||||||
@expose([str])
|
|
||||||
def getlist(self):
|
|
||||||
return ['a', 'b', 'c']
|
|
||||||
|
|
||||||
- Dictionaries -- Statically typed mappings are allowed. When exposing
|
|
||||||
a dictionary datatype, you can specify the key and value types,
|
|
||||||
with a restriction on the key value that must be a 'pod' type.
|
|
||||||
Example::
|
|
||||||
|
|
||||||
class SomeType(object):
|
|
||||||
amap = {str: SomeOthertype}
|
|
||||||
|
|
||||||
There are other types that are supported out of the box. See the
|
|
||||||
:ref:`pre-defined-user-types`.
|
|
||||||
|
|
||||||
User types
|
|
||||||
----------
|
|
||||||
|
|
||||||
User types allow you to define new, almost-native types.
|
|
||||||
|
|
||||||
The idea is that you may have Python data that should be transported as base
|
|
||||||
types by the different protocols, but needs conversion to/from these base types,
|
|
||||||
or needs to validate data integrity.
|
|
||||||
|
|
||||||
To define a user type, you just have to inherit from
|
|
||||||
:class:`wsme.types.UserType` and instantiate your new class. This instance
|
|
||||||
will be your new type and can be used as @\ :class:`wsme.expose` or
|
|
||||||
@\ :class:`wsme.validate` parameters.
|
|
||||||
|
|
||||||
Note that protocols can choose to specifically handle a user type or
|
|
||||||
a base class of user types. This is case with the two pre-defined
|
|
||||||
user types, :class:`wsme.types.Enum` and :data:`wsme.types.binary`.
|
|
||||||
|
|
||||||
.. _pre-defined-user-types:
|
|
||||||
|
|
||||||
Pre-defined user types
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
WSME provides some pre-defined user types:
|
|
||||||
|
|
||||||
- :class:`binary <wsme.types.binary>` -- for transporting binary data as
|
|
||||||
base64 strings.
|
|
||||||
- :class:`Enum <wsme.types.Enum>` -- enforce that the values belongs to a
|
|
||||||
pre-defined list of values.
|
|
||||||
|
|
||||||
These types are good examples of how to define user types. Have
|
|
||||||
a look at their source code!
|
|
||||||
|
|
||||||
Here is a little example that combines :class:`binary <wsme.types.binary>`
|
|
||||||
and :class:`Enum <wsme.types.Enum>`::
|
|
||||||
|
|
||||||
ImageKind = Enum(str, 'jpeg', 'gif')
|
|
||||||
|
|
||||||
class Image(object):
|
|
||||||
name = unicode
|
|
||||||
kind = ImageKind
|
|
||||||
data = binary
|
|
||||||
|
|
||||||
.. data:: wsme.types.binary
|
|
||||||
|
|
||||||
The :class:`wsme.types.BinaryType` instance to use when you need to
|
|
||||||
transfer base64 encoded data.
|
|
||||||
|
|
||||||
.. autoclass:: wsme.types.BinaryType
|
|
||||||
|
|
||||||
.. autoclass:: wsme.types.Enum
|
|
||||||
|
|
||||||
|
|
||||||
Complex types
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Complex types are structured types. They are defined as simple Python classes
|
|
||||||
and will be mapped to adequate structured types in the various protocols.
|
|
||||||
|
|
||||||
A base class for structured types is provided, :class:`wsme.types.Base`,
|
|
||||||
but is not mandatory. The only thing it adds is a default constructor.
|
|
||||||
|
|
||||||
The attributes that are set at the class level will be used by WSME to discover
|
|
||||||
the structure. These attributes can be:
|
|
||||||
|
|
||||||
- A datatype -- Any native, user or complex type.
|
|
||||||
- A :class:`wsattr <wsme.wsattr>` -- This allows you to add more information about
|
|
||||||
the attribute, for example if it is mandatory.
|
|
||||||
- A :class:`wsproperty <wsme.wsproperty>` -- A special typed property. Works
|
|
||||||
like standard ``property`` with additional properties like
|
|
||||||
:class:`wsattr <wsme.wsattr>`.
|
|
||||||
|
|
||||||
Attributes having a leading '_' in their name will be ignored, as well as the
|
|
||||||
attributes that are not in the above list. This means the type can have methods,
|
|
||||||
they will not get in the way.
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Gender = wsme.types.Enum(str, 'male', 'female')
|
|
||||||
Title = wsme.types.Enum(str, 'M', 'Mrs')
|
|
||||||
|
|
||||||
class Person(wsme.types.Base):
|
|
||||||
lastname = wsme.types.wsattr(unicode, mandatory=True)
|
|
||||||
firstname = wsme.types.wsattr(unicode, mandatory=True)
|
|
||||||
|
|
||||||
age = int
|
|
||||||
gender = Gender
|
|
||||||
title = Title
|
|
||||||
|
|
||||||
hobbies = [unicode]
|
|
||||||
|
|
||||||
Rules
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
A few things you should know about complex types:
|
|
||||||
|
|
||||||
- The class must have a default constructor --
|
|
||||||
Since instances of the type will be created by the protocols when
|
|
||||||
used as input types, they must be instantiable without any argument.
|
|
||||||
|
|
||||||
- Complex types are registered automatically
|
|
||||||
(and thus inspected) as soon a they are used in expose or validate,
|
|
||||||
even if they are nested in another complex type.
|
|
||||||
|
|
||||||
If for some reason you need to control when type is inspected, you
|
|
||||||
can use :func:`wsme.types.register_type`.
|
|
||||||
|
|
||||||
- The datatype attributes will be replaced.
|
|
||||||
|
|
||||||
When using the 'short' way of defining attributes, ie setting a
|
|
||||||
simple data type, they will be replaced by a wsattr instance.
|
|
||||||
|
|
||||||
So, when you write::
|
|
||||||
|
|
||||||
class Person(object):
|
|
||||||
name = unicode
|
|
||||||
|
|
||||||
After type registration the class will actually be equivalent to::
|
|
||||||
|
|
||||||
class Person(object):
|
|
||||||
name = wsattr(unicode)
|
|
||||||
|
|
||||||
You can still access the datatype by accessing the attribute on the
|
|
||||||
class, along with the other wsattr properties::
|
|
||||||
|
|
||||||
class Person(object):
|
|
||||||
name = unicode
|
|
||||||
|
|
||||||
register_type(Person)
|
|
||||||
|
|
||||||
assert Person.name.datatype is unicode
|
|
||||||
assert Person.name.key == "name"
|
|
||||||
assert Person.name.mandatory is False
|
|
||||||
|
|
||||||
- The default value of instance attributes is
|
|
||||||
:data:`Unset <wsme.Unset>`.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
class Person(object):
|
|
||||||
name = wsattr(unicode)
|
|
||||||
|
|
||||||
p = Person()
|
|
||||||
assert p.name is Unset
|
|
||||||
|
|
||||||
This allows the protocol to make a clear distinction between null values
|
|
||||||
that will be transmitted, and unset values that will not be transmitted.
|
|
||||||
|
|
||||||
For input values, it allows the code to know if the values were, or were not,
|
|
||||||
sent by the caller.
|
|
||||||
|
|
||||||
- When 2 complex types refer to each other, their names can be
|
|
||||||
used as datatypes to avoid adding attributes afterwards:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
class A(object):
|
|
||||||
b = wsattr('B')
|
|
||||||
|
|
||||||
class B(object):
|
|
||||||
a = wsattr(A)
|
|
||||||
|
|
||||||
|
|
||||||
Predefined Types
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. default-domain:: wsme
|
|
||||||
|
|
||||||
- .. autotype:: wsme.types.File
|
|
||||||
:members:
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
from suds.client import Client
|
|
||||||
|
|
||||||
url = 'http://127.0.0.1:8080/ws/api.wsdl'
|
|
||||||
|
|
||||||
client = Client(url, cache=None)
|
|
||||||
|
|
||||||
print client
|
|
||||||
|
|
||||||
print client.service.multiply(4, 5)
|
|
||||||
print client.service.helloworld()
|
|
||||||
print client.service.getperson()
|
|
||||||
p = client.service.listpersons()
|
|
||||||
print repr(p)
|
|
||||||
p = client.service.setpersons(p)
|
|
||||||
print repr(p)
|
|
||||||
|
|
||||||
p = client.factory.create('ns0:Person')
|
|
||||||
p.id = 4
|
|
||||||
print p
|
|
||||||
|
|
||||||
a = client.factory.create('ns0:Person_Array')
|
|
||||||
print a
|
|
||||||
|
|
||||||
a = client.service.setpersons(a)
|
|
||||||
print repr(a)
|
|
||||||
|
|
||||||
a.item.append(p)
|
|
||||||
print repr(a)
|
|
||||||
|
|
||||||
a = client.service.setpersons(a)
|
|
||||||
print repr(a)
|
|
||||||
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# coding=utf8
|
|
||||||
"""
|
|
||||||
A mini-demo of what wsme can do.
|
|
||||||
|
|
||||||
To run it::
|
|
||||||
|
|
||||||
python setup.py develop
|
|
||||||
|
|
||||||
Then::
|
|
||||||
|
|
||||||
python demo.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
from wsme import WSRoot, expose, validate
|
|
||||||
from wsme.types import File
|
|
||||||
|
|
||||||
import bottle
|
|
||||||
|
|
||||||
from six import u
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
class Person(object):
|
|
||||||
id = int
|
|
||||||
firstname = unicode
|
|
||||||
lastname = unicode
|
|
||||||
|
|
||||||
hobbies = [unicode]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "Person(%s, %s %s, %s)" % (
|
|
||||||
self.id,
|
|
||||||
self.firstname, self.lastname,
|
|
||||||
self.hobbies
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DemoRoot(WSRoot):
|
|
||||||
@expose(int)
|
|
||||||
@validate(int, int)
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
@expose(File)
|
|
||||||
@validate(File)
|
|
||||||
def echofile(self, afile):
|
|
||||||
return afile
|
|
||||||
|
|
||||||
@expose(unicode)
|
|
||||||
def helloworld(self):
|
|
||||||
return u"Здраво, свете (<- Hello World in Serbian !)"
|
|
||||||
|
|
||||||
@expose(Person)
|
|
||||||
def getperson(self):
|
|
||||||
p = Person()
|
|
||||||
p.id = 12
|
|
||||||
p.firstname = u'Ross'
|
|
||||||
p.lastname = u'Geler'
|
|
||||||
p.hobbies = []
|
|
||||||
print p
|
|
||||||
return p
|
|
||||||
|
|
||||||
@expose([Person])
|
|
||||||
def listpersons(self):
|
|
||||||
p = Person()
|
|
||||||
p.id = 12
|
|
||||||
p.firstname = u('Ross')
|
|
||||||
p.lastname = u('Geler')
|
|
||||||
r = [p]
|
|
||||||
p = Person()
|
|
||||||
p.id = 13
|
|
||||||
p.firstname = u('Rachel')
|
|
||||||
p.lastname = u('Green')
|
|
||||||
r.append(p)
|
|
||||||
print r
|
|
||||||
return r
|
|
||||||
|
|
||||||
@expose(Person)
|
|
||||||
@validate(Person)
|
|
||||||
def setperson(self, person):
|
|
||||||
return person
|
|
||||||
|
|
||||||
@expose([Person])
|
|
||||||
@validate([Person])
|
|
||||||
def setpersons(self, persons):
|
|
||||||
print persons
|
|
||||||
return persons
|
|
||||||
|
|
||||||
|
|
||||||
root = DemoRoot(webpath='/ws')
|
|
||||||
|
|
||||||
root.addprotocol('soap',
|
|
||||||
tns='http://example.com/demo',
|
|
||||||
typenamespace='http://example.com/demo/types',
|
|
||||||
baseURL='http://127.0.0.1:8080/ws/',
|
|
||||||
)
|
|
||||||
|
|
||||||
root.addprotocol('restjson')
|
|
||||||
|
|
||||||
bottle.mount('/ws/', root.wsgiapp())
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
bottle.run()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[easy_install]
|
|
||||||
find_links = http://www.owlfish.com/software/wsgiutils/download.html
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(name='demo',
|
|
||||||
install_requires=[
|
|
||||||
'WSME',
|
|
||||||
'Bottle',
|
|
||||||
'Pygments',
|
|
||||||
],
|
|
||||||
package=['demo'])
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import spyre
|
|
||||||
import spyre.middleware
|
|
||||||
|
|
||||||
|
|
||||||
class CTypeHeader(spyre.middleware.Middleware):
|
|
||||||
def __call__(self, env):
|
|
||||||
env.setdefault('spore.headers', [])
|
|
||||||
env['spore.headers'].extend([
|
|
||||||
('Accept', 'application/json'),
|
|
||||||
('Content-Type', 'application/json')
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
demo = spyre.new_from_url('http://127.0.0.1:8080/ws/api.spore')
|
|
||||||
demo.enable(CTypeHeader)
|
|
||||||
demo.enable('format.Json')
|
|
||||||
|
|
||||||
print demo.helloworld().content
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
six>=1.9.0
|
|
||||||
WebOb>=1.2.3
|
|
||||||
simplegeneric
|
|
||||||
pytz
|
|
||||||
netaddr>=0.7.12
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
six>=1.9.0
|
|
||||||
WebOb>=1.2.3
|
|
||||||
simplegeneric
|
|
||||||
pytz
|
|
||||||
netaddr>=0.7.12
|
|
||||||
51
setup.cfg
51
setup.cfg
@@ -1,51 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name = WSME
|
|
||||||
|
|
||||||
author = Christophe de Vienne
|
|
||||||
author-email = python-wsme@googlegroups.com
|
|
||||||
|
|
||||||
summary = Simplify the writing of REST APIs, and extend them with additional protocols.
|
|
||||||
|
|
||||||
description-file = README.rst
|
|
||||||
|
|
||||||
url = http://git.openstack.org/cgit/openstack/wsme
|
|
||||||
|
|
||||||
license = MIT
|
|
||||||
|
|
||||||
classifier =
|
|
||||||
Development Status :: 3 - Alpha
|
|
||||||
Operating System :: OS Independent
|
|
||||||
Programming Language :: Python
|
|
||||||
Programming Language :: Python :: 2.7
|
|
||||||
Programming Language :: Python :: 3.4
|
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
|
||||||
Programming Language :: Python :: Implementation :: PyPy
|
|
||||||
License :: OSI Approved :: MIT License
|
|
||||||
Topic :: Internet :: WWW/HTTP :: WSGI
|
|
||||||
Topic :: Software Development :: Libraries :: Python Modules
|
|
||||||
|
|
||||||
[entry_points]
|
|
||||||
wsme.protocols =
|
|
||||||
rest = wsme.rest.protocol:RestProtocol
|
|
||||||
restjson = wsme.rest.protocol:RestProtocol
|
|
||||||
restxml = wsme.rest.protocol:RestProtocol
|
|
||||||
soap = wsmeext.soap:SoapProtocol
|
|
||||||
extdirect = wsmeext.extdirect:ExtDirectProtocol
|
|
||||||
|
|
||||||
[files]
|
|
||||||
packages =
|
|
||||||
wsme
|
|
||||||
wsmeext
|
|
||||||
|
|
||||||
namespace_packages =
|
|
||||||
wsmeext
|
|
||||||
|
|
||||||
extra_files =
|
|
||||||
setup.py
|
|
||||||
README.rst
|
|
||||||
tests
|
|
||||||
|
|
||||||
[wheel]
|
|
||||||
# WSME has different requirements depending on the version of Python
|
|
||||||
# being used, so we cannot build universal wheels.
|
|
||||||
universal = 0
|
|
||||||
6
setup.py
6
setup.py
@@ -1,6 +0,0 @@
|
|||||||
import setuptools
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
setup_requires=['pbr'],
|
|
||||||
pbr=True
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[nosetests]
|
|
||||||
match=^test
|
|
||||||
where=test
|
|
||||||
nocapture=1
|
|
||||||
cover-package=test
|
|
||||||
cover-erase=1
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
try:
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
except ImportError:
|
|
||||||
from ez_setup import use_setuptools
|
|
||||||
use_setuptools()
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name = 'test',
|
|
||||||
version = '0.1',
|
|
||||||
description = '',
|
|
||||||
author = '',
|
|
||||||
author_email = '',
|
|
||||||
install_requires = [
|
|
||||||
"pecan",
|
|
||||||
],
|
|
||||||
test_suite = 'test',
|
|
||||||
zip_safe = False,
|
|
||||||
include_package_data = True,
|
|
||||||
packages = find_packages(exclude=['ez_setup'])
|
|
||||||
)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from pecan import make_app
|
|
||||||
from test import model
|
|
||||||
|
|
||||||
def setup_app(config):
|
|
||||||
|
|
||||||
model.init_model()
|
|
||||||
|
|
||||||
return make_app(
|
|
||||||
config.app.root,
|
|
||||||
static_root = config.app.static_root,
|
|
||||||
template_path = config.app.template_path,
|
|
||||||
logging = getattr(config, 'logging', {}),
|
|
||||||
debug = getattr(config.app, 'debug', False),
|
|
||||||
force_canonical = getattr(config.app, 'force_canonical', True)
|
|
||||||
)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from pecan import expose
|
|
||||||
from webob.exc import status_map
|
|
||||||
from .ws import AuthorsController
|
|
||||||
from wsmeext.pecan import wsexpose
|
|
||||||
|
|
||||||
|
|
||||||
class RootController(object):
|
|
||||||
authors = AuthorsController()
|
|
||||||
|
|
||||||
@expose('error.html')
|
|
||||||
def error(self, status):
|
|
||||||
try:
|
|
||||||
status = int(status)
|
|
||||||
except ValueError: # pragma: no cover
|
|
||||||
status = 500
|
|
||||||
message = getattr(status_map.get(status), 'explanation', '')
|
|
||||||
return dict(status=status, message=message)
|
|
||||||
|
|
||||||
@wsexpose()
|
|
||||||
def divide_by_zero(self):
|
|
||||||
1 / 0
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
# encoding=utf8
|
|
||||||
from pecan.rest import RestController
|
|
||||||
|
|
||||||
from wsme.types import Base, text, wsattr
|
|
||||||
|
|
||||||
import wsme
|
|
||||||
import wsmeext.pecan
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
class Author(Base):
|
|
||||||
id = int
|
|
||||||
firstname = text
|
|
||||||
books = wsattr(['Book'])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate(author):
|
|
||||||
if author.firstname == 'Robert':
|
|
||||||
raise wsme.exc.ClientSideError("I don't like this author!")
|
|
||||||
return author
|
|
||||||
|
|
||||||
|
|
||||||
class Book(Base):
|
|
||||||
id = int
|
|
||||||
name = text
|
|
||||||
author = wsattr('Author')
|
|
||||||
|
|
||||||
|
|
||||||
class BookNotFound(Exception):
|
|
||||||
message = "Book with ID={id} Not Found"
|
|
||||||
code = 404
|
|
||||||
|
|
||||||
def __init__(self, id):
|
|
||||||
message = self.message.format(id=id)
|
|
||||||
super(BookNotFound, self).__init__(message)
|
|
||||||
|
|
||||||
|
|
||||||
class NonHttpException(Exception):
|
|
||||||
message = "Internal Exception for Book ID={id}"
|
|
||||||
code = 684
|
|
||||||
|
|
||||||
def __init__(self, id):
|
|
||||||
message = self.message.format(id=id)
|
|
||||||
super(NonHttpException, self).__init__(message)
|
|
||||||
|
|
||||||
|
|
||||||
class BooksController(RestController):
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose(Book, int, int)
|
|
||||||
def get(self, author_id, id):
|
|
||||||
book = Book(
|
|
||||||
name=u"Les Confessions d’un révolutionnaire pour servir à "
|
|
||||||
u"l’histoire de la révolution de février",
|
|
||||||
author=Author(lastname=u"Proudhon")
|
|
||||||
)
|
|
||||||
return book
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose(Book, int, int, body=Book)
|
|
||||||
def put(self, author_id, id, book=None):
|
|
||||||
book.id = id
|
|
||||||
book.author = Author(id=author_id)
|
|
||||||
return book
|
|
||||||
|
|
||||||
|
|
||||||
class Criterion(Base):
|
|
||||||
op = text
|
|
||||||
attrname = text
|
|
||||||
value = text
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorsController(RestController):
|
|
||||||
|
|
||||||
_custom_actions = {
|
|
||||||
'json_only': ['GET'],
|
|
||||||
'xml_only': ['GET']
|
|
||||||
}
|
|
||||||
|
|
||||||
books = BooksController()
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose([Author], [six.text_type], [Criterion])
|
|
||||||
def get_all(self, q=None, r=None):
|
|
||||||
if q:
|
|
||||||
return [
|
|
||||||
Author(id=i, firstname=value)
|
|
||||||
for i, value in enumerate(q)
|
|
||||||
]
|
|
||||||
if r:
|
|
||||||
return [
|
|
||||||
Author(id=i, firstname=c.value)
|
|
||||||
for i, c in enumerate(r)
|
|
||||||
]
|
|
||||||
return [
|
|
||||||
Author(id=1, firstname=u'FirstName')
|
|
||||||
]
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose(Author, int)
|
|
||||||
def get(self, id):
|
|
||||||
if id == 999:
|
|
||||||
raise wsme.exc.ClientSideError('Wrong ID')
|
|
||||||
|
|
||||||
if id == 998:
|
|
||||||
raise BookNotFound(id)
|
|
||||||
|
|
||||||
if id == 997:
|
|
||||||
raise NonHttpException(id)
|
|
||||||
|
|
||||||
if id == 996:
|
|
||||||
raise wsme.exc.ClientSideError('Disabled ID', status_code=403)
|
|
||||||
|
|
||||||
if id == 911:
|
|
||||||
return wsme.api.Response(Author(),
|
|
||||||
status_code=401)
|
|
||||||
if id == 912:
|
|
||||||
return wsme.api.Response(None, status_code=204)
|
|
||||||
|
|
||||||
if id == 913:
|
|
||||||
return wsme.api.Response('foo', status_code=200, return_type=text)
|
|
||||||
|
|
||||||
author = Author()
|
|
||||||
author.id = id
|
|
||||||
author.firstname = u"aname"
|
|
||||||
author.books = [
|
|
||||||
Book(
|
|
||||||
name=u"Les Confessions d’un révolutionnaire pour servir à "
|
|
||||||
u"l’histoire de la révolution de février",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
return author
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose(Author, body=Author, status_code=201)
|
|
||||||
def post(self, author):
|
|
||||||
author.id = 10
|
|
||||||
return author
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose(None, int)
|
|
||||||
def delete(self, author_id):
|
|
||||||
print("Deleting", author_id)
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose(Book, int, body=Author)
|
|
||||||
def put(self, author_id, author=None):
|
|
||||||
return author
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose([Author], rest_content_types=('json',))
|
|
||||||
def json_only(self):
|
|
||||||
return [Author(id=1, firstname=u"aname", books=[])]
|
|
||||||
|
|
||||||
@wsmeext.pecan.wsexpose([Author], rest_content_types=('xml',))
|
|
||||||
def xml_only(self):
|
|
||||||
return [Author(id=1, firstname=u"aname", books=[])]
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
def init_model():
|
|
||||||
pass
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import os
|
|
||||||
from unittest import TestCase
|
|
||||||
from pecan import set_config
|
|
||||||
from pecan import testing
|
|
||||||
|
|
||||||
__all__ = ['FunctionalTest']
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionalTest(TestCase):
|
|
||||||
"""
|
|
||||||
Used for functional tests where you need to test your
|
|
||||||
literal application and its integration with the framework.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.app = testing.load_test_app(os.path.join(
|
|
||||||
os.path.dirname(__file__),
|
|
||||||
'config.py'
|
|
||||||
))
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
set_config({}, overwrite=True)
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Server Specific Configurations
|
|
||||||
server = {
|
|
||||||
'port' : '8080',
|
|
||||||
'host' : '0.0.0.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pecan Application Configurations
|
|
||||||
app = {
|
|
||||||
'root' : 'test.controllers.root.RootController',
|
|
||||||
'modules' : ['test'],
|
|
||||||
'static_root' : '%(confdir)s/../../public',
|
|
||||||
'template_path' : '%(confdir)s/../templates',
|
|
||||||
'errors' : {
|
|
||||||
'404' : '/error/404',
|
|
||||||
'__force_dict__' : True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Custom Configurations must be in Python dictionary format::
|
|
||||||
#
|
|
||||||
# foo = {'bar':'baz'}
|
|
||||||
#
|
|
||||||
# All configurations are accessible at::
|
|
||||||
# pecan.conf
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
from six.moves import http_client
|
|
||||||
from test.tests import FunctionalTest
|
|
||||||
import json
|
|
||||||
import pecan
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
used_status_codes = [400, 401, 403, 404, 500]
|
|
||||||
http_response_messages = {}
|
|
||||||
for code in used_status_codes:
|
|
||||||
http_response_messages[code] = '%s %s' % (code, http_client.responses[code])
|
|
||||||
|
|
||||||
class TestWS(FunctionalTest):
|
|
||||||
|
|
||||||
def test_get_all(self):
|
|
||||||
self.app.get('/authors')
|
|
||||||
|
|
||||||
def test_optional_array_param(self):
|
|
||||||
r = self.app.get('/authors?q=a&q=b')
|
|
||||||
l = json.loads(r.body.decode('utf-8'))
|
|
||||||
assert len(l) == 2
|
|
||||||
assert l[0]['firstname'] == 'a'
|
|
||||||
assert l[1]['firstname'] == 'b'
|
|
||||||
|
|
||||||
def test_optional_indexed_array_param(self):
|
|
||||||
r = self.app.get('/authors?q[0]=a&q[1]=b')
|
|
||||||
l = json.loads(r.body.decode('utf-8'))
|
|
||||||
assert len(l) == 2
|
|
||||||
assert l[0]['firstname'] == 'a'
|
|
||||||
assert l[1]['firstname'] == 'b'
|
|
||||||
|
|
||||||
def test_options_object_array_param(self):
|
|
||||||
r = self.app.get('/authors?r.value=a&r.value=b')
|
|
||||||
l = json.loads(r.body.decode('utf-8'))
|
|
||||||
assert len(l) == 2
|
|
||||||
assert l[0]['firstname'] == 'a'
|
|
||||||
assert l[1]['firstname'] == 'b'
|
|
||||||
|
|
||||||
def test_options_indexed_object_array_param(self):
|
|
||||||
r = self.app.get('/authors?r[0].value=a&r[1].value=b')
|
|
||||||
l = json.loads(r.body.decode('utf-8'))
|
|
||||||
assert len(l) == 2
|
|
||||||
assert l[0]['firstname'] == 'a'
|
|
||||||
assert l[1]['firstname'] == 'b'
|
|
||||||
|
|
||||||
def test_get_author(self):
|
|
||||||
a = self.app.get(
|
|
||||||
'/authors/1.json',
|
|
||||||
)
|
|
||||||
a = json.loads(a.body.decode('utf-8'))
|
|
||||||
|
|
||||||
assert a['id'] == 1
|
|
||||||
assert a['firstname'] == 'aname'
|
|
||||||
|
|
||||||
a = self.app.get(
|
|
||||||
'/authors/1.xml',
|
|
||||||
)
|
|
||||||
body = a.body.decode('utf-8')
|
|
||||||
assert '<id>1</id>' in body
|
|
||||||
assert '<firstname>aname</firstname>' in body
|
|
||||||
|
|
||||||
def test_post_body_parameter_validation(self):
|
|
||||||
res = self.app.post(
|
|
||||||
'/authors', '{"firstname": "Robert"}',
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
self.assertEqual(a['faultcode'], 'Client')
|
|
||||||
self.assertEqual(a['faultstring'], "I don't like this author!")
|
|
||||||
|
|
||||||
def test_post_body_parameter(self):
|
|
||||||
res = self.app.post(
|
|
||||||
'/authors', '{"firstname": "test"}',
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
assert res.status_int == 201
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert a['id'] == 10
|
|
||||||
assert a['firstname'] == 'test'
|
|
||||||
|
|
||||||
def test_put_parameter_validate(self):
|
|
||||||
res = self.app.put(
|
|
||||||
'/authors/foobar', '{"firstname": "test"}',
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
self.assertEqual(
|
|
||||||
a['faultstring'],
|
|
||||||
"Invalid input for field/attribute author_id. "
|
|
||||||
"Value: 'foobar'. unable to convert to int. Error: invalid "
|
|
||||||
"literal for int() with base 10: 'foobar'")
|
|
||||||
|
|
||||||
def test_clientsideerror(self):
|
|
||||||
expected_status_code = 400
|
|
||||||
expected_status = http_response_messages[expected_status_code]
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/999.json',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert a['faultcode'] == 'Client'
|
|
||||||
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/999.xml',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
assert '<faultcode>Client</faultcode>' in res.body.decode('utf-8')
|
|
||||||
|
|
||||||
def test_custom_clientside_error(self):
|
|
||||||
expected_status_code = 404
|
|
||||||
expected_status = http_response_messages[expected_status_code]
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/998.json',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert a['faultcode'] == 'Client'
|
|
||||||
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/998.xml',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
assert '<faultcode>Client</faultcode>' in res.body.decode('utf-8')
|
|
||||||
|
|
||||||
def test_custom_non_http_clientside_error(self):
|
|
||||||
expected_status_code = 500
|
|
||||||
expected_status = http_response_messages[expected_status_code]
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/997.json',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert a['faultcode'] == 'Server'
|
|
||||||
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/997.xml',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
assert '<faultcode>Server</faultcode>' in res.body.decode('utf-8')
|
|
||||||
|
|
||||||
def test_clientsideerror_status_code(self):
|
|
||||||
expected_status_code = 403
|
|
||||||
expected_status = http_response_messages[expected_status_code]
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/996.json',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert a['faultcode'] == 'Client'
|
|
||||||
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/996.xml',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
assert '<faultcode>Client</faultcode>' in res.body.decode('utf-8')
|
|
||||||
|
|
||||||
def test_non_default_response(self):
|
|
||||||
expected_status_code = 401
|
|
||||||
expected_status = http_response_messages[expected_status_code]
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/911.json',
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_int, expected_status_code)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
|
|
||||||
def test_non_default_response_return_type(self):
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/913',
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_int, 200)
|
|
||||||
self.assertEqual(res.body, b'"foo"')
|
|
||||||
self.assertEqual(res.content_length, 5)
|
|
||||||
|
|
||||||
def test_non_default_response_return_type_no_content(self):
|
|
||||||
res = self.app.get(
|
|
||||||
'/authors/912',
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_int, 204)
|
|
||||||
self.assertEqual(res.body, b'')
|
|
||||||
self.assertEqual(res.content_length, 0)
|
|
||||||
|
|
||||||
def test_serversideerror(self):
|
|
||||||
expected_status_code = 500
|
|
||||||
expected_status = http_response_messages[expected_status_code]
|
|
||||||
res = self.app.get('/divide_by_zero.json', expect_errors=True)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert a['faultcode'] == 'Server'
|
|
||||||
assert a['debuginfo'] is None
|
|
||||||
|
|
||||||
def test_serversideerror_with_debug(self):
|
|
||||||
expected_status_code = 500
|
|
||||||
expected_status = http_response_messages[expected_status_code]
|
|
||||||
pecan.set_config({'wsme': {'debug': True}})
|
|
||||||
res = self.app.get('/divide_by_zero.json', expect_errors=True)
|
|
||||||
self.assertEqual(res.status, expected_status)
|
|
||||||
a = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert a['faultcode'] == 'Server'
|
|
||||||
assert a['debuginfo'].startswith('Traceback (most recent call last):')
|
|
||||||
|
|
||||||
def test_json_only(self):
|
|
||||||
res = self.app.get('/authors/json_only.json')
|
|
||||||
assert res.status_int == 200
|
|
||||||
body = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert len(body) == 1
|
|
||||||
assert body[0]['firstname'] == u"aname"
|
|
||||||
assert body[0]['books'] == []
|
|
||||||
assert body[0]['id'] == 1
|
|
||||||
res = self.app.get('/authors/json_only.xml', expect_errors=True)
|
|
||||||
|
|
||||||
def test_xml_only(self):
|
|
||||||
res = self.app.get('/authors/xml_only.xml')
|
|
||||||
assert res.status_int == 200
|
|
||||||
assert '<id>1</id>' in res.body.decode('utf-8')
|
|
||||||
assert '<firstname>aname</firstname>' in res.body.decode('utf-8')
|
|
||||||
assert '<books />' in res.body.decode('utf-8')
|
|
||||||
res = self.app.get('/authors/xml_only.json', expect_errors=True)
|
|
||||||
|
|
||||||
def test_body_parameter(self):
|
|
||||||
res = self.app.put(
|
|
||||||
'/authors/1/books/2.json',
|
|
||||||
'{"name": "Alice au pays des merveilles"}',
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
book = json.loads(res.body.decode('utf-8'))
|
|
||||||
assert book['id'] == 2
|
|
||||||
assert book['author']['id'] == 1
|
|
||||||
|
|
||||||
def test_no_content_type_if_no_return_type(self):
|
|
||||||
if six.PY3:
|
|
||||||
self.skipTest(
|
|
||||||
"This test does not work in Python 3 until https://review.openstack.org/#/c/48439/ is merged")
|
|
||||||
res = self.app.delete('/authors/4')
|
|
||||||
assert "Content-Type" not in res.headers, res.headers['Content-Type']
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import mock
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from wsme import exc
|
|
||||||
from wsme.rest import args
|
|
||||||
from wsme.rest import json
|
|
||||||
|
|
||||||
|
|
||||||
class TestArgs(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_args_from_body(self):
|
|
||||||
|
|
||||||
funcdef = mock.MagicMock()
|
|
||||||
body = mock.MagicMock()
|
|
||||||
mimetype = "application/json"
|
|
||||||
funcdef.ignore_extra_args = True
|
|
||||||
json.parse = mock.MagicMock()
|
|
||||||
json.parse.side_effect = (exc.UnknownArgument(""))
|
|
||||||
resp = args.args_from_body(funcdef, body, mimetype)
|
|
||||||
self.assertEqual(resp, ((), {}))
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Web Services Made Easy documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Sun Oct 2 20:27:45 2011.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
import sys, os
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
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', 'wsmeext.sphinxext']
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
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'wsmeext.sphinxext Test'
|
|
||||||
copyright = u'2011, Christophe de Vienne'
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
import pkg_resources
|
|
||||||
dist = pkg_resources.require('WSME')[0]
|
|
||||||
|
|
||||||
# The short X.Y version.
|
|
||||||
version = '.'.join(dist.version[:2])
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = dist.version
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#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.
|
|
||||||
exclude_patterns = ['_build']
|
|
||||||
|
|
||||||
# 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 = []
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
html_theme = 'agogo'
|
|
||||||
html_theme_options = {
|
|
||||||
"pagewidth": "60em",
|
|
||||||
"documentwidth": "40em",
|
|
||||||
}
|
|
||||||
|
|
||||||
html_style = 'wsme.css'
|
|
||||||
|
|
||||||
# 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. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
html_title = "WSME %s" % release
|
|
||||||
|
|
||||||
# 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 (within the static path) to use as 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']
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
#html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# 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 <link> 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
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'WebServicesMadeEasydoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
|
||||||
|
|
||||||
# The paper size ('letter' or 'a4').
|
|
||||||
#latex_paper_size = 'letter'
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#latex_font_size = '10pt'
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'WebServicesMadeEasy.tex', u'Web Services Made Easy Documentation',
|
|
||||||
u'Christophe de Vienne', '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
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
#latex_preamble = ''
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#latex_appendices = []
|
|
||||||
|
|
||||||
# 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 = [
|
|
||||||
('index', 'webservicesmadeeasy', u'Web Services Made Easy Documentation',
|
|
||||||
[u'Christophe de Vienne'], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
autodoc_member_order = 'bysource'
|
|
||||||
|
|
||||||
wsme_protocols = [
|
|
||||||
'restjson', 'restxml'
|
|
||||||
]
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
API Documentation test
|
|
||||||
======================
|
|
||||||
|
|
||||||
Example
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. wsme:root:: wsmeext.sphinxext.SampleService
|
|
||||||
:webpath: /api
|
|
||||||
|
|
||||||
.. wsme:type:: MyType
|
|
||||||
|
|
||||||
.. wsme:attribute:: test
|
|
||||||
|
|
||||||
:type: int
|
|
||||||
|
|
||||||
.. wsme:service:: name/space/SampleService
|
|
||||||
|
|
||||||
.. wsme:function:: getType
|
|
||||||
|
|
||||||
Returns a :wsme:type:`MyType <MyType>`
|
|
||||||
|
|
||||||
|
|
||||||
.. default-domain:: wsme
|
|
||||||
|
|
||||||
.. type:: int
|
|
||||||
|
|
||||||
An integer
|
|
||||||
|
|
||||||
.. autotype:: wsmeext.sphinxext.SampleType
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoservice:: wsmeext.sphinxext.SampleService
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
.. autotype:: test_sphinxext.ASampleType
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autotype:: wsme.types.bytes
|
|
||||||
|
|
||||||
.. autotype:: wsme.types.text
|
|
||||||
|
|
||||||
.. _Sphinx: http://sphinx.pocoo.org/
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.. toctree::
|
|
||||||
|
|
||||||
document
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import json
|
|
||||||
|
|
||||||
import webtest
|
|
||||||
|
|
||||||
from cornice import Service
|
|
||||||
from cornice import resource
|
|
||||||
from pyramid.config import Configurator
|
|
||||||
from pyramid.httpexceptions import HTTPUnauthorized
|
|
||||||
|
|
||||||
from wsme.types import text, Base, HostRequest
|
|
||||||
from wsmeext.cornice import signature
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
id = int
|
|
||||||
name = text
|
|
||||||
|
|
||||||
users = Service(name='users', path='/users')
|
|
||||||
|
|
||||||
|
|
||||||
@users.get()
|
|
||||||
@signature([User])
|
|
||||||
def users_get():
|
|
||||||
return [User(id=1, name='first')]
|
|
||||||
|
|
||||||
|
|
||||||
@users.post()
|
|
||||||
@signature(User, body=User)
|
|
||||||
def users_create(data):
|
|
||||||
data.id = 2
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
secret = Service(name='secrets', path='/secret')
|
|
||||||
|
|
||||||
|
|
||||||
@secret.get()
|
|
||||||
@signature()
|
|
||||||
def secret_get():
|
|
||||||
raise HTTPUnauthorized()
|
|
||||||
|
|
||||||
|
|
||||||
divide = Service(name='divide', path='/divide')
|
|
||||||
|
|
||||||
|
|
||||||
@divide.get()
|
|
||||||
@signature(int, int, int)
|
|
||||||
def do_divide(a, b):
|
|
||||||
return a / b
|
|
||||||
|
|
||||||
needrequest = Service(name='needrequest', path='/needrequest')
|
|
||||||
|
|
||||||
|
|
||||||
@needrequest.get()
|
|
||||||
@signature(bool, HostRequest)
|
|
||||||
def needrequest_get(request):
|
|
||||||
assert request.path == '/needrequest', request.path
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Author(Base):
|
|
||||||
authorId = int
|
|
||||||
name = text
|
|
||||||
|
|
||||||
|
|
||||||
@resource.resource(collection_path='/author', path='/author/{authorId}')
|
|
||||||
class AuthorResource(object):
|
|
||||||
def __init__(self, request):
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
@signature(Author, int)
|
|
||||||
def get(self, authorId):
|
|
||||||
return Author(authorId=authorId, name="Author %s" % authorId)
|
|
||||||
|
|
||||||
@signature(Author, int, body=Author)
|
|
||||||
def post(self, authorId, data):
|
|
||||||
data.authorId = authorId
|
|
||||||
return data
|
|
||||||
|
|
||||||
@signature([Author], text)
|
|
||||||
def collection_get(self, where=None):
|
|
||||||
return [
|
|
||||||
Author(authorId=1, name="Author 1"),
|
|
||||||
Author(authorId=2, name="Author 2"),
|
|
||||||
Author(authorId=3, name="Author 3")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def make_app():
|
|
||||||
config = Configurator()
|
|
||||||
config.include("cornice")
|
|
||||||
config.include("wsmeext.cornice")
|
|
||||||
config.scan("test_cornice")
|
|
||||||
return config.make_wsgi_app()
|
|
||||||
|
|
||||||
|
|
||||||
class WSMECorniceTestCase(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.app = webtest.TestApp(make_app())
|
|
||||||
|
|
||||||
def test_get_json_list(self):
|
|
||||||
resp = self.app.get('/users')
|
|
||||||
self.assertEqual(
|
|
||||||
resp.body,
|
|
||||||
b'[{"id": 1, "name": "first"}]'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_xml_list(self):
|
|
||||||
resp = self.app.get('/users', headers={"Accept": "text/xml"})
|
|
||||||
self.assertEqual(
|
|
||||||
resp.body,
|
|
||||||
b'<result><item><id>1</id><name>first</name></item></result>'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_post_json_data(self):
|
|
||||||
data = json.dumps({"name": "new"})
|
|
||||||
resp = self.app.post(
|
|
||||||
'/users', data,
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
resp.body,
|
|
||||||
b'{"id": 2, "name": "new"}'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_post_xml_data(self):
|
|
||||||
data = '<data><name>new</name></data>'
|
|
||||||
resp = self.app.post(
|
|
||||||
'/users', data,
|
|
||||||
headers={"Content-Type": "text/xml"}
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
resp.body,
|
|
||||||
b'<result><id>2</id><name>new</name></result>'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pass_request(self):
|
|
||||||
resp = self.app.get('/needrequest')
|
|
||||||
assert resp.json is True
|
|
||||||
|
|
||||||
def test_resource_collection_get(self):
|
|
||||||
resp = self.app.get('/author')
|
|
||||||
assert len(resp.json) == 3
|
|
||||||
assert resp.json[0]['name'] == 'Author 1'
|
|
||||||
assert resp.json[1]['name'] == 'Author 2'
|
|
||||||
assert resp.json[2]['name'] == 'Author 3'
|
|
||||||
|
|
||||||
def test_resource_get(self):
|
|
||||||
resp = self.app.get('/author/5')
|
|
||||||
assert resp.json['name'] == 'Author 5'
|
|
||||||
|
|
||||||
def test_resource_post(self):
|
|
||||||
resp = self.app.post(
|
|
||||||
'/author/5',
|
|
||||||
json.dumps({"name": "Author 5"}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
assert resp.json['authorId'] == 5
|
|
||||||
assert resp.json['name'] == 'Author 5'
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
resp = self.app.get('/divide?a=1&b=0', expect_errors=True)
|
|
||||||
self.assertEqual(resp.json['faultcode'], 'Server')
|
|
||||||
self.assertEqual(resp.status_code, 500)
|
|
||||||
|
|
||||||
def test_client_error(self):
|
|
||||||
resp = self.app.get(
|
|
||||||
'/divide?a=1&c=0',
|
|
||||||
headers={'Accept': 'application/json'},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.json['faultcode'], 'Client')
|
|
||||||
self.assertEqual(resp.status_code, 400)
|
|
||||||
|
|
||||||
def test_runtime_error(self):
|
|
||||||
resp = self.app.get(
|
|
||||||
'/secret',
|
|
||||||
headers={'Accept': 'application/json'},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.json['faultcode'], 'Client')
|
|
||||||
self.assertEqual(resp.status_code, 401)
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
# encoding=utf8
|
|
||||||
import unittest
|
|
||||||
from flask import Flask, json, abort
|
|
||||||
from flask.ext import restful
|
|
||||||
|
|
||||||
from wsmeext.flask import signature
|
|
||||||
from wsme.api import Response
|
|
||||||
from wsme.types import Base, text
|
|
||||||
|
|
||||||
|
|
||||||
class Model(Base):
|
|
||||||
id = int
|
|
||||||
name = text
|
|
||||||
|
|
||||||
|
|
||||||
class Criterion(Base):
|
|
||||||
op = text
|
|
||||||
attr = text
|
|
||||||
value = text
|
|
||||||
|
|
||||||
test_app = Flask(__name__)
|
|
||||||
api = restful.Api(test_app)
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/multiply')
|
|
||||||
@signature(int, int, int)
|
|
||||||
def multiply(a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/divide_by_zero')
|
|
||||||
@signature(None)
|
|
||||||
def divide_by_zero():
|
|
||||||
return 1 / 0
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/models')
|
|
||||||
@signature([Model], [Criterion])
|
|
||||||
def list_models(q=None):
|
|
||||||
if q:
|
|
||||||
name = q[0].value
|
|
||||||
else:
|
|
||||||
name = 'first'
|
|
||||||
return [Model(name=name)]
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/models/<name>')
|
|
||||||
@signature(Model, text)
|
|
||||||
def get_model(name):
|
|
||||||
return Model(name=name)
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/models/<name>/secret')
|
|
||||||
@signature(Model, text)
|
|
||||||
def model_secret(name):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/models/<name>/custom-error')
|
|
||||||
@signature(Model, text)
|
|
||||||
def model_custom_error(name):
|
|
||||||
class CustomError(Exception):
|
|
||||||
code = 412
|
|
||||||
raise CustomError("FOO!")
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/models', methods=['POST'])
|
|
||||||
@signature(Model, body=Model)
|
|
||||||
def post_model(body):
|
|
||||||
return Model(name=body.name)
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/status_sig')
|
|
||||||
@signature(int, status_code=201)
|
|
||||||
def get_status_sig():
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
@test_app.route('/status_response')
|
|
||||||
@signature(int)
|
|
||||||
def get_status_response():
|
|
||||||
return Response(1, status_code=201)
|
|
||||||
|
|
||||||
|
|
||||||
class RestFullApi(restful.Resource):
|
|
||||||
@signature(Model)
|
|
||||||
def get(self):
|
|
||||||
return Model(id=1, name=u"Gérard")
|
|
||||||
|
|
||||||
@signature(int, body=Model)
|
|
||||||
def post(self, model):
|
|
||||||
return model.id
|
|
||||||
|
|
||||||
api.add_resource(RestFullApi, '/restful')
|
|
||||||
|
|
||||||
|
|
||||||
class FlaskrTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
test_app.config['TESTING'] = True
|
|
||||||
self.app = test_app.test_client()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_multiply(self):
|
|
||||||
r = self.app.get('/multiply?a=2&b=5')
|
|
||||||
assert r.data == b'10', r.data
|
|
||||||
|
|
||||||
def test_get_model(self):
|
|
||||||
resp = self.app.get('/models/test')
|
|
||||||
assert resp.status_code == 200
|
|
||||||
|
|
||||||
def test_list_models(self):
|
|
||||||
resp = self.app.get('/models')
|
|
||||||
assert resp.status_code == 200
|
|
||||||
|
|
||||||
def test_array_parameter(self):
|
|
||||||
resp = self.app.get('/models?q.op=%3D&q.attr=name&q.value=second')
|
|
||||||
assert resp.status_code == 200
|
|
||||||
self.assertEqual(
|
|
||||||
resp.data, b'[{"name": "second"}]'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_post_model(self):
|
|
||||||
resp = self.app.post('/models', data={"body.name": "test"})
|
|
||||||
assert resp.status_code == 200
|
|
||||||
resp = self.app.post(
|
|
||||||
'/models',
|
|
||||||
data=json.dumps({"name": "test"}),
|
|
||||||
content_type="application/json"
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200
|
|
||||||
|
|
||||||
def test_get_status_sig(self):
|
|
||||||
resp = self.app.get('/status_sig')
|
|
||||||
assert resp.status_code == 201
|
|
||||||
|
|
||||||
def test_get_status_response(self):
|
|
||||||
resp = self.app.get('/status_response')
|
|
||||||
assert resp.status_code == 201
|
|
||||||
|
|
||||||
def test_custom_clientside_error(self):
|
|
||||||
r = self.app.get(
|
|
||||||
'/models/test/secret',
|
|
||||||
headers={'Accept': 'application/json'}
|
|
||||||
)
|
|
||||||
assert r.status_code == 403, r.status_code
|
|
||||||
assert json.loads(r.data)['faultstring'] == '403: Forbidden'
|
|
||||||
|
|
||||||
r = self.app.get(
|
|
||||||
'/models/test/secret',
|
|
||||||
headers={'Accept': 'application/xml'}
|
|
||||||
)
|
|
||||||
assert r.status_code == 403, r.status_code
|
|
||||||
assert r.data == (b'<error><faultcode>Client</faultcode>'
|
|
||||||
b'<faultstring>403: Forbidden</faultstring>'
|
|
||||||
b'<debuginfo /></error>')
|
|
||||||
|
|
||||||
def test_custom_non_http_clientside_error(self):
|
|
||||||
r = self.app.get(
|
|
||||||
'/models/test/custom-error',
|
|
||||||
headers={'Accept': 'application/json'}
|
|
||||||
)
|
|
||||||
assert r.status_code == 412, r.status_code
|
|
||||||
assert json.loads(r.data)['faultstring'] == 'FOO!'
|
|
||||||
|
|
||||||
r = self.app.get(
|
|
||||||
'/models/test/custom-error',
|
|
||||||
headers={'Accept': 'application/xml'}
|
|
||||||
)
|
|
||||||
assert r.status_code == 412, r.status_code
|
|
||||||
assert r.data == (b'<error><faultcode>Client</faultcode>'
|
|
||||||
b'<faultstring>FOO!</faultstring>'
|
|
||||||
b'<debuginfo /></error>')
|
|
||||||
|
|
||||||
def test_serversideerror(self):
|
|
||||||
r = self.app.get('/divide_by_zero')
|
|
||||||
assert r.status_code == 500
|
|
||||||
data = json.loads(r.data)
|
|
||||||
self.assertEqual(data['debuginfo'], None)
|
|
||||||
self.assertEqual(data['faultcode'], 'Server')
|
|
||||||
self.assertIn('by zero', data['faultstring'])
|
|
||||||
|
|
||||||
def test_restful_get(self):
|
|
||||||
r = self.app.get('/restful', headers={'Accept': 'application/json'})
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
data = json.loads(r.data)
|
|
||||||
|
|
||||||
self.assertEqual(data['id'], 1)
|
|
||||||
self.assertEqual(data['name'], u"Gérard")
|
|
||||||
|
|
||||||
def test_restful_post(self):
|
|
||||||
r = self.app.post(
|
|
||||||
'/restful',
|
|
||||||
data=json.dumps({'id': 5, 'name': u'Huguette'}),
|
|
||||||
headers={
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'})
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
data = json.loads(r.data)
|
|
||||||
|
|
||||||
self.assertEqual(data, 5)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_app.run()
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import sphinx
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
import wsme.types
|
|
||||||
from wsmeext import sphinxext
|
|
||||||
|
|
||||||
docpath = os.path.join(
|
|
||||||
os.path.dirname(__file__),
|
|
||||||
'sphinxexample')
|
|
||||||
|
|
||||||
|
|
||||||
class ASampleType(object):
|
|
||||||
somebytes = wsme.types.bytes
|
|
||||||
sometext = wsme.types.text
|
|
||||||
someint = int
|
|
||||||
|
|
||||||
|
|
||||||
class TestSphinxExt(unittest.TestCase):
|
|
||||||
def test_buildhtml(self):
|
|
||||||
if not os.path.exists('.test_sphinxext/'):
|
|
||||||
os.makedirs('.test_sphinxext/')
|
|
||||||
try:
|
|
||||||
sphinx.main([
|
|
||||||
'',
|
|
||||||
'-b', 'html',
|
|
||||||
'-d', '.test_sphinxext/doctree',
|
|
||||||
docpath,
|
|
||||||
'.test_sphinxext/html'
|
|
||||||
])
|
|
||||||
assert Exception("Should raise SystemExit 0")
|
|
||||||
except SystemExit as e:
|
|
||||||
assert e.code == 0
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataTypeName(unittest.TestCase):
|
|
||||||
def test_user_type(self):
|
|
||||||
self.assertEqual(sphinxext.datatypename(ASampleType),
|
|
||||||
'ASampleType')
|
|
||||||
|
|
||||||
def test_dict_type(self):
|
|
||||||
d = wsme.types.DictType(str, str)
|
|
||||||
self.assertEqual(sphinxext.datatypename(d), 'dict(str: str)')
|
|
||||||
d = wsme.types.DictType(str, ASampleType)
|
|
||||||
self.assertEqual(sphinxext.datatypename(d), 'dict(str: ASampleType)')
|
|
||||||
|
|
||||||
def test_array_type(self):
|
|
||||||
d = wsme.types.ArrayType(str)
|
|
||||||
self.assertEqual(sphinxext.datatypename(d), 'list(str)')
|
|
||||||
d = wsme.types.ArrayType(ASampleType)
|
|
||||||
self.assertEqual(sphinxext.datatypename(d), 'list(ASampleType)')
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
import wsmeext.tg11
|
|
||||||
from wsme import WSRoot
|
|
||||||
from wsmeext.tg11 import wsexpose, wsvalidate
|
|
||||||
import wsmeext.tg1
|
|
||||||
|
|
||||||
from turbogears.controllers import RootController
|
|
||||||
import cherrypy
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import simplejson
|
|
||||||
|
|
||||||
|
|
||||||
from wsmeext.tests import test_soap
|
|
||||||
|
|
||||||
|
|
||||||
class WSController(WSRoot):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Subcontroller(object):
|
|
||||||
@wsexpose(int, int, int)
|
|
||||||
def add(self, a, b):
|
|
||||||
return a + b
|
|
||||||
|
|
||||||
|
|
||||||
class Root(RootController):
|
|
||||||
class UselessSubClass:
|
|
||||||
# This class is here only to make sure wsmeext.tg1.scan_api
|
|
||||||
# does its job properly
|
|
||||||
pass
|
|
||||||
|
|
||||||
ws = WSController(webpath='/ws')
|
|
||||||
ws.addprotocol(
|
|
||||||
'soap',
|
|
||||||
tns=test_soap.tns,
|
|
||||||
typenamespace=test_soap.typenamespace,
|
|
||||||
baseURL='/ws/'
|
|
||||||
)
|
|
||||||
ws = wsmeext.tg11.adapt(ws)
|
|
||||||
|
|
||||||
@wsexpose(int)
|
|
||||||
@wsvalidate(int, int)
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
@wsexpose(int)
|
|
||||||
@wsvalidate(int, int)
|
|
||||||
def divide(self, a, b):
|
|
||||||
if b == 0:
|
|
||||||
raise cherrypy.HTTPError(400, 'Cannot divide by zero!')
|
|
||||||
return a / b
|
|
||||||
|
|
||||||
sub = Subcontroller()
|
|
||||||
|
|
||||||
from turbogears import testutil, config, startup
|
|
||||||
|
|
||||||
|
|
||||||
class TestController(unittest.TestCase):
|
|
||||||
root = Root
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"Tests the output of the index method"
|
|
||||||
self.app = testutil.make_app(self.root)
|
|
||||||
testutil.start_server()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
# implementation copied from turbogears.testutil.stop_server.
|
|
||||||
# The only change is that cherrypy.root is set to None
|
|
||||||
# AFTER stopTurbogears has been called so that wsmeext.tg11
|
|
||||||
# can correctly uninstall its filter.
|
|
||||||
if config.get("cp_started"):
|
|
||||||
cherrypy.server.stop()
|
|
||||||
config.update({"cp_started": False})
|
|
||||||
|
|
||||||
if config.get("server_started"):
|
|
||||||
startup.stopTurboGears()
|
|
||||||
config.update({"server_started": False})
|
|
||||||
|
|
||||||
def test_restcall(self):
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert simplejson.loads(response.body) == 50
|
|
||||||
|
|
||||||
response = self.app.post("/sub/add",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert simplejson.loads(response.body) == 15
|
|
||||||
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'application/json'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert simplejson.loads(response.body) == 50
|
|
||||||
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'text/javascript'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert simplejson.loads(response.body) == 50
|
|
||||||
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json',
|
|
||||||
'Accept': 'text/xml'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert response.body == "<result>50</result>"
|
|
||||||
|
|
||||||
def test_custom_clientside_error(self):
|
|
||||||
response = self.app.post(
|
|
||||||
"/divide",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 0}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'application/json'},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
assert response.status_int == 400
|
|
||||||
assert simplejson.loads(response.body) == {
|
|
||||||
"debuginfo": None,
|
|
||||||
"faultcode": "Server",
|
|
||||||
"faultstring": "(400, 'Cannot divide by zero!')"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = self.app.post(
|
|
||||||
"/divide",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 0}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'text/xml'},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
assert response.status_int == 400
|
|
||||||
assert response.body == ("<error><faultcode>Server</faultcode>"
|
|
||||||
"<faultstring>(400, 'Cannot divide by zero!')"
|
|
||||||
"</faultstring><debuginfo /></error>")
|
|
||||||
|
|
||||||
def test_soap_wsdl(self):
|
|
||||||
ts = test_soap.TestSOAP('test_wsdl')
|
|
||||||
ts.app = self.app
|
|
||||||
ts.ws_path = '/ws/'
|
|
||||||
ts.run()
|
|
||||||
#wsdl = self.app.get('/ws/api.wsdl').body
|
|
||||||
#print wsdl
|
|
||||||
#assert 'multiply' in wsdl
|
|
||||||
|
|
||||||
def test_soap_call(self):
|
|
||||||
ts = test_soap.TestSOAP('test_wsdl')
|
|
||||||
ts.app = self.app
|
|
||||||
ts.ws_path = '/ws/'
|
|
||||||
|
|
||||||
print ts.ws_path
|
|
||||||
assert ts.call('multiply', a=5, b=10, _rt=int) == 50
|
|
||||||
|
|
||||||
def test_scan_api_loops(self):
|
|
||||||
class MyRoot(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
MyRoot.loop = MyRoot()
|
|
||||||
|
|
||||||
root = MyRoot()
|
|
||||||
|
|
||||||
api = list(wsmeext.tg1._scan_api(root))
|
|
||||||
print(api)
|
|
||||||
|
|
||||||
self.assertEqual(len(api), 0)
|
|
||||||
|
|
||||||
def test_scan_api_maxlen(self):
|
|
||||||
class ARoot(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def make_subcontrollers(n):
|
|
||||||
c = type('Controller%s' % n, (object,), {})
|
|
||||||
return c
|
|
||||||
|
|
||||||
c = ARoot
|
|
||||||
for n in xrange(55):
|
|
||||||
subc = make_subcontrollers(n)
|
|
||||||
c.sub = subc()
|
|
||||||
c = subc
|
|
||||||
root = ARoot()
|
|
||||||
self.assertRaises(ValueError, list, wsmeext.tg1._scan_api(root))
|
|
||||||
|
|
||||||
def test_templates_content_type(self):
|
|
||||||
self.assertEqual(
|
|
||||||
"application/json",
|
|
||||||
wsmeext.tg1.AutoJSONTemplate().get_content_type('dummy')
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
"text/xml",
|
|
||||||
wsmeext.tg1.AutoXMLTemplate().get_content_type('dummy')
|
|
||||||
)
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import wsmeext.tg15
|
|
||||||
from wsme import WSRoot
|
|
||||||
|
|
||||||
from turbogears.controllers import RootController
|
|
||||||
import cherrypy
|
|
||||||
|
|
||||||
from wsmeext.tests import test_soap
|
|
||||||
|
|
||||||
import simplejson
|
|
||||||
|
|
||||||
|
|
||||||
class Subcontroller(object):
|
|
||||||
@wsmeext.tg15.wsexpose(int, int, int)
|
|
||||||
def add(self, a, b):
|
|
||||||
return a + b
|
|
||||||
|
|
||||||
|
|
||||||
class Root(RootController):
|
|
||||||
class UselessSubClass:
|
|
||||||
# This class is here only to make sure wsmeext.tg1.scan_api
|
|
||||||
# does its job properly
|
|
||||||
pass
|
|
||||||
|
|
||||||
sub = Subcontroller()
|
|
||||||
|
|
||||||
ws = WSRoot(webpath='/ws')
|
|
||||||
ws.addprotocol('soap',
|
|
||||||
tns=test_soap.tns,
|
|
||||||
typenamespace=test_soap.typenamespace,
|
|
||||||
baseURL='/ws/'
|
|
||||||
)
|
|
||||||
ws = wsmeext.tg15.adapt(ws)
|
|
||||||
|
|
||||||
@wsmeext.tg15.wsexpose(int)
|
|
||||||
@wsmeext.tg15.wsvalidate(int, int)
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
@wsmeext.tg15.wsexpose(int)
|
|
||||||
@wsmeext.tg15.wsvalidate(int, int)
|
|
||||||
def divide(self, a, b):
|
|
||||||
if b == 0:
|
|
||||||
raise cherrypy.HTTPError(400, 'Cannot divide by zero!')
|
|
||||||
return a / b
|
|
||||||
|
|
||||||
|
|
||||||
from turbogears import testutil
|
|
||||||
|
|
||||||
|
|
||||||
class TestController(testutil.TGTest):
|
|
||||||
root = Root
|
|
||||||
|
|
||||||
# def setUp(self):
|
|
||||||
# "Tests the output of the index method"
|
|
||||||
# self.app = testutil.make_app(self.root)
|
|
||||||
# #print cherrypy.root
|
|
||||||
# testutil.start_server()
|
|
||||||
|
|
||||||
# def tearDown(self):
|
|
||||||
# # implementation copied from turbogears.testutil.stop_server.
|
|
||||||
# # The only change is that cherrypy.root is set to None
|
|
||||||
# # AFTER stopTurbogears has been called so that wsmeext.tg15
|
|
||||||
# # can correctly uninstall its filter.
|
|
||||||
# if config.get("cp_started"):
|
|
||||||
# cherrypy.server.stop()
|
|
||||||
# config.update({"cp_started": False})
|
|
||||||
#
|
|
||||||
# if config.get("server_started"):
|
|
||||||
# startup.stopTurboGears()
|
|
||||||
# config.update({"server_started": False})
|
|
||||||
|
|
||||||
def test_restcall(self):
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert simplejson.loads(response.body) == 50
|
|
||||||
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'application/json'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert simplejson.loads(response.body) == 50
|
|
||||||
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'text/javascript'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert simplejson.loads(response.body) == 50
|
|
||||||
|
|
||||||
response = self.app.post("/multiply",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 10}),
|
|
||||||
{'Content-Type': 'application/json',
|
|
||||||
'Accept': 'text/xml'}
|
|
||||||
)
|
|
||||||
print response
|
|
||||||
assert response.body == "<result>50</result>"
|
|
||||||
|
|
||||||
def test_custom_clientside_error(self):
|
|
||||||
response = self.app.post(
|
|
||||||
"/divide",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 0}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'application/json'},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
assert response.status_int == 400
|
|
||||||
assert simplejson.loads(response.body) == {
|
|
||||||
"debuginfo": None,
|
|
||||||
"faultcode": "Client",
|
|
||||||
"faultstring": "(400, 'Cannot divide by zero!')"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = self.app.post(
|
|
||||||
"/divide",
|
|
||||||
simplejson.dumps({'a': 5, 'b': 0}),
|
|
||||||
{'Content-Type': 'application/json', 'Accept': 'text/xml'},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
assert response.status_int == 400
|
|
||||||
assert response.body == ("<error><faultcode>Client</faultcode>"
|
|
||||||
"<faultstring>(400, 'Cannot divide by zero!')"
|
|
||||||
"</faultstring><debuginfo /></error>")
|
|
||||||
|
|
||||||
def test_soap_wsdl(self):
|
|
||||||
wsdl = self.app.get('/ws/api.wsdl').body
|
|
||||||
print wsdl
|
|
||||||
assert 'multiply' in wsdl
|
|
||||||
|
|
||||||
def test_soap_call(self):
|
|
||||||
ts = test_soap.TestSOAP('test_wsdl')
|
|
||||||
ts.app = self.app
|
|
||||||
ts.ws_path = '/ws/'
|
|
||||||
|
|
||||||
print ts.ws_path
|
|
||||||
assert ts.call('multiply', a=5, b=10, _rt=int) == 50
|
|
||||||
|
|
||||||
def test_scan_api_loops(self):
|
|
||||||
class MyRoot(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
MyRoot.loop = MyRoot()
|
|
||||||
|
|
||||||
root = MyRoot()
|
|
||||||
|
|
||||||
api = list(wsmeext.tg1._scan_api(root))
|
|
||||||
print(api)
|
|
||||||
|
|
||||||
self.assertEqual(len(api), 0)
|
|
||||||
|
|
||||||
def test_scan_api_maxlen(self):
|
|
||||||
class ARoot(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def make_subcontrollers(n):
|
|
||||||
c = type('Controller%s' % n, (object,), {})
|
|
||||||
return c
|
|
||||||
|
|
||||||
c = ARoot
|
|
||||||
for n in xrange(55):
|
|
||||||
subc = make_subcontrollers(n)
|
|
||||||
c.sub = subc()
|
|
||||||
c = subc
|
|
||||||
root = ARoot()
|
|
||||||
self.assertRaises(ValueError, list, wsmeext.tg1._scan_api(root))
|
|
||||||
|
|
||||||
def test_templates_content_type(self):
|
|
||||||
self.assertEqual(
|
|
||||||
"application/json",
|
|
||||||
wsmeext.tg1.AutoJSONTemplate().get_content_type('dummy')
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
"text/xml",
|
|
||||||
wsmeext.tg1.AutoXMLTemplate().get_content_type('dummy')
|
|
||||||
)
|
|
||||||
143
tox-tmpl.ini
143
tox-tmpl.ini
@@ -1,143 +0,0 @@
|
|||||||
# content of: tox.ini , put in same dir as setup.py
|
|
||||||
[tox]
|
|
||||||
envlist = py27,py27-nolxml,pypy,tg11,tg15,cornice,cornice-py3,coverage,py35,py35-nolxml,pecan-dev27,pecan-dev35,pep8
|
|
||||||
|
|
||||||
[common]
|
|
||||||
testtools=
|
|
||||||
nose
|
|
||||||
coverage < 3.99
|
|
||||||
pbr
|
|
||||||
webtest
|
|
||||||
basedeps=
|
|
||||||
transaction
|
|
||||||
pecan
|
|
||||||
cloud_sptheme
|
|
||||||
Sphinx < 1.2.99
|
|
||||||
Flask
|
|
||||||
flask-restful
|
|
||||||
|
|
||||||
[axes]
|
|
||||||
python=py27,py35,pypy
|
|
||||||
sqlalchemy=sa5,sa6,sa7*
|
|
||||||
lxml=lxml*,nolxml
|
|
||||||
json=json*,simplejson
|
|
||||||
|
|
||||||
[axis:python]
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
|
|
||||||
commands=
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
|
|
||||||
[axis:python:py27]
|
|
||||||
basepython=python2.7
|
|
||||||
|
|
||||||
[axis:python:py35]
|
|
||||||
basepython=python3.5
|
|
||||||
|
|
||||||
[axis:sqlalchemy:sa5]
|
|
||||||
deps=
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
|
|
||||||
[axis:sqlalchemy:sa6]
|
|
||||||
deps=
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
|
|
||||||
[axis:sqlalchemy:sa7]
|
|
||||||
deps=
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
|
|
||||||
[axis:json:simplejson]
|
|
||||||
deps=
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[axis:lxml:lxml]
|
|
||||||
deps=
|
|
||||||
lxml
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
setenv=
|
|
||||||
COVERAGE_FILE=.coverage.{envname}
|
|
||||||
|
|
||||||
[testenv:cornice]
|
|
||||||
basepython=python2.7
|
|
||||||
usedevelop=True
|
|
||||||
deps=
|
|
||||||
pbr
|
|
||||||
nose
|
|
||||||
webtest
|
|
||||||
coverage < 3.99
|
|
||||||
cornice
|
|
||||||
commands=
|
|
||||||
{envbindir}/nosetests tests/test_cornice.py --with-xunit --xunit-file nosetests-{envname}.xml --verbose --with-coverage --cover-package wsmeext {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsmeext/cornice.py
|
|
||||||
|
|
||||||
[testenv:cornice-py3]
|
|
||||||
basepython = python3.5
|
|
||||||
usedevelop = {[testenv:cornice]usedevelop}
|
|
||||||
deps = {[testenv:cornice]deps}
|
|
||||||
# disable hash randomization
|
|
||||||
setenv =
|
|
||||||
PYTHONHASHSEED=0
|
|
||||||
commands =
|
|
||||||
{envbindir}/nosetests tests/test_cornice.py --with-xunit --xunit-file nosetests-{envname}.xml --verbose --with-coverage --cover-package wsmeext {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsmeext/cornice.py
|
|
||||||
|
|
||||||
[testenv:pecan-dev-base]
|
|
||||||
deps=
|
|
||||||
{[common]testtools}
|
|
||||||
transaction
|
|
||||||
suds-jurko
|
|
||||||
https://github.com/stackforge/pecan/zipball/master
|
|
||||||
|
|
||||||
[testenv:pecan-dev27]
|
|
||||||
basepython=python2.7
|
|
||||||
deps={[testenv:pecan-dev-base]deps}
|
|
||||||
commands=
|
|
||||||
{envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs}
|
|
||||||
|
|
||||||
[testenv:pecan-dev35]
|
|
||||||
basepython=python3.5
|
|
||||||
deps={[testenv:pecan-dev-base]deps}
|
|
||||||
commands=
|
|
||||||
{envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs}
|
|
||||||
|
|
||||||
[testenv:coverage]
|
|
||||||
basepython=python
|
|
||||||
deps=
|
|
||||||
coverage < 3.99
|
|
||||||
setenv=
|
|
||||||
COVERAGE_FILE=.coverage
|
|
||||||
commands=
|
|
||||||
{envbindir}/coverage erase
|
|
||||||
{envbindir}/coverage combine
|
|
||||||
{envbindir}/coverage xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/protocols/*.py wsmeext/*.py
|
|
||||||
|
|
||||||
[testenv:doc]
|
|
||||||
deps=
|
|
||||||
cloud_sptheme
|
|
||||||
Sphinx < 1.2.99
|
|
||||||
|
|
||||||
changedir=
|
|
||||||
doc
|
|
||||||
|
|
||||||
commands=
|
|
||||||
make clean ziphtml
|
|
||||||
|
|
||||||
[testenv:pep8]
|
|
||||||
deps = flake8
|
|
||||||
commands = flake8 wsme wsmeext setup.py
|
|
||||||
|
|
||||||
# Generic environment for running commands like packaging
|
|
||||||
[testenv:venv]
|
|
||||||
commands = {posargs}
|
|
||||||
usedevelop=True
|
|
||||||
deps =
|
|
||||||
pbr
|
|
||||||
oslo.config
|
|
||||||
oslotest
|
|
||||||
932
tox.ini
932
tox.ini
@@ -1,932 +0,0 @@
|
|||||||
[tox]
|
|
||||||
envlist = py27,py27-nolxml,pypy,tg11,tg15,cornice,cornice-py3,coverage,py35,py35-nolxml,pecan-dev27,pecan-dev35,pep8
|
|
||||||
|
|
||||||
[common]
|
|
||||||
testtools =
|
|
||||||
nose
|
|
||||||
coverage < 3.99
|
|
||||||
pbr
|
|
||||||
webtest
|
|
||||||
basedeps =
|
|
||||||
transaction
|
|
||||||
pecan
|
|
||||||
cloud_sptheme
|
|
||||||
Sphinx < 1.2.99
|
|
||||||
Flask
|
|
||||||
flask-restful
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
setenv =
|
|
||||||
COVERAGE_FILE=.coverage.{envname}
|
|
||||||
|
|
||||||
[testenv:cornice]
|
|
||||||
basepython = python2.7
|
|
||||||
usedevelop = True
|
|
||||||
deps =
|
|
||||||
pbr
|
|
||||||
nose
|
|
||||||
webtest
|
|
||||||
coverage < 3.99
|
|
||||||
cornice
|
|
||||||
commands =
|
|
||||||
{envbindir}/nosetests tests/test_cornice.py --with-xunit --xunit-file nosetests-{envname}.xml --verbose --with-coverage --cover-package wsmeext {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsmeext/cornice.py
|
|
||||||
|
|
||||||
[testenv:cornice-py3]
|
|
||||||
basepython = python3.5
|
|
||||||
usedevelop = {[testenv:cornice]usedevelop}
|
|
||||||
deps = {[testenv:cornice]deps}
|
|
||||||
setenv =
|
|
||||||
PYTHONHASHSEED=0
|
|
||||||
commands =
|
|
||||||
{envbindir}/nosetests tests/test_cornice.py --with-xunit --xunit-file nosetests-{envname}.xml --verbose --with-coverage --cover-package wsmeext {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsmeext/cornice.py
|
|
||||||
|
|
||||||
[testenv:pecan-dev-base]
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
transaction
|
|
||||||
suds-jurko
|
|
||||||
https://github.com/stackforge/pecan/zipball/master
|
|
||||||
|
|
||||||
[testenv:pecan-dev27]
|
|
||||||
basepython = python2.7
|
|
||||||
deps = {[testenv:pecan-dev-base]deps}
|
|
||||||
commands =
|
|
||||||
{envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs}
|
|
||||||
|
|
||||||
[testenv:pecan-dev35]
|
|
||||||
basepython = python3.5
|
|
||||||
deps = {[testenv:pecan-dev-base]deps}
|
|
||||||
commands =
|
|
||||||
{envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs}
|
|
||||||
|
|
||||||
[testenv:coverage]
|
|
||||||
basepython = python
|
|
||||||
deps =
|
|
||||||
coverage < 3.99
|
|
||||||
setenv =
|
|
||||||
COVERAGE_FILE=.coverage
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage erase
|
|
||||||
{envbindir}/coverage combine
|
|
||||||
{envbindir}/coverage xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/protocols/*.py wsmeext/*.py
|
|
||||||
|
|
||||||
[testenv:doc]
|
|
||||||
deps =
|
|
||||||
cloud_sptheme
|
|
||||||
Sphinx < 1.2.99
|
|
||||||
changedir =
|
|
||||||
doc
|
|
||||||
commands =
|
|
||||||
make clean ziphtml
|
|
||||||
|
|
||||||
[testenv:pep8]
|
|
||||||
deps = flake8
|
|
||||||
commands = flake8 wsme wsmeext setup.py
|
|
||||||
|
|
||||||
[testenv:venv]
|
|
||||||
commands = {posargs}
|
|
||||||
usedevelop = True
|
|
||||||
deps =
|
|
||||||
pbr
|
|
||||||
oslo.config
|
|
||||||
oslotest
|
|
||||||
|
|
||||||
[testenv:py27-sa5-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa5]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa5-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa5-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa5-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa5-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa5-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa6-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa6]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa6-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa6-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa6-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa6-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa6-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa7-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa7-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa7-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-sa7-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py27-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
simplejson
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py35-sa5-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa5]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa5-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa5-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa5-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa5-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa5-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa6-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa6]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa6-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa6-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa6-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa6-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa6-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa7-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa7-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa7-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-sa7-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:py35-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
simplejson
|
|
||||||
basepython = python3.5
|
|
||||||
|
|
||||||
[testenv:pypy-sa5-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
|
|
||||||
[testenv:pypy-sa5]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
|
|
||||||
[testenv:pypy-sa5-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-sa5-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-sa5-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
|
|
||||||
[testenv:pypy-sa5-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
|
|
||||||
[testenv:pypy-sa5-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.5.99
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-sa6-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
|
|
||||||
[testenv:pypy-sa6]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
|
|
||||||
[testenv:pypy-sa6-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-sa6-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-sa6-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
|
|
||||||
[testenv:pypy-sa6-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
|
|
||||||
[testenv:pypy-sa6-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.6.99
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-sa7-lxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
|
|
||||||
[testenv:pypy]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
|
|
||||||
[testenv:pypy-sa7-lxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
lxml
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-sa7-nolxml-json]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
|
|
||||||
[testenv:pypy-nolxml]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
|
|
||||||
[testenv:pypy-sa7-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
[testenv:pypy-nolxml-simplejson]
|
|
||||||
commands =
|
|
||||||
{envbindir}/coverage run {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.py tests/test_flask.py --verbose {posargs}
|
|
||||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
{envbindir}/coverage report --show-missing wsme/*.py wsme/rest/*.py wsmeext/*.py
|
|
||||||
deps =
|
|
||||||
{[common]testtools}
|
|
||||||
{[common]basedeps}
|
|
||||||
suds-jurko
|
|
||||||
SQLAlchemy<=0.7.99
|
|
||||||
simplejson
|
|
||||||
|
|
||||||
144
toxgen.py
144
toxgen.py
@@ -1,144 +0,0 @@
|
|||||||
"""
|
|
||||||
Produce a tox.ini file from a template config file.
|
|
||||||
|
|
||||||
The template config file is a standard tox.ini file with additional sections.
|
|
||||||
Theses sections will be combined to create new testenv: sections if they do
|
|
||||||
not exists yet.
|
|
||||||
|
|
||||||
See REAME.rst for more detail.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import collections
|
|
||||||
import optparse
|
|
||||||
|
|
||||||
try:
|
|
||||||
from configparser import ConfigParser
|
|
||||||
except:
|
|
||||||
from ConfigParser import ConfigParser # noqa
|
|
||||||
|
|
||||||
|
|
||||||
parser = optparse.OptionParser(epilog=__doc__)
|
|
||||||
parser.add_option('-i', '--input', dest='input',
|
|
||||||
default='tox-tmpl.ini', metavar='FILE')
|
|
||||||
parser.add_option('-o', '--output', dest='output',
|
|
||||||
default='tox.ini', metavar='FILE')
|
|
||||||
|
|
||||||
|
|
||||||
class AxisItem(object):
|
|
||||||
def __init__(self, axis, name, config):
|
|
||||||
self.axis = axis
|
|
||||||
self.isdefault = name[-1] == '*'
|
|
||||||
self.name = name[:-1] if self.isdefault else name
|
|
||||||
self.load(config)
|
|
||||||
|
|
||||||
def load(self, config):
|
|
||||||
sectionname = 'axis:%s:%s' % (self.axis.name, self.name)
|
|
||||||
if config.has_section(sectionname):
|
|
||||||
self.options = collections.OrderedDict(config.items(sectionname))
|
|
||||||
else:
|
|
||||||
self.options = collections.OrderedDict()
|
|
||||||
|
|
||||||
for name, value in self.axis.defaults.items():
|
|
||||||
if name not in self.options:
|
|
||||||
self.options[name] = value
|
|
||||||
|
|
||||||
|
|
||||||
class Axis(object):
|
|
||||||
def __init__(self, name, config):
|
|
||||||
self.name = name
|
|
||||||
self.load(config)
|
|
||||||
|
|
||||||
def load(self, config):
|
|
||||||
self.items = collections.OrderedDict()
|
|
||||||
values = config.get('axes', self.name).split(',')
|
|
||||||
if config.has_section('axis:%s' % self.name):
|
|
||||||
self.defaults = collections.OrderedDict(
|
|
||||||
config.items('axis:%s' % self.name)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.defaults = {}
|
|
||||||
for value in values:
|
|
||||||
self.items[value.strip('*')] = AxisItem(self, value, config)
|
|
||||||
|
|
||||||
|
|
||||||
def render(incfg):
|
|
||||||
axes = collections.OrderedDict()
|
|
||||||
|
|
||||||
if incfg.has_section('axes'):
|
|
||||||
for axis in incfg.options('axes'):
|
|
||||||
axes[axis] = Axis(axis, incfg)
|
|
||||||
|
|
||||||
out = ConfigParser()
|
|
||||||
for section in incfg.sections():
|
|
||||||
if section == 'axes' or section.startswith('axis:'):
|
|
||||||
continue
|
|
||||||
out.add_section(section)
|
|
||||||
for name, value in incfg.items(section):
|
|
||||||
out.set(section, name, value)
|
|
||||||
|
|
||||||
for combination in itertools.product(
|
|
||||||
*[axis.items.keys() for axis in axes.values()]):
|
|
||||||
options = collections.OrderedDict()
|
|
||||||
|
|
||||||
section_name = (
|
|
||||||
'testenv:' + '-'.join([item for item in combination if item])
|
|
||||||
)
|
|
||||||
section_alt_name = (
|
|
||||||
'testenv:' + '-'.join([
|
|
||||||
itemname
|
|
||||||
for axis, itemname in zip(axes.values(), combination)
|
|
||||||
if itemname and not axis.items[itemname].isdefault
|
|
||||||
])
|
|
||||||
)
|
|
||||||
if section_alt_name == section_name:
|
|
||||||
section_alt_name = None
|
|
||||||
|
|
||||||
axes_items = [
|
|
||||||
'%s:%s' % (axis, itemname)
|
|
||||||
for axis, itemname in zip(axes, combination)
|
|
||||||
]
|
|
||||||
|
|
||||||
for axis, itemname in zip(axes.values(), combination):
|
|
||||||
axis_options = axis.items[itemname].options
|
|
||||||
if 'constraints' in axis_options:
|
|
||||||
constraints = axis_options['constraints'].split('\n')
|
|
||||||
for c in constraints:
|
|
||||||
if c.startswith('!') and c[1:] in axes_items:
|
|
||||||
continue
|
|
||||||
for name, value in axis_options.items():
|
|
||||||
if name in options:
|
|
||||||
options[name] += value
|
|
||||||
else:
|
|
||||||
options[name] = value
|
|
||||||
|
|
||||||
constraints = options.pop('constraints', '').split('\n')
|
|
||||||
neg_constraints = [c[1:] for c in constraints if c and c[0] == '!']
|
|
||||||
if not set(neg_constraints).isdisjoint(axes_items):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not out.has_section(section_name):
|
|
||||||
out.add_section(section_name)
|
|
||||||
|
|
||||||
if (section_alt_name and not out.has_section(section_alt_name)):
|
|
||||||
out.add_section(section_alt_name)
|
|
||||||
|
|
||||||
for name, value in reversed(options.items()):
|
|
||||||
if not out.has_option(section_name, name):
|
|
||||||
out.set(section_name, name, value)
|
|
||||||
if section_alt_name and not out.has_option(section_alt_name, name):
|
|
||||||
out.set(section_alt_name, name, value)
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
options, args = parser.parse_args()
|
|
||||||
tmpl = ConfigParser()
|
|
||||||
tmpl.read(options.input)
|
|
||||||
with open(options.output, 'wb') as outfile:
|
|
||||||
render(tmpl).write(outfile)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from wsme.api import signature
|
|
||||||
from wsme.rest import expose, validate
|
|
||||||
from wsme.root import WSRoot
|
|
||||||
from wsme.types import wsattr, wsproperty, Unset
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'expose', 'validate', 'signature',
|
|
||||||
'WSRoot',
|
|
||||||
'wsattr', 'wsproperty', 'Unset'
|
|
||||||
]
|
|
||||||
237
wsme/api.py
237
wsme/api.py
@@ -1,237 +0,0 @@
|
|||||||
import traceback
|
|
||||||
import functools
|
|
||||||
import inspect
|
|
||||||
import logging
|
|
||||||
import six
|
|
||||||
|
|
||||||
import wsme.exc
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
from wsme import utils
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def iswsmefunction(f):
|
|
||||||
return hasattr(f, '_wsme_definition')
|
|
||||||
|
|
||||||
|
|
||||||
def wrapfunc(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
wrapper._wsme_original_func = f
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def getargspec(f):
|
|
||||||
f = getattr(f, '_wsme_original_func', f)
|
|
||||||
return inspect.getargspec(f)
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionArgument(object):
|
|
||||||
"""
|
|
||||||
An argument definition of an api entry
|
|
||||||
"""
|
|
||||||
def __init__(self, name, datatype, mandatory, default):
|
|
||||||
#: argument name
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
#: Data type
|
|
||||||
self.datatype = datatype
|
|
||||||
|
|
||||||
#: True if the argument is mandatory
|
|
||||||
self.mandatory = mandatory
|
|
||||||
|
|
||||||
#: Default value if argument is omitted
|
|
||||||
self.default = default
|
|
||||||
|
|
||||||
def resolve_type(self, registry):
|
|
||||||
self.datatype = registry.resolve_type(self.datatype)
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionDefinition(object):
|
|
||||||
"""
|
|
||||||
An api entry definition
|
|
||||||
"""
|
|
||||||
def __init__(self, func):
|
|
||||||
#: Function name
|
|
||||||
self.name = func.__name__
|
|
||||||
|
|
||||||
#: Function documentation
|
|
||||||
self.doc = func.__doc__
|
|
||||||
|
|
||||||
#: Return type
|
|
||||||
self.return_type = None
|
|
||||||
|
|
||||||
#: The function arguments (list of :class:`FunctionArgument`)
|
|
||||||
self.arguments = []
|
|
||||||
|
|
||||||
#: If the body carry the datas of a single argument, its type
|
|
||||||
self.body_type = None
|
|
||||||
|
|
||||||
#: Status code
|
|
||||||
self.status_code = 200
|
|
||||||
|
|
||||||
#: True if extra arguments should be ignored, NOT inserted in
|
|
||||||
#: the kwargs of the function and not raise UnknownArgument
|
|
||||||
#: exceptions
|
|
||||||
self.ignore_extra_args = False
|
|
||||||
|
|
||||||
#: name of the function argument to pass the host request object.
|
|
||||||
#: Should be set by using the :class:`wsme.types.HostRequest` type
|
|
||||||
#: in the function @\ :function:`signature`
|
|
||||||
self.pass_request = False
|
|
||||||
|
|
||||||
#: Dictionnary of protocol-specific options.
|
|
||||||
self.extra_options = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(func):
|
|
||||||
"""
|
|
||||||
Returns the :class:`FunctionDefinition` of a method.
|
|
||||||
"""
|
|
||||||
if not hasattr(func, '_wsme_definition'):
|
|
||||||
fd = FunctionDefinition(func)
|
|
||||||
func._wsme_definition = fd
|
|
||||||
|
|
||||||
return func._wsme_definition
|
|
||||||
|
|
||||||
def get_arg(self, name):
|
|
||||||
"""
|
|
||||||
Returns a :class:`FunctionArgument` from its name
|
|
||||||
"""
|
|
||||||
for arg in self.arguments:
|
|
||||||
if arg.name == name:
|
|
||||||
return arg
|
|
||||||
return None
|
|
||||||
|
|
||||||
def resolve_types(self, registry):
|
|
||||||
self.return_type = registry.resolve_type(self.return_type)
|
|
||||||
self.body_type = registry.resolve_type(self.body_type)
|
|
||||||
for arg in self.arguments:
|
|
||||||
arg.resolve_type(registry)
|
|
||||||
|
|
||||||
def set_options(self, body=None, ignore_extra_args=False, status_code=200,
|
|
||||||
rest_content_types=('json', 'xml'), **extra_options):
|
|
||||||
self.body_type = body
|
|
||||||
self.status_code = status_code
|
|
||||||
self.ignore_extra_args = ignore_extra_args
|
|
||||||
self.rest_content_types = rest_content_types
|
|
||||||
self.extra_options = extra_options
|
|
||||||
|
|
||||||
def set_arg_types(self, argspec, arg_types):
|
|
||||||
args, varargs, keywords, defaults = argspec
|
|
||||||
if args[0] == 'self':
|
|
||||||
args = args[1:]
|
|
||||||
arg_types = list(arg_types)
|
|
||||||
if self.body_type is not None:
|
|
||||||
arg_types.append(self.body_type)
|
|
||||||
for i, argname in enumerate(args):
|
|
||||||
datatype = arg_types[i]
|
|
||||||
mandatory = defaults is None or i < (len(args) - len(defaults))
|
|
||||||
default = None
|
|
||||||
if not mandatory:
|
|
||||||
default = defaults[i - (len(args) - len(defaults))]
|
|
||||||
if datatype is wsme.types.HostRequest:
|
|
||||||
self.pass_request = argname
|
|
||||||
else:
|
|
||||||
self.arguments.append(FunctionArgument(argname, datatype,
|
|
||||||
mandatory, default))
|
|
||||||
|
|
||||||
|
|
||||||
class signature(object):
|
|
||||||
|
|
||||||
"""Decorator that specify the argument types of an exposed function.
|
|
||||||
|
|
||||||
:param return_type: Type of the value returned by the function
|
|
||||||
:param argN: Type of the Nth argument
|
|
||||||
:param body: If the function takes a final argument that is supposed to be
|
|
||||||
the request body by itself, its type.
|
|
||||||
:param status_code: HTTP return status code of the function.
|
|
||||||
:param ignore_extra_args: Allow extra/unknow arguments (default to False)
|
|
||||||
|
|
||||||
Most of the time this decorator is not supposed to be used directly,
|
|
||||||
unless you are not using WSME on top of another framework.
|
|
||||||
|
|
||||||
If an adapter is used, it will provide either a specialised version of this
|
|
||||||
decororator, either a new decorator named @wsexpose that takes the same
|
|
||||||
parameters (it will in addition expose the function, hence its name).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *types, **options):
|
|
||||||
self.return_type = types[0] if types else None
|
|
||||||
self.arg_types = []
|
|
||||||
if len(types) > 1:
|
|
||||||
self.arg_types.extend(types[1:])
|
|
||||||
if 'body' in options:
|
|
||||||
self.arg_types.append(options['body'])
|
|
||||||
self.wrap = options.pop('wrap', False)
|
|
||||||
self.options = options
|
|
||||||
|
|
||||||
def __call__(self, func):
|
|
||||||
argspec = getargspec(func)
|
|
||||||
if self.wrap:
|
|
||||||
func = wrapfunc(func)
|
|
||||||
fd = FunctionDefinition.get(func)
|
|
||||||
if fd.extra_options is not None:
|
|
||||||
raise ValueError("This function is already exposed")
|
|
||||||
fd.return_type = self.return_type
|
|
||||||
fd.set_options(**self.options)
|
|
||||||
if self.arg_types:
|
|
||||||
fd.set_arg_types(argspec, self.arg_types)
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
sig = signature
|
|
||||||
|
|
||||||
|
|
||||||
class Response(object):
|
|
||||||
"""
|
|
||||||
Object to hold the "response" from a view function
|
|
||||||
"""
|
|
||||||
def __init__(self, obj, status_code=None, error=None,
|
|
||||||
return_type=wsme.types.Unset):
|
|
||||||
#: Store the result object from the view
|
|
||||||
self.obj = obj
|
|
||||||
|
|
||||||
#: Store an optional status_code
|
|
||||||
self.status_code = status_code
|
|
||||||
|
|
||||||
#: Return error details
|
|
||||||
#: Must be a dictionnary with the following keys: faultcode,
|
|
||||||
#: faultstring and an optional debuginfo
|
|
||||||
self.error = error
|
|
||||||
|
|
||||||
#: Return type
|
|
||||||
#: Type of the value returned by the function
|
|
||||||
#: If the return type is wsme.types.Unset it will be ignored
|
|
||||||
#: and the default return type will prevail.
|
|
||||||
self.return_type = return_type
|
|
||||||
|
|
||||||
|
|
||||||
def format_exception(excinfo, debug=False):
|
|
||||||
"""Extract informations that can be sent to the client."""
|
|
||||||
error = excinfo[1]
|
|
||||||
code = getattr(error, 'code', None)
|
|
||||||
if code and utils.is_valid_code(code) and utils.is_client_error(code):
|
|
||||||
faultstring = (error.faultstring if hasattr(error, 'faultstring')
|
|
||||||
else six.text_type(error))
|
|
||||||
r = dict(faultcode="Client",
|
|
||||||
faultstring=faultstring)
|
|
||||||
log.debug("Client-side error: %s" % r['faultstring'])
|
|
||||||
r['debuginfo'] = None
|
|
||||||
return r
|
|
||||||
else:
|
|
||||||
faultstring = six.text_type(error)
|
|
||||||
debuginfo = "\n".join(traceback.format_exception(*excinfo))
|
|
||||||
|
|
||||||
log.error('Server-side error: "%s". Detail: \n%s' % (
|
|
||||||
faultstring, debuginfo))
|
|
||||||
|
|
||||||
r = dict(faultcode="Server", faultstring=faultstring)
|
|
||||||
if debug:
|
|
||||||
r['debuginfo'] = debuginfo
|
|
||||||
else:
|
|
||||||
r['debuginfo'] = None
|
|
||||||
return r
|
|
||||||
92
wsme/exc.py
92
wsme/exc.py
@@ -1,92 +0,0 @@
|
|||||||
import six
|
|
||||||
|
|
||||||
from wsme.utils import _
|
|
||||||
|
|
||||||
|
|
||||||
class ClientSideError(RuntimeError):
|
|
||||||
def __init__(self, msg=None, status_code=400):
|
|
||||||
self.msg = msg
|
|
||||||
self.code = status_code
|
|
||||||
super(ClientSideError, self).__init__(self.faultstring)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faultstring(self):
|
|
||||||
if self.msg is None:
|
|
||||||
return str(self)
|
|
||||||
elif isinstance(self.msg, six.text_type):
|
|
||||||
return self.msg
|
|
||||||
else:
|
|
||||||
return six.u(self.msg)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidInput(ClientSideError):
|
|
||||||
def __init__(self, fieldname, value, msg=''):
|
|
||||||
self.fieldname = fieldname
|
|
||||||
self.value = value
|
|
||||||
super(InvalidInput, self).__init__(msg)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faultstring(self):
|
|
||||||
return _(six.u(
|
|
||||||
"Invalid input for field/attribute %s. Value: '%s'. %s")
|
|
||||||
) % (self.fieldname, self.value, self.msg)
|
|
||||||
|
|
||||||
|
|
||||||
class MissingArgument(ClientSideError):
|
|
||||||
def __init__(self, argname, msg=''):
|
|
||||||
self.argname = argname
|
|
||||||
super(MissingArgument, self).__init__(msg)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faultstring(self):
|
|
||||||
return _(six.u('Missing argument: "%s"%s')) % (
|
|
||||||
self.argname, self.msg and ": " + self.msg or "")
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownArgument(ClientSideError):
|
|
||||||
def __init__(self, argname, msg=''):
|
|
||||||
self.argname = argname
|
|
||||||
super(UnknownArgument, self).__init__(msg)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faultstring(self):
|
|
||||||
return _(six.u('Unknown argument: "%s"%s')) % (
|
|
||||||
self.argname, self.msg and ": " + self.msg or "")
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownFunction(ClientSideError):
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
super(UnknownFunction, self).__init__()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faultstring(self):
|
|
||||||
return _(six.u("Unknown function name: %s")) % (self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownAttribute(ClientSideError):
|
|
||||||
def __init__(self, fieldname, attributes, msg=''):
|
|
||||||
self.fieldname = fieldname
|
|
||||||
self.attributes = attributes
|
|
||||||
self.msg = msg
|
|
||||||
super(UnknownAttribute, self).__init__(self.msg)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faultstring(self):
|
|
||||||
error = _("Unknown attribute for argument %(argn)s: %(attrs)s")
|
|
||||||
if len(self.attributes) > 1:
|
|
||||||
error = _("Unknown attributes for argument %(argn)s: %(attrs)s")
|
|
||||||
str_attrs = ", ".join(self.attributes)
|
|
||||||
return error % {'argn': self.fieldname, 'attrs': str_attrs}
|
|
||||||
|
|
||||||
def add_fieldname(self, name):
|
|
||||||
"""Add a fieldname to concatenate the full name.
|
|
||||||
|
|
||||||
Add a fieldname so that the whole hierarchy is displayed. Successive
|
|
||||||
calls to this method will prepend ``name`` to the hierarchy of names.
|
|
||||||
"""
|
|
||||||
if self.fieldname is not None:
|
|
||||||
self.fieldname = "{}.{}".format(name, self.fieldname)
|
|
||||||
else:
|
|
||||||
self.fieldname = name
|
|
||||||
super(UnknownAttribute, self).__init__(self.msg)
|
|
||||||
147
wsme/protocol.py
147
wsme/protocol.py
@@ -1,147 +0,0 @@
|
|||||||
import weakref
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
|
|
||||||
from wsme.exc import ClientSideError
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'CallContext',
|
|
||||||
|
|
||||||
'register_protocol', 'getprotocol',
|
|
||||||
]
|
|
||||||
|
|
||||||
registered_protocols = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _cfg(f):
|
|
||||||
cfg = getattr(f, '_cfg', None)
|
|
||||||
if cfg is None:
|
|
||||||
f._cfg = cfg = {}
|
|
||||||
return cfg
|
|
||||||
|
|
||||||
|
|
||||||
class expose(object):
|
|
||||||
def __init__(self, path, content_type):
|
|
||||||
self.path = path
|
|
||||||
self.content_type = content_type
|
|
||||||
|
|
||||||
def __call__(self, func):
|
|
||||||
func.exposed = True
|
|
||||||
cfg = _cfg(func)
|
|
||||||
cfg['content-type'] = self.content_type
|
|
||||||
cfg.setdefault('paths', []).append(self.path)
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
class CallContext(object):
|
|
||||||
def __init__(self, request):
|
|
||||||
self._request = weakref.ref(request)
|
|
||||||
self.path = None
|
|
||||||
|
|
||||||
self.func = None
|
|
||||||
self.funcdef = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def request(self):
|
|
||||||
return self._request()
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectDict(object):
|
|
||||||
def __init__(self, obj):
|
|
||||||
self.obj = obj
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
return getattr(self.obj, name)
|
|
||||||
|
|
||||||
|
|
||||||
class Protocol(object):
|
|
||||||
name = None
|
|
||||||
displayname = None
|
|
||||||
content_types = []
|
|
||||||
|
|
||||||
def resolve_path(self, path):
|
|
||||||
if '$' in path:
|
|
||||||
from string import Template
|
|
||||||
s = Template(path)
|
|
||||||
path = s.substitute(ObjectDict(self))
|
|
||||||
return path
|
|
||||||
|
|
||||||
def iter_routes(self):
|
|
||||||
for attrname in dir(self):
|
|
||||||
attr = getattr(self, attrname)
|
|
||||||
if getattr(attr, 'exposed', False):
|
|
||||||
for path in _cfg(attr)['paths']:
|
|
||||||
yield self.resolve_path(path), attr
|
|
||||||
|
|
||||||
def accept(self, request):
|
|
||||||
return request.headers.get('Content-Type') in self.content_types
|
|
||||||
|
|
||||||
def iter_calls(self, request):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def extract_path(self, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def read_arguments(self, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def encode_result(self, context, result):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def encode_sample_value(self, datatype, value, format=False):
|
|
||||||
return ('none', 'N/A')
|
|
||||||
|
|
||||||
def encode_sample_params(self, params, format=False):
|
|
||||||
return ('none', 'N/A')
|
|
||||||
|
|
||||||
def encode_sample_result(self, datatype, value, format=False):
|
|
||||||
return ('none', 'N/A')
|
|
||||||
|
|
||||||
|
|
||||||
def register_protocol(protocol):
|
|
||||||
registered_protocols[protocol.name] = protocol
|
|
||||||
|
|
||||||
|
|
||||||
def getprotocol(name, **options):
|
|
||||||
protocol_class = registered_protocols.get(name)
|
|
||||||
if protocol_class is None:
|
|
||||||
for entry_point in pkg_resources.iter_entry_points(
|
|
||||||
'wsme.protocols', name):
|
|
||||||
if entry_point.name == name:
|
|
||||||
protocol_class = entry_point.load()
|
|
||||||
if protocol_class is None:
|
|
||||||
raise ValueError("Cannot find protocol '%s'" % name)
|
|
||||||
registered_protocols[name] = protocol_class
|
|
||||||
return protocol_class(**options)
|
|
||||||
|
|
||||||
|
|
||||||
def media_type_accept(request, content_types):
|
|
||||||
"""Validate media types against request.method.
|
|
||||||
|
|
||||||
When request.method is GET or HEAD compare with the Accept header.
|
|
||||||
When request.method is POST, PUT or PATCH compare with the Content-Type
|
|
||||||
header.
|
|
||||||
When request.method is DELETE media type is irrelevant, so return True.
|
|
||||||
"""
|
|
||||||
if request.method in ['GET', 'HEAD']:
|
|
||||||
if request.accept:
|
|
||||||
if request.accept.best_match(content_types):
|
|
||||||
return True
|
|
||||||
error_message = ('Unacceptable Accept type: %s not in %s'
|
|
||||||
% (request.accept, content_types))
|
|
||||||
raise ClientSideError(error_message, status_code=406)
|
|
||||||
elif request.method in ['PUT', 'POST', 'PATCH']:
|
|
||||||
content_type = request.headers.get('Content-Type')
|
|
||||||
if content_type:
|
|
||||||
for ct in content_types:
|
|
||||||
if request.headers.get('Content-Type', '').startswith(ct):
|
|
||||||
return True
|
|
||||||
error_message = ('Unacceptable Content-Type: %s not in %s'
|
|
||||||
% (content_type, content_types))
|
|
||||||
raise ClientSideError(error_message, status_code=415)
|
|
||||||
else:
|
|
||||||
raise ClientSideError('missing Content-Type header')
|
|
||||||
elif request.method in ['DELETE']:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import wsme.api
|
|
||||||
|
|
||||||
APIPATH_MAXLEN = 20
|
|
||||||
|
|
||||||
|
|
||||||
class expose(object):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.signature = wsme.api.signature(*args, **kwargs)
|
|
||||||
|
|
||||||
def __call__(self, func):
|
|
||||||
return self.signature(func)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def with_method(cls, method, *args, **kwargs):
|
|
||||||
kwargs['method'] = method
|
|
||||||
return cls(*args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, *args, **kwargs):
|
|
||||||
return cls.with_method('GET', *args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def post(cls, *args, **kwargs):
|
|
||||||
return cls.with_method('POST', *args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def put(cls, *args, **kwargs):
|
|
||||||
return cls.with_method('PUT', *args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def delete(cls, *args, **kwargs):
|
|
||||||
return cls.with_method('DELETE', *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class validate(object):
|
|
||||||
"""
|
|
||||||
Decorator that define the arguments types of a function.
|
|
||||||
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
class MyController(object):
|
|
||||||
@expose(str)
|
|
||||||
@validate(datetime.date, datetime.time)
|
|
||||||
def format(self, d, t):
|
|
||||||
return d.isoformat() + ' ' + t.isoformat()
|
|
||||||
"""
|
|
||||||
def __init__(self, *param_types):
|
|
||||||
self.param_types = param_types
|
|
||||||
|
|
||||||
def __call__(self, func):
|
|
||||||
argspec = wsme.api.getargspec(func)
|
|
||||||
fd = wsme.api.FunctionDefinition.get(func)
|
|
||||||
fd.set_arg_types(argspec, self.param_types)
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
def scan_api(controller, path=[], objects=[]):
|
|
||||||
"""
|
|
||||||
Recursively iterate a controller api entries.
|
|
||||||
"""
|
|
||||||
for name in dir(controller):
|
|
||||||
if name.startswith('_'):
|
|
||||||
continue
|
|
||||||
a = getattr(controller, name)
|
|
||||||
if a in objects:
|
|
||||||
continue
|
|
||||||
if inspect.ismethod(a):
|
|
||||||
if wsme.api.iswsmefunction(a):
|
|
||||||
yield path + [name], a, []
|
|
||||||
elif inspect.isclass(a):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if len(path) > APIPATH_MAXLEN:
|
|
||||||
raise ValueError("Path is too long: " + str(path))
|
|
||||||
for i in scan_api(a, path + [name], objects + [a]):
|
|
||||||
yield i
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import cgi
|
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
|
|
||||||
from simplegeneric import generic
|
|
||||||
|
|
||||||
from wsme.exc import ClientSideError, UnknownArgument, InvalidInput
|
|
||||||
|
|
||||||
from wsme.types import iscomplex, list_attributes, Unset
|
|
||||||
from wsme.types import UserType, ArrayType, DictType, File
|
|
||||||
from wsme.utils import parse_isodate, parse_isotime, parse_isodatetime
|
|
||||||
import wsme.runtime
|
|
||||||
|
|
||||||
from six import moves
|
|
||||||
|
|
||||||
ARRAY_MAX_SIZE = 1000
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def from_param(datatype, value):
|
|
||||||
return datatype(value) if value is not None else None
|
|
||||||
|
|
||||||
|
|
||||||
@from_param.when_object(datetime.date)
|
|
||||||
def date_from_param(datatype, value):
|
|
||||||
return parse_isodate(value) if value else None
|
|
||||||
|
|
||||||
|
|
||||||
@from_param.when_object(datetime.time)
|
|
||||||
def time_from_param(datatype, value):
|
|
||||||
return parse_isotime(value) if value else None
|
|
||||||
|
|
||||||
|
|
||||||
@from_param.when_object(datetime.datetime)
|
|
||||||
def datetime_from_param(datatype, value):
|
|
||||||
return parse_isodatetime(value) if value else None
|
|
||||||
|
|
||||||
|
|
||||||
@from_param.when_object(File)
|
|
||||||
def filetype_from_param(datatype, value):
|
|
||||||
if isinstance(value, cgi.FieldStorage):
|
|
||||||
return File(fieldstorage=value)
|
|
||||||
return File(content=value)
|
|
||||||
|
|
||||||
|
|
||||||
@from_param.when_type(UserType)
|
|
||||||
def usertype_from_param(datatype, value):
|
|
||||||
return datatype.frombasetype(
|
|
||||||
from_param(datatype.basetype, value))
|
|
||||||
|
|
||||||
|
|
||||||
@from_param.when_type(ArrayType)
|
|
||||||
def array_from_param(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return [
|
|
||||||
from_param(datatype.item_type, item)
|
|
||||||
for item in value
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def from_params(datatype, params, path, hit_paths):
|
|
||||||
if iscomplex(datatype) and datatype is not File:
|
|
||||||
objfound = False
|
|
||||||
for key in params:
|
|
||||||
if key.startswith(path + '.'):
|
|
||||||
objfound = True
|
|
||||||
break
|
|
||||||
if objfound:
|
|
||||||
r = datatype()
|
|
||||||
for attrdef in list_attributes(datatype):
|
|
||||||
value = from_params(
|
|
||||||
attrdef.datatype,
|
|
||||||
params, '%s.%s' % (path, attrdef.key), hit_paths
|
|
||||||
)
|
|
||||||
if value is not Unset:
|
|
||||||
setattr(r, attrdef.key, value)
|
|
||||||
return r
|
|
||||||
else:
|
|
||||||
if path in params:
|
|
||||||
hit_paths.add(path)
|
|
||||||
return from_param(datatype, params[path])
|
|
||||||
return Unset
|
|
||||||
|
|
||||||
|
|
||||||
@from_params.when_type(ArrayType)
|
|
||||||
def array_from_params(datatype, params, path, hit_paths):
|
|
||||||
if hasattr(params, 'getall'):
|
|
||||||
# webob multidict
|
|
||||||
def getall(params, path):
|
|
||||||
return params.getall(path)
|
|
||||||
elif hasattr(params, 'getlist'):
|
|
||||||
# werkzeug multidict
|
|
||||||
def getall(params, path): # noqa
|
|
||||||
return params.getlist(path)
|
|
||||||
if path in params:
|
|
||||||
hit_paths.add(path)
|
|
||||||
return [
|
|
||||||
from_param(datatype.item_type, value)
|
|
||||||
for value in getall(params, path)]
|
|
||||||
|
|
||||||
if iscomplex(datatype.item_type):
|
|
||||||
attributes = set()
|
|
||||||
r = re.compile('^%s\.(?P<attrname>[^\.])' % re.escape(path))
|
|
||||||
for p in params.keys():
|
|
||||||
m = r.match(p)
|
|
||||||
if m:
|
|
||||||
attributes.add(m.group('attrname'))
|
|
||||||
if attributes:
|
|
||||||
value = []
|
|
||||||
for attrdef in list_attributes(datatype.item_type):
|
|
||||||
attrpath = '%s.%s' % (path, attrdef.key)
|
|
||||||
hit_paths.add(attrpath)
|
|
||||||
attrvalues = getall(params, attrpath)
|
|
||||||
if len(value) < len(attrvalues):
|
|
||||||
value[-1:] = [
|
|
||||||
datatype.item_type()
|
|
||||||
for i in moves.range(len(attrvalues) - len(value))
|
|
||||||
]
|
|
||||||
for i, attrvalue in enumerate(attrvalues):
|
|
||||||
setattr(
|
|
||||||
value[i],
|
|
||||||
attrdef.key,
|
|
||||||
from_param(attrdef.datatype, attrvalue)
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
indexes = set()
|
|
||||||
r = re.compile('^%s\[(?P<index>\d+)\]' % re.escape(path))
|
|
||||||
|
|
||||||
for p in params.keys():
|
|
||||||
m = r.match(p)
|
|
||||||
if m:
|
|
||||||
indexes.add(int(m.group('index')))
|
|
||||||
|
|
||||||
if not indexes:
|
|
||||||
return Unset
|
|
||||||
|
|
||||||
indexes = list(indexes)
|
|
||||||
indexes.sort()
|
|
||||||
|
|
||||||
return [from_params(datatype.item_type, params,
|
|
||||||
'%s[%s]' % (path, index), hit_paths)
|
|
||||||
for index in indexes]
|
|
||||||
|
|
||||||
|
|
||||||
@from_params.when_type(DictType)
|
|
||||||
def dict_from_params(datatype, params, path, hit_paths):
|
|
||||||
|
|
||||||
keys = set()
|
|
||||||
r = re.compile('^%s\[(?P<key>[a-zA-Z0-9_\.]+)\]' % re.escape(path))
|
|
||||||
|
|
||||||
for p in params.keys():
|
|
||||||
m = r.match(p)
|
|
||||||
if m:
|
|
||||||
keys.add(from_param(datatype.key_type, m.group('key')))
|
|
||||||
|
|
||||||
if not keys:
|
|
||||||
return Unset
|
|
||||||
|
|
||||||
return dict((
|
|
||||||
(key, from_params(datatype.value_type,
|
|
||||||
params, '%s[%s]' % (path, key), hit_paths))
|
|
||||||
for key in keys))
|
|
||||||
|
|
||||||
|
|
||||||
@from_params.when_type(UserType)
|
|
||||||
def usertype_from_params(datatype, params, path, hit_paths):
|
|
||||||
value = from_params(datatype.basetype, params, path, hit_paths)
|
|
||||||
if value is not Unset:
|
|
||||||
return datatype.frombasetype(value)
|
|
||||||
return Unset
|
|
||||||
|
|
||||||
|
|
||||||
def args_from_args(funcdef, args, kwargs):
|
|
||||||
newargs = []
|
|
||||||
for argdef, arg in zip(funcdef.arguments[:len(args)], args):
|
|
||||||
try:
|
|
||||||
newargs.append(from_param(argdef.datatype, arg))
|
|
||||||
except Exception as e:
|
|
||||||
if isinstance(argdef.datatype, UserType):
|
|
||||||
datatype_name = argdef.datatype.name
|
|
||||||
elif isinstance(argdef.datatype, type):
|
|
||||||
datatype_name = argdef.datatype.__name__
|
|
||||||
else:
|
|
||||||
datatype_name = argdef.datatype.__class__.__name__
|
|
||||||
raise InvalidInput(
|
|
||||||
argdef.name,
|
|
||||||
arg,
|
|
||||||
"unable to convert to %(datatype)s. Error: %(error)s" % {
|
|
||||||
'datatype': datatype_name, 'error': e})
|
|
||||||
newkwargs = {}
|
|
||||||
for argname, value in kwargs.items():
|
|
||||||
newkwargs[argname] = from_param(
|
|
||||||
funcdef.get_arg(argname).datatype, value
|
|
||||||
)
|
|
||||||
return newargs, newkwargs
|
|
||||||
|
|
||||||
|
|
||||||
def args_from_params(funcdef, params):
|
|
||||||
kw = {}
|
|
||||||
hit_paths = set()
|
|
||||||
for argdef in funcdef.arguments:
|
|
||||||
value = from_params(
|
|
||||||
argdef.datatype, params, argdef.name, hit_paths)
|
|
||||||
if value is not Unset:
|
|
||||||
kw[argdef.name] = value
|
|
||||||
paths = set(params.keys())
|
|
||||||
unknown_paths = paths - hit_paths
|
|
||||||
if '__body__' in unknown_paths:
|
|
||||||
unknown_paths.remove('__body__')
|
|
||||||
if not funcdef.ignore_extra_args and unknown_paths:
|
|
||||||
raise UnknownArgument(', '.join(unknown_paths))
|
|
||||||
return [], kw
|
|
||||||
|
|
||||||
|
|
||||||
def args_from_body(funcdef, body, mimetype):
|
|
||||||
from wsme.rest import json as restjson
|
|
||||||
from wsme.rest import xml as restxml
|
|
||||||
|
|
||||||
if funcdef.body_type is not None:
|
|
||||||
datatypes = {funcdef.arguments[-1].name: funcdef.body_type}
|
|
||||||
else:
|
|
||||||
datatypes = dict(((a.name, a.datatype) for a in funcdef.arguments))
|
|
||||||
|
|
||||||
if not body:
|
|
||||||
return (), {}
|
|
||||||
if mimetype == "application/x-www-form-urlencoded":
|
|
||||||
# the parameters should have been parsed in params
|
|
||||||
return (), {}
|
|
||||||
elif mimetype in restjson.accept_content_types:
|
|
||||||
dataformat = restjson
|
|
||||||
elif mimetype in restxml.accept_content_types:
|
|
||||||
dataformat = restxml
|
|
||||||
else:
|
|
||||||
raise ClientSideError("Unknown mimetype: %s" % mimetype,
|
|
||||||
status_code=415)
|
|
||||||
|
|
||||||
try:
|
|
||||||
kw = dataformat.parse(
|
|
||||||
body, datatypes, bodyarg=funcdef.body_type is not None
|
|
||||||
)
|
|
||||||
except UnknownArgument:
|
|
||||||
if not funcdef.ignore_extra_args:
|
|
||||||
raise
|
|
||||||
kw = {}
|
|
||||||
|
|
||||||
return (), kw
|
|
||||||
|
|
||||||
|
|
||||||
def combine_args(funcdef, akw, allow_override=False):
|
|
||||||
newargs, newkwargs = [], {}
|
|
||||||
for args, kwargs in akw:
|
|
||||||
for i, arg in enumerate(args):
|
|
||||||
n = funcdef.arguments[i].name
|
|
||||||
if not allow_override and n in newkwargs:
|
|
||||||
raise ClientSideError(
|
|
||||||
"Parameter %s was given several times" % n)
|
|
||||||
newkwargs[n] = arg
|
|
||||||
for name, value in kwargs.items():
|
|
||||||
n = str(name)
|
|
||||||
if not allow_override and n in newkwargs:
|
|
||||||
raise ClientSideError(
|
|
||||||
"Parameter %s was given several times" % n)
|
|
||||||
newkwargs[n] = value
|
|
||||||
return newargs, newkwargs
|
|
||||||
|
|
||||||
|
|
||||||
def get_args(funcdef, args, kwargs, params, form, body, mimetype):
|
|
||||||
"""Combine arguments from :
|
|
||||||
* the host framework args and kwargs
|
|
||||||
* the request params
|
|
||||||
* the request body
|
|
||||||
|
|
||||||
Note that the host framework args and kwargs can be overridden
|
|
||||||
by arguments from params of body
|
|
||||||
"""
|
|
||||||
# get the body from params if not given directly
|
|
||||||
if not body and '__body__' in params:
|
|
||||||
body = params['__body__']
|
|
||||||
|
|
||||||
# extract args from the host args and kwargs
|
|
||||||
from_args = args_from_args(funcdef, args, kwargs)
|
|
||||||
|
|
||||||
# extract args from the request parameters
|
|
||||||
from_params = args_from_params(funcdef, params)
|
|
||||||
|
|
||||||
# extract args from the form parameters
|
|
||||||
if form:
|
|
||||||
from_form_params = args_from_params(funcdef, form)
|
|
||||||
else:
|
|
||||||
from_form_params = (), {}
|
|
||||||
|
|
||||||
# extract args from the request body
|
|
||||||
from_body = args_from_body(funcdef, body, mimetype)
|
|
||||||
|
|
||||||
# combine params and body arguments
|
|
||||||
from_params_and_body = combine_args(
|
|
||||||
funcdef,
|
|
||||||
(from_params, from_form_params, from_body)
|
|
||||||
)
|
|
||||||
|
|
||||||
args, kwargs = combine_args(
|
|
||||||
funcdef,
|
|
||||||
(from_args, from_params_and_body),
|
|
||||||
allow_override=True
|
|
||||||
)
|
|
||||||
wsme.runtime.check_arguments(funcdef, args, kwargs)
|
|
||||||
return args, kwargs
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
"""REST+Json protocol implementation."""
|
|
||||||
from __future__ import absolute_import
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from simplegeneric import generic
|
|
||||||
|
|
||||||
import wsme.exc
|
|
||||||
import wsme.types
|
|
||||||
from wsme.types import Unset
|
|
||||||
import wsme.utils
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json # noqa
|
|
||||||
|
|
||||||
|
|
||||||
content_type = 'application/json'
|
|
||||||
accept_content_types = [
|
|
||||||
content_type,
|
|
||||||
'text/javascript',
|
|
||||||
'application/javascript'
|
|
||||||
]
|
|
||||||
ENUM_TRUE = ('true', 't', 'yes', 'y', 'on', '1')
|
|
||||||
ENUM_FALSE = ('false', 'f', 'no', 'n', 'off', '0')
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def tojson(datatype, value):
|
|
||||||
"""
|
|
||||||
A generic converter from python to jsonify-able datatypes.
|
|
||||||
|
|
||||||
If a non-complex user specific type is to be used in the api,
|
|
||||||
a specific tojson should be added::
|
|
||||||
|
|
||||||
from wsme.protocol.restjson import tojson
|
|
||||||
|
|
||||||
myspecialtype = object()
|
|
||||||
|
|
||||||
@tojson.when_object(myspecialtype)
|
|
||||||
def myspecialtype_tojson(datatype, value):
|
|
||||||
return str(value)
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if wsme.types.iscomplex(datatype):
|
|
||||||
d = dict()
|
|
||||||
for attr in wsme.types.list_attributes(datatype):
|
|
||||||
attr_value = getattr(value, attr.key)
|
|
||||||
if attr_value is not Unset:
|
|
||||||
d[attr.name] = tojson(attr.datatype, attr_value)
|
|
||||||
return d
|
|
||||||
elif wsme.types.isusertype(datatype):
|
|
||||||
return tojson(datatype.basetype, datatype.tobasetype(value))
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(wsme.types.bytes)
|
|
||||||
def bytes_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return value.decode('ascii')
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_type(wsme.types.ArrayType)
|
|
||||||
def array_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return [tojson(datatype.item_type, item) for item in value]
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_type(wsme.types.DictType)
|
|
||||||
def dict_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return dict((
|
|
||||||
(tojson(datatype.key_type, item[0]),
|
|
||||||
tojson(datatype.value_type, item[1]))
|
|
||||||
for item in value.items()
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(decimal.Decimal)
|
|
||||||
def decimal_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(datetime.date)
|
|
||||||
def date_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return value.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(datetime.time)
|
|
||||||
def time_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return value.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(datetime.datetime)
|
|
||||||
def datetime_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return value.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def fromjson(datatype, value):
|
|
||||||
"""A generic converter from json base types to python datatype.
|
|
||||||
|
|
||||||
If a non-complex user specific type is to be used in the api,
|
|
||||||
a specific fromjson should be added::
|
|
||||||
|
|
||||||
from wsme.protocol.restjson import fromjson
|
|
||||||
|
|
||||||
class MySpecialType(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@fromjson.when_object(MySpecialType)
|
|
||||||
def myspecialtype_fromjson(datatype, value):
|
|
||||||
return MySpecialType(value)
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if wsme.types.iscomplex(datatype):
|
|
||||||
obj = datatype()
|
|
||||||
attributes = wsme.types.list_attributes(datatype)
|
|
||||||
|
|
||||||
# Here we check that all the attributes in the value are also defined
|
|
||||||
# in our type definition, otherwise we raise an Error.
|
|
||||||
v_keys = set(value.keys())
|
|
||||||
a_keys = set(adef.name for adef in attributes)
|
|
||||||
if not v_keys <= a_keys:
|
|
||||||
raise wsme.exc.UnknownAttribute(None, v_keys - a_keys)
|
|
||||||
|
|
||||||
for attrdef in attributes:
|
|
||||||
if attrdef.name in value:
|
|
||||||
try:
|
|
||||||
val_fromjson = fromjson(attrdef.datatype,
|
|
||||||
value[attrdef.name])
|
|
||||||
except wsme.exc.UnknownAttribute as e:
|
|
||||||
e.add_fieldname(attrdef.name)
|
|
||||||
raise
|
|
||||||
if getattr(attrdef, 'readonly', False):
|
|
||||||
raise wsme.exc.InvalidInput(attrdef.name, val_fromjson,
|
|
||||||
"Cannot set read only field.")
|
|
||||||
setattr(obj, attrdef.key, val_fromjson)
|
|
||||||
elif attrdef.mandatory:
|
|
||||||
raise wsme.exc.InvalidInput(attrdef.name, None,
|
|
||||||
"Mandatory field missing.")
|
|
||||||
|
|
||||||
return wsme.types.validate_value(datatype, obj)
|
|
||||||
elif wsme.types.isusertype(datatype):
|
|
||||||
value = datatype.frombasetype(
|
|
||||||
fromjson(datatype.basetype, value))
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_type(wsme.types.ArrayType)
|
|
||||||
def array_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if not isinstance(value, list):
|
|
||||||
raise ValueError("Value not a valid list: %s" % value)
|
|
||||||
return [fromjson(datatype.item_type, item) for item in value]
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_type(wsme.types.DictType)
|
|
||||||
def dict_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
raise ValueError("Value not a valid dict: %s" % value)
|
|
||||||
return dict((
|
|
||||||
(fromjson(datatype.key_type, item[0]),
|
|
||||||
fromjson(datatype.value_type, item[1]))
|
|
||||||
for item in value.items()))
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(six.binary_type)
|
|
||||||
def str_fromjson(datatype, value):
|
|
||||||
if (isinstance(value, six.string_types) or
|
|
||||||
isinstance(value, six.integer_types) or
|
|
||||||
isinstance(value, float)):
|
|
||||||
return six.text_type(value).encode('utf8')
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(wsme.types.text)
|
|
||||||
def text_fromjson(datatype, value):
|
|
||||||
if value is not None and isinstance(value, wsme.types.bytes):
|
|
||||||
return wsme.types.text(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(*six.integer_types + (float,))
|
|
||||||
def numeric_fromjson(datatype, value):
|
|
||||||
"""Convert string object to built-in types int, long or float."""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return datatype(value)
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(bool)
|
|
||||||
def bool_fromjson(datatype, value):
|
|
||||||
"""Convert to bool, restricting strings to just unambiguous values."""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if isinstance(value, six.integer_types + (bool,)):
|
|
||||||
return bool(value)
|
|
||||||
if value in ENUM_TRUE:
|
|
||||||
return True
|
|
||||||
if value in ENUM_FALSE:
|
|
||||||
return False
|
|
||||||
raise ValueError("Value not an unambiguous boolean: %s" % value)
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(decimal.Decimal)
|
|
||||||
def decimal_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return decimal.Decimal(value)
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(datetime.date)
|
|
||||||
def date_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return wsme.utils.parse_isodate(value)
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(datetime.time)
|
|
||||||
def time_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return wsme.utils.parse_isotime(value)
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_object(datetime.datetime)
|
|
||||||
def datetime_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return wsme.utils.parse_isodatetime(value)
|
|
||||||
|
|
||||||
|
|
||||||
def parse(s, datatypes, bodyarg, encoding='utf8'):
|
|
||||||
jload = json.load
|
|
||||||
if not hasattr(s, 'read'):
|
|
||||||
if six.PY3 and isinstance(s, six.binary_type):
|
|
||||||
s = s.decode(encoding)
|
|
||||||
jload = json.loads
|
|
||||||
try:
|
|
||||||
jdata = jload(s)
|
|
||||||
except ValueError:
|
|
||||||
raise wsme.exc.ClientSideError("Request is not in valid JSON format")
|
|
||||||
if bodyarg:
|
|
||||||
argname = list(datatypes.keys())[0]
|
|
||||||
try:
|
|
||||||
kw = {argname: fromjson(datatypes[argname], jdata)}
|
|
||||||
except ValueError as e:
|
|
||||||
raise wsme.exc.InvalidInput(argname, jdata, e.args[0])
|
|
||||||
except wsme.exc.UnknownAttribute as e:
|
|
||||||
# We only know the fieldname at this level, not in the
|
|
||||||
# called function. We fill in this information here.
|
|
||||||
e.add_fieldname(argname)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
kw = {}
|
|
||||||
extra_args = []
|
|
||||||
if not isinstance(jdata, dict):
|
|
||||||
raise wsme.exc.ClientSideError("Request must be a JSON dict")
|
|
||||||
for key in jdata:
|
|
||||||
if key not in datatypes:
|
|
||||||
extra_args.append(key)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
kw[key] = fromjson(datatypes[key], jdata[key])
|
|
||||||
except ValueError as e:
|
|
||||||
raise wsme.exc.InvalidInput(key, jdata[key], e.args[0])
|
|
||||||
except wsme.exc.UnknownAttribute as e:
|
|
||||||
# We only know the fieldname at this level, not in the
|
|
||||||
# called function. We fill in this information here.
|
|
||||||
e.add_fieldname(key)
|
|
||||||
raise
|
|
||||||
if extra_args:
|
|
||||||
raise wsme.exc.UnknownArgument(', '.join(extra_args))
|
|
||||||
return kw
|
|
||||||
|
|
||||||
|
|
||||||
def encode_result(value, datatype, **options):
|
|
||||||
jsondata = tojson(datatype, value)
|
|
||||||
if options.get('nest_result', False):
|
|
||||||
jsondata = {options.get('nested_result_attrname', 'result'): jsondata}
|
|
||||||
return json.dumps(jsondata)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_error(context, errordetail):
|
|
||||||
return json.dumps(errordetail)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_sample_value(datatype, value, format=False):
|
|
||||||
r = tojson(datatype, value)
|
|
||||||
content = json.dumps(r, ensure_ascii=False, indent=4 if format else 0,
|
|
||||||
sort_keys=format)
|
|
||||||
return ('javascript', content)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_sample_params(params, format=False):
|
|
||||||
kw = {}
|
|
||||||
for name, datatype, value in params:
|
|
||||||
kw[name] = tojson(datatype, value)
|
|
||||||
content = json.dumps(kw, ensure_ascii=False, indent=4 if format else 0,
|
|
||||||
sort_keys=format)
|
|
||||||
return ('javascript', content)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_sample_result(datatype, value, format=False):
|
|
||||||
r = tojson(datatype, value)
|
|
||||||
content = json.dumps(r, ensure_ascii=False, indent=4 if format else 0,
|
|
||||||
sort_keys=format)
|
|
||||||
return ('javascript', content)
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import os.path
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from wsme.utils import OrderedDict
|
|
||||||
from wsme.protocol import CallContext, Protocol, media_type_accept
|
|
||||||
|
|
||||||
import wsme.rest
|
|
||||||
import wsme.rest.args
|
|
||||||
import wsme.runtime
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RestProtocol(Protocol):
|
|
||||||
name = 'rest'
|
|
||||||
displayname = 'REST'
|
|
||||||
dataformats = ['json', 'xml']
|
|
||||||
content_types = ['application/json', 'text/xml']
|
|
||||||
|
|
||||||
def __init__(self, dataformats=None):
|
|
||||||
if dataformats is None:
|
|
||||||
dataformats = RestProtocol.dataformats
|
|
||||||
|
|
||||||
self.dataformats = OrderedDict()
|
|
||||||
self.content_types = []
|
|
||||||
|
|
||||||
for dataformat in dataformats:
|
|
||||||
__import__('wsme.rest.' + dataformat)
|
|
||||||
dfmod = getattr(wsme.rest, dataformat)
|
|
||||||
self.dataformats[dataformat] = dfmod
|
|
||||||
self.content_types.extend(dfmod.accept_content_types)
|
|
||||||
|
|
||||||
def accept(self, request):
|
|
||||||
for dataformat in self.dataformats:
|
|
||||||
if request.path.endswith('.' + dataformat):
|
|
||||||
return True
|
|
||||||
return media_type_accept(request, self.content_types)
|
|
||||||
|
|
||||||
def iter_calls(self, request):
|
|
||||||
context = CallContext(request)
|
|
||||||
context.outformat = None
|
|
||||||
ext = os.path.splitext(request.path.split('/')[-1])[1]
|
|
||||||
inmime = request.content_type
|
|
||||||
outmime = request.accept.best_match(self.content_types)
|
|
||||||
|
|
||||||
outformat = None
|
|
||||||
informat = None
|
|
||||||
for dfname, df in self.dataformats.items():
|
|
||||||
if ext == '.' + dfname:
|
|
||||||
outformat = df
|
|
||||||
if not inmime:
|
|
||||||
informat = df
|
|
||||||
|
|
||||||
if outformat is None and request.accept:
|
|
||||||
for dfname, df in self.dataformats.items():
|
|
||||||
if outmime in df.accept_content_types:
|
|
||||||
outformat = df
|
|
||||||
if not inmime:
|
|
||||||
informat = df
|
|
||||||
|
|
||||||
if outformat is None:
|
|
||||||
for dfname, df in self.dataformats.items():
|
|
||||||
if inmime == df.content_type:
|
|
||||||
outformat = df
|
|
||||||
|
|
||||||
context.outformat = outformat
|
|
||||||
context.outformat_options = {
|
|
||||||
'nest_result': getattr(self, 'nest_result', False)
|
|
||||||
}
|
|
||||||
if not inmime and informat:
|
|
||||||
inmime = informat.content_type
|
|
||||||
log.debug("Inferred input type: %s" % inmime)
|
|
||||||
context.inmime = inmime
|
|
||||||
yield context
|
|
||||||
|
|
||||||
def extract_path(self, context):
|
|
||||||
path = context.request.path
|
|
||||||
assert path.startswith(self.root._webpath)
|
|
||||||
path = path[len(self.root._webpath):]
|
|
||||||
path = path.strip('/').split('/')
|
|
||||||
|
|
||||||
for dataformat in self.dataformats:
|
|
||||||
if path[-1].endswith('.' + dataformat):
|
|
||||||
path[-1] = path[-1][:-len(dataformat) - 1]
|
|
||||||
|
|
||||||
# Check if the path is actually a function, and if not
|
|
||||||
# see if the http method make a difference
|
|
||||||
# TODO Re-think the function lookup phases. Here we are
|
|
||||||
# doing the job that will be done in a later phase, which
|
|
||||||
# is sub-optimal
|
|
||||||
for p, fdef in self.root.getapi():
|
|
||||||
if p == path:
|
|
||||||
return path
|
|
||||||
|
|
||||||
# No function at this path. Now check for function that have
|
|
||||||
# this path as a prefix, and declared an http method
|
|
||||||
for p, fdef in self.root.getapi():
|
|
||||||
if len(p) == len(path) + 1 and p[:len(path)] == path and \
|
|
||||||
fdef.extra_options.get('method') == context.request.method:
|
|
||||||
return p
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
def read_arguments(self, context):
|
|
||||||
request = context.request
|
|
||||||
funcdef = context.funcdef
|
|
||||||
|
|
||||||
body = None
|
|
||||||
if request.content_length not in (None, 0, '0'):
|
|
||||||
body = request.body
|
|
||||||
if not body and '__body__' in request.params:
|
|
||||||
body = request.params['__body__']
|
|
||||||
|
|
||||||
args, kwargs = wsme.rest.args.combine_args(
|
|
||||||
funcdef,
|
|
||||||
(wsme.rest.args.args_from_params(funcdef, request.params),
|
|
||||||
wsme.rest.args.args_from_body(funcdef, body, context.inmime))
|
|
||||||
)
|
|
||||||
wsme.runtime.check_arguments(funcdef, args, kwargs)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def encode_result(self, context, result):
|
|
||||||
out = context.outformat.encode_result(
|
|
||||||
result, context.funcdef.return_type,
|
|
||||||
**context.outformat_options
|
|
||||||
)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def encode_error(self, context, errordetail):
|
|
||||||
out = context.outformat.encode_error(
|
|
||||||
context, errordetail
|
|
||||||
)
|
|
||||||
return out
|
|
||||||
298
wsme/rest/xml.py
298
wsme/rest/xml.py
@@ -1,298 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as et
|
|
||||||
|
|
||||||
from simplegeneric import generic
|
|
||||||
|
|
||||||
import wsme.types
|
|
||||||
from wsme.exc import UnknownArgument, InvalidInput
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
content_type = 'text/xml'
|
|
||||||
accept_content_types = [
|
|
||||||
content_type,
|
|
||||||
]
|
|
||||||
|
|
||||||
time_re = re.compile(r'(?P<h>[0-2][0-9]):(?P<m>[0-5][0-9]):(?P<s>[0-6][0-9])')
|
|
||||||
|
|
||||||
|
|
||||||
def xml_indent(elem, level=0):
|
|
||||||
i = "\n" + level * " "
|
|
||||||
if len(elem):
|
|
||||||
if not elem.text or not elem.text.strip():
|
|
||||||
elem.text = i + " "
|
|
||||||
for e in elem:
|
|
||||||
xml_indent(e, level + 1)
|
|
||||||
if not e.tail or not e.tail.strip():
|
|
||||||
e.tail = i
|
|
||||||
if level and (not elem.tail or not elem.tail.strip()):
|
|
||||||
elem.tail = i
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def toxml(datatype, key, value):
|
|
||||||
"""
|
|
||||||
A generic converter from python to xml elements.
|
|
||||||
|
|
||||||
If a non-complex user specific type is to be used in the api,
|
|
||||||
a specific toxml should be added::
|
|
||||||
|
|
||||||
from wsme.protocol.restxml import toxml
|
|
||||||
|
|
||||||
myspecialtype = object()
|
|
||||||
|
|
||||||
@toxml.when_object(myspecialtype)
|
|
||||||
def myspecialtype_toxml(datatype, key, value):
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
el.text = str(value)
|
|
||||||
return el
|
|
||||||
"""
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
if wsme.types.isusertype(datatype):
|
|
||||||
return toxml(datatype.basetype,
|
|
||||||
key, datatype.tobasetype(value))
|
|
||||||
elif wsme.types.iscomplex(datatype):
|
|
||||||
for attrdef in datatype._wsme_attributes:
|
|
||||||
attrvalue = getattr(value, attrdef.key)
|
|
||||||
if attrvalue is not wsme.types.Unset:
|
|
||||||
el.append(toxml(attrdef.datatype, attrdef.name,
|
|
||||||
attrvalue))
|
|
||||||
else:
|
|
||||||
el.text = six.text_type(value)
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def fromxml(datatype, element):
|
|
||||||
"""
|
|
||||||
A generic converter from xml elements to python datatype.
|
|
||||||
|
|
||||||
If a non-complex user specific type is to be used in the api,
|
|
||||||
a specific fromxml should be added::
|
|
||||||
|
|
||||||
from wsme.protocol.restxml import fromxml
|
|
||||||
|
|
||||||
class MySpecialType(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@fromxml.when_object(MySpecialType)
|
|
||||||
def myspecialtype_fromxml(datatype, element):
|
|
||||||
if element.get('nil', False):
|
|
||||||
return None
|
|
||||||
return MySpecialType(element.text)
|
|
||||||
"""
|
|
||||||
if element.get('nil', False):
|
|
||||||
return None
|
|
||||||
if wsme.types.isusertype(datatype):
|
|
||||||
return datatype.frombasetype(fromxml(datatype.basetype, element))
|
|
||||||
if wsme.types.iscomplex(datatype):
|
|
||||||
obj = datatype()
|
|
||||||
for attrdef in wsme.types.list_attributes(datatype):
|
|
||||||
sub = element.find(attrdef.name)
|
|
||||||
if sub is not None:
|
|
||||||
val_fromxml = fromxml(attrdef.datatype, sub)
|
|
||||||
if getattr(attrdef, 'readonly', False):
|
|
||||||
raise InvalidInput(attrdef.name, val_fromxml,
|
|
||||||
"Cannot set read only field.")
|
|
||||||
setattr(obj, attrdef.key, val_fromxml)
|
|
||||||
elif attrdef.mandatory:
|
|
||||||
raise InvalidInput(attrdef.name, None,
|
|
||||||
"Mandatory field missing.")
|
|
||||||
return wsme.types.validate_value(datatype, obj)
|
|
||||||
if datatype is wsme.types.bytes:
|
|
||||||
return element.text.encode('ascii')
|
|
||||||
return datatype(element.text)
|
|
||||||
|
|
||||||
|
|
||||||
@toxml.when_type(wsme.types.ArrayType)
|
|
||||||
def array_toxml(datatype, key, value):
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
for item in value:
|
|
||||||
el.append(toxml(datatype.item_type, 'item', item))
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
@toxml.when_type(wsme.types.DictType)
|
|
||||||
def dict_toxml(datatype, key, value):
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
for item in value.items():
|
|
||||||
key = toxml(datatype.key_type, 'key', item[0])
|
|
||||||
value = toxml(datatype.value_type, 'value', item[1])
|
|
||||||
node = et.Element('item')
|
|
||||||
node.append(key)
|
|
||||||
node.append(value)
|
|
||||||
el.append(node)
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
@toxml.when_object(wsme.types.bytes)
|
|
||||||
def bytes_toxml(datatype, key, value):
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
el.text = value.decode('ascii')
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
@toxml.when_object(bool)
|
|
||||||
def bool_toxml(datatype, key, value):
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
el.text = value and 'true' or 'false'
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
@toxml.when_object(datetime.date)
|
|
||||||
def date_toxml(datatype, key, value):
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
el.text = value.isoformat()
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
@toxml.when_object(datetime.datetime)
|
|
||||||
def datetime_toxml(datatype, key, value):
|
|
||||||
el = et.Element(key)
|
|
||||||
if value is None:
|
|
||||||
el.set('nil', 'true')
|
|
||||||
else:
|
|
||||||
el.text = value.isoformat()
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
@fromxml.when_type(wsme.types.ArrayType)
|
|
||||||
def array_fromxml(datatype, element):
|
|
||||||
if element.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
return [
|
|
||||||
fromxml(datatype.item_type, item)
|
|
||||||
for item in element.findall('item')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@fromxml.when_object(bool)
|
|
||||||
def bool_fromxml(datatype, element):
|
|
||||||
if element.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
return element.text.lower() != 'false'
|
|
||||||
|
|
||||||
|
|
||||||
@fromxml.when_type(wsme.types.DictType)
|
|
||||||
def dict_fromxml(datatype, element):
|
|
||||||
if element.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
return dict((
|
|
||||||
(fromxml(datatype.key_type, item.find('key')),
|
|
||||||
fromxml(datatype.value_type, item.find('value')))
|
|
||||||
for item in element.findall('item')))
|
|
||||||
|
|
||||||
|
|
||||||
@fromxml.when_object(wsme.types.text)
|
|
||||||
def unicode_fromxml(datatype, element):
|
|
||||||
if element.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
return wsme.types.text(element.text) if element.text else six.u('')
|
|
||||||
|
|
||||||
|
|
||||||
@fromxml.when_object(datetime.date)
|
|
||||||
def date_fromxml(datatype, element):
|
|
||||||
if element.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
return wsme.utils.parse_isodate(element.text)
|
|
||||||
|
|
||||||
|
|
||||||
@fromxml.when_object(datetime.time)
|
|
||||||
def time_fromxml(datatype, element):
|
|
||||||
if element.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
return wsme.utils.parse_isotime(element.text)
|
|
||||||
|
|
||||||
|
|
||||||
@fromxml.when_object(datetime.datetime)
|
|
||||||
def datetime_fromxml(datatype, element):
|
|
||||||
if element.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
return wsme.utils.parse_isodatetime(element.text)
|
|
||||||
|
|
||||||
|
|
||||||
def parse(s, datatypes, bodyarg):
|
|
||||||
if hasattr(s, 'read'):
|
|
||||||
tree = et.parse(s)
|
|
||||||
else:
|
|
||||||
tree = et.fromstring(s)
|
|
||||||
if bodyarg:
|
|
||||||
name = list(datatypes.keys())[0]
|
|
||||||
return {name: fromxml(datatypes[name], tree)}
|
|
||||||
else:
|
|
||||||
kw = {}
|
|
||||||
extra_args = []
|
|
||||||
for sub in tree:
|
|
||||||
if sub.tag not in datatypes:
|
|
||||||
extra_args.append(sub.tag)
|
|
||||||
kw[sub.tag] = fromxml(datatypes[sub.tag], sub)
|
|
||||||
if extra_args:
|
|
||||||
raise UnknownArgument(', '.join(extra_args))
|
|
||||||
return kw
|
|
||||||
|
|
||||||
|
|
||||||
def encode_result(value, datatype, **options):
|
|
||||||
return et.tostring(toxml(
|
|
||||||
datatype, options.get('nested_result_attrname', 'result'), value
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def encode_error(context, errordetail):
|
|
||||||
el = et.Element('error')
|
|
||||||
et.SubElement(el, 'faultcode').text = errordetail['faultcode']
|
|
||||||
et.SubElement(el, 'faultstring').text = errordetail['faultstring']
|
|
||||||
if 'debuginfo' in errordetail:
|
|
||||||
et.SubElement(el, 'debuginfo').text = errordetail['debuginfo']
|
|
||||||
return et.tostring(el)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_sample_value(datatype, value, format=False):
|
|
||||||
r = toxml(datatype, 'value', value)
|
|
||||||
if format:
|
|
||||||
xml_indent(r)
|
|
||||||
content = et.tostring(r)
|
|
||||||
return ('xml', content)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_sample_params(params, format=False):
|
|
||||||
node = et.Element('parameters')
|
|
||||||
for name, datatype, value in params:
|
|
||||||
node.append(toxml(datatype, name, value))
|
|
||||||
if format:
|
|
||||||
xml_indent(node)
|
|
||||||
content = et.tostring(node)
|
|
||||||
return ('xml', content)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_sample_result(datatype, value, format=False):
|
|
||||||
r = toxml(datatype, 'result', value)
|
|
||||||
if format:
|
|
||||||
xml_indent(r)
|
|
||||||
content = et.tostring(r)
|
|
||||||
return ('xml', content)
|
|
||||||
372
wsme/root.py
372
wsme/root.py
@@ -1,372 +0,0 @@
|
|||||||
import logging
|
|
||||||
import sys
|
|
||||||
import weakref
|
|
||||||
|
|
||||||
from six import u, b
|
|
||||||
import six
|
|
||||||
|
|
||||||
import webob
|
|
||||||
|
|
||||||
from wsme.exc import ClientSideError, UnknownFunction
|
|
||||||
from wsme.protocol import getprotocol
|
|
||||||
from wsme.rest import scan_api
|
|
||||||
from wsme import spore
|
|
||||||
import wsme.api
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
html_body = u("""
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style type='text/css'>
|
|
||||||
%(css)s
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
%(content)s
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
def default_prepare_response_body(request, results):
|
|
||||||
r = None
|
|
||||||
sep = None
|
|
||||||
for value in results:
|
|
||||||
if sep is None:
|
|
||||||
if isinstance(value, six.text_type):
|
|
||||||
sep = u('\n')
|
|
||||||
r = u('')
|
|
||||||
else:
|
|
||||||
sep = b('\n')
|
|
||||||
r = b('')
|
|
||||||
else:
|
|
||||||
r += sep
|
|
||||||
r += value
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
class DummyTransaction:
|
|
||||||
def commit(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def abort(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WSRoot(object):
|
|
||||||
"""
|
|
||||||
Root controller for webservices.
|
|
||||||
|
|
||||||
:param protocols: A list of protocols to enable (see :meth:`addprotocol`)
|
|
||||||
:param webpath: The web path where the webservice is published.
|
|
||||||
|
|
||||||
:type transaction: A `transaction
|
|
||||||
<http://pypi.python.org/pypi/transaction>`_-like
|
|
||||||
object or ``True``.
|
|
||||||
:param transaction: If specified, a transaction will be created and
|
|
||||||
handled on a per-call base.
|
|
||||||
|
|
||||||
This option *can* be enabled along with `repoze.tm2
|
|
||||||
<http://pypi.python.org/pypi/repoze.tm2>`_
|
|
||||||
(it will only make it void).
|
|
||||||
|
|
||||||
If ``True``, the default :mod:`transaction`
|
|
||||||
module will be imported and used.
|
|
||||||
|
|
||||||
"""
|
|
||||||
__registry__ = wsme.types.registry
|
|
||||||
|
|
||||||
def __init__(self, protocols=[], webpath='', transaction=None,
|
|
||||||
scan_api=scan_api):
|
|
||||||
self._debug = True
|
|
||||||
self._webpath = webpath
|
|
||||||
self.protocols = []
|
|
||||||
self._scan_api = scan_api
|
|
||||||
|
|
||||||
self._transaction = transaction
|
|
||||||
if self._transaction is True:
|
|
||||||
import transaction
|
|
||||||
self._transaction = transaction
|
|
||||||
|
|
||||||
for protocol in protocols:
|
|
||||||
self.addprotocol(protocol)
|
|
||||||
|
|
||||||
self._api = None
|
|
||||||
|
|
||||||
def wsgiapp(self):
|
|
||||||
"""Returns a wsgi application"""
|
|
||||||
from webob.dec import wsgify
|
|
||||||
return wsgify(self._handle_request)
|
|
||||||
|
|
||||||
def begin(self):
|
|
||||||
if self._transaction:
|
|
||||||
return self._transaction.begin()
|
|
||||||
else:
|
|
||||||
return DummyTransaction()
|
|
||||||
|
|
||||||
def addprotocol(self, protocol, **options):
|
|
||||||
"""
|
|
||||||
Enable a new protocol on the controller.
|
|
||||||
|
|
||||||
:param protocol: A registered protocol name or an instance
|
|
||||||
of a protocol.
|
|
||||||
"""
|
|
||||||
if isinstance(protocol, str):
|
|
||||||
protocol = getprotocol(protocol, **options)
|
|
||||||
self.protocols.append(protocol)
|
|
||||||
protocol.root = weakref.proxy(self)
|
|
||||||
|
|
||||||
def getapi(self):
|
|
||||||
"""
|
|
||||||
Returns the api description.
|
|
||||||
|
|
||||||
:rtype: list of (path, :class:`FunctionDefinition`)
|
|
||||||
"""
|
|
||||||
if self._api is None:
|
|
||||||
self._api = [
|
|
||||||
(path, f, f._wsme_definition, args)
|
|
||||||
for path, f, args in self._scan_api(self)
|
|
||||||
]
|
|
||||||
for path, f, fdef, args in self._api:
|
|
||||||
fdef.resolve_types(self.__registry__)
|
|
||||||
return [
|
|
||||||
(path, fdef)
|
|
||||||
for path, f, fdef, args in self._api
|
|
||||||
]
|
|
||||||
|
|
||||||
def _get_protocol(self, name):
|
|
||||||
for protocol in self.protocols:
|
|
||||||
if protocol.name == name:
|
|
||||||
return protocol
|
|
||||||
|
|
||||||
def _select_protocol(self, request):
|
|
||||||
log.debug("Selecting a protocol for the following request :\n"
|
|
||||||
"headers: %s\nbody: %s", request.headers.items(),
|
|
||||||
request.content_length and (
|
|
||||||
request.content_length > 512 and
|
|
||||||
request.body[:512] or
|
|
||||||
request.body) or '')
|
|
||||||
protocol = None
|
|
||||||
error = ClientSideError(status_code=406)
|
|
||||||
path = str(request.path)
|
|
||||||
assert path.startswith(self._webpath)
|
|
||||||
path = path[len(self._webpath) + 1:]
|
|
||||||
if 'wsmeproto' in request.params:
|
|
||||||
return self._get_protocol(request.params['wsmeproto'])
|
|
||||||
else:
|
|
||||||
|
|
||||||
for p in self.protocols:
|
|
||||||
try:
|
|
||||||
if p.accept(request):
|
|
||||||
protocol = p
|
|
||||||
break
|
|
||||||
except ClientSideError as e:
|
|
||||||
error = e
|
|
||||||
# If we could not select a protocol, we raise the last exception
|
|
||||||
# that we got, or the default one.
|
|
||||||
if not protocol:
|
|
||||||
raise error
|
|
||||||
return protocol
|
|
||||||
|
|
||||||
def _do_call(self, protocol, context):
|
|
||||||
request = context.request
|
|
||||||
request.calls.append(context)
|
|
||||||
try:
|
|
||||||
if context.path is None:
|
|
||||||
context.path = protocol.extract_path(context)
|
|
||||||
|
|
||||||
if context.path is None:
|
|
||||||
raise ClientSideError(u(
|
|
||||||
'The %s protocol was unable to extract a function '
|
|
||||||
'path from the request') % protocol.name)
|
|
||||||
|
|
||||||
context.func, context.funcdef, args = \
|
|
||||||
self._lookup_function(context.path)
|
|
||||||
kw = protocol.read_arguments(context)
|
|
||||||
args = list(args)
|
|
||||||
|
|
||||||
txn = self.begin()
|
|
||||||
try:
|
|
||||||
result = context.func(*args, **kw)
|
|
||||||
txn.commit()
|
|
||||||
except:
|
|
||||||
txn.abort()
|
|
||||||
raise
|
|
||||||
|
|
||||||
else:
|
|
||||||
# TODO make sure result type == a._wsme_definition.return_type
|
|
||||||
return protocol.encode_result(context, result)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
infos = wsme.api.format_exception(sys.exc_info(), self._debug)
|
|
||||||
if isinstance(e, ClientSideError):
|
|
||||||
request.client_errorcount += 1
|
|
||||||
request.client_last_status_code = e.code
|
|
||||||
else:
|
|
||||||
request.server_errorcount += 1
|
|
||||||
return protocol.encode_error(context, infos)
|
|
||||||
|
|
||||||
def find_route(self, path):
|
|
||||||
for p in self.protocols:
|
|
||||||
for routepath, func in p.iter_routes():
|
|
||||||
if path.startswith(routepath):
|
|
||||||
return routepath, func
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def _handle_request(self, request):
|
|
||||||
res = webob.Response()
|
|
||||||
res_content_type = None
|
|
||||||
|
|
||||||
path = request.path
|
|
||||||
if path.startswith(self._webpath):
|
|
||||||
path = path[len(self._webpath):]
|
|
||||||
routepath, func = self.find_route(path)
|
|
||||||
if routepath:
|
|
||||||
content = func()
|
|
||||||
if isinstance(content, six.text_type):
|
|
||||||
res.text = content
|
|
||||||
elif isinstance(content, six.binary_type):
|
|
||||||
res.body = content
|
|
||||||
res.content_type = func._cfg['content-type']
|
|
||||||
return res
|
|
||||||
|
|
||||||
if request.path == self._webpath + '/api.spore':
|
|
||||||
res.body = spore.getdesc(self, request.host_url)
|
|
||||||
res.content_type = 'application/json'
|
|
||||||
return res
|
|
||||||
|
|
||||||
try:
|
|
||||||
msg = None
|
|
||||||
error_status = 500
|
|
||||||
protocol = self._select_protocol(request)
|
|
||||||
except ClientSideError as e:
|
|
||||||
error_status = e.code
|
|
||||||
msg = e.faultstring
|
|
||||||
protocol = None
|
|
||||||
except Exception as e:
|
|
||||||
msg = ("Unexpected error while selecting protocol: %s" % str(e))
|
|
||||||
log.exception(msg)
|
|
||||||
protocol = None
|
|
||||||
error_status = 500
|
|
||||||
|
|
||||||
if protocol is None:
|
|
||||||
if not msg:
|
|
||||||
msg = ("None of the following protocols can handle this "
|
|
||||||
"request : %s" % ','.join((
|
|
||||||
p.name for p in self.protocols)))
|
|
||||||
res.status = error_status
|
|
||||||
res.content_type = 'text/plain'
|
|
||||||
try:
|
|
||||||
res.text = u(msg)
|
|
||||||
except TypeError:
|
|
||||||
res.text = msg
|
|
||||||
log.error(msg)
|
|
||||||
return res
|
|
||||||
|
|
||||||
request.calls = []
|
|
||||||
request.client_errorcount = 0
|
|
||||||
request.client_last_status_code = None
|
|
||||||
request.server_errorcount = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
context = None
|
|
||||||
|
|
||||||
if hasattr(protocol, 'prepare_response_body'):
|
|
||||||
prepare_response_body = protocol.prepare_response_body
|
|
||||||
else:
|
|
||||||
prepare_response_body = default_prepare_response_body
|
|
||||||
|
|
||||||
body = prepare_response_body(request, (
|
|
||||||
self._do_call(protocol, context)
|
|
||||||
for context in protocol.iter_calls(request)))
|
|
||||||
|
|
||||||
if isinstance(body, six.text_type):
|
|
||||||
res.text = body
|
|
||||||
else:
|
|
||||||
res.body = body
|
|
||||||
|
|
||||||
if len(request.calls) == 1:
|
|
||||||
if hasattr(protocol, 'get_response_status'):
|
|
||||||
res.status = protocol.get_response_status(request)
|
|
||||||
else:
|
|
||||||
if request.client_errorcount == 1:
|
|
||||||
res.status = request.client_last_status_code
|
|
||||||
elif request.client_errorcount:
|
|
||||||
res.status = 400
|
|
||||||
elif request.server_errorcount:
|
|
||||||
res.status = 500
|
|
||||||
else:
|
|
||||||
res.status = 200
|
|
||||||
else:
|
|
||||||
res.status = protocol.get_response_status(request)
|
|
||||||
res_content_type = protocol.get_response_contenttype(request)
|
|
||||||
except ClientSideError as e:
|
|
||||||
request.server_errorcount += 1
|
|
||||||
res.status = e.code
|
|
||||||
res.text = e.faultstring
|
|
||||||
except Exception:
|
|
||||||
infos = wsme.api.format_exception(sys.exc_info(), self._debug)
|
|
||||||
request.server_errorcount += 1
|
|
||||||
res.text = protocol.encode_error(context, infos)
|
|
||||||
res.status = 500
|
|
||||||
|
|
||||||
if res_content_type is None:
|
|
||||||
# Attempt to correctly guess what content-type we should return.
|
|
||||||
ctypes = [ct for ct in protocol.content_types if ct]
|
|
||||||
if ctypes:
|
|
||||||
res_content_type = request.accept.best_match(ctypes)
|
|
||||||
|
|
||||||
# If not we will attempt to convert the body to an accepted
|
|
||||||
# output format.
|
|
||||||
if res_content_type is None:
|
|
||||||
if "text/html" in request.accept:
|
|
||||||
res.text = self._html_format(res.body, protocol.content_types)
|
|
||||||
res_content_type = "text/html"
|
|
||||||
|
|
||||||
# TODO should we consider the encoding asked by
|
|
||||||
# the web browser ?
|
|
||||||
res.headers['Content-Type'] = "%s; charset=UTF-8" % res_content_type
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _lookup_function(self, path):
|
|
||||||
if not self._api:
|
|
||||||
self.getapi()
|
|
||||||
|
|
||||||
for fpath, f, fdef, args in self._api:
|
|
||||||
if path == fpath:
|
|
||||||
return f, fdef, args
|
|
||||||
raise UnknownFunction('/'.join(path))
|
|
||||||
|
|
||||||
def _html_format(self, content, content_types):
|
|
||||||
try:
|
|
||||||
from pygments import highlight
|
|
||||||
from pygments.lexers import get_lexer_for_mimetype
|
|
||||||
from pygments.formatters import HtmlFormatter
|
|
||||||
|
|
||||||
lexer = None
|
|
||||||
for ct in content_types:
|
|
||||||
try:
|
|
||||||
lexer = get_lexer_for_mimetype(ct)
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if lexer is None:
|
|
||||||
raise ValueError("No lexer found")
|
|
||||||
formatter = HtmlFormatter()
|
|
||||||
return html_body % dict(
|
|
||||||
css=formatter.get_style_defs(),
|
|
||||||
content=highlight(content, lexer, formatter).encode('utf8'))
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(
|
|
||||||
"Could not pygment the content because of the following "
|
|
||||||
"error :\n%s" % e)
|
|
||||||
return html_body % dict(
|
|
||||||
css='',
|
|
||||||
content=u('<pre>%s</pre>') %
|
|
||||||
content.replace(b('>'), b('>'))
|
|
||||||
.replace(b('<'), b('<')))
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from wsme.exc import MissingArgument
|
|
||||||
|
|
||||||
|
|
||||||
def check_arguments(funcdef, args, kw):
|
|
||||||
"""Check if some arguments are missing"""
|
|
||||||
assert len(args) == 0
|
|
||||||
for arg in funcdef.arguments:
|
|
||||||
if arg.mandatory and arg.name not in kw:
|
|
||||||
raise MissingArgument(arg.name)
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
from wsme import types
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def getdesc(root, host_url=''):
|
|
||||||
methods = {}
|
|
||||||
|
|
||||||
for path, funcdef in root.getapi():
|
|
||||||
method = funcdef.extra_options.get('method', None)
|
|
||||||
name = '_'.join(path)
|
|
||||||
if method is not None:
|
|
||||||
path = path[:-1]
|
|
||||||
else:
|
|
||||||
method = 'GET'
|
|
||||||
for argdef in funcdef.arguments:
|
|
||||||
if types.iscomplex(argdef.datatype) \
|
|
||||||
or types.isarray(argdef.datatype) \
|
|
||||||
or types.isdict(argdef.datatype):
|
|
||||||
method = 'POST'
|
|
||||||
break
|
|
||||||
|
|
||||||
required_params = []
|
|
||||||
optional_params = []
|
|
||||||
for argdef in funcdef.arguments:
|
|
||||||
if method == 'GET' and argdef.mandatory:
|
|
||||||
required_params.append(argdef.name)
|
|
||||||
else:
|
|
||||||
optional_params.append(argdef.name)
|
|
||||||
|
|
||||||
methods[name] = {
|
|
||||||
'method': method,
|
|
||||||
'path': '/'.join(path)
|
|
||||||
}
|
|
||||||
if required_params:
|
|
||||||
methods[name]['required_params'] = required_params
|
|
||||||
if optional_params:
|
|
||||||
methods[name]['optional_params'] = optional_params
|
|
||||||
if funcdef.doc:
|
|
||||||
methods[name]['documentation'] = funcdef.doc
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
for p in root.protocols:
|
|
||||||
if p.name == 'restxml':
|
|
||||||
formats.append('xml')
|
|
||||||
if p.name == 'restjson':
|
|
||||||
formats.append('json')
|
|
||||||
|
|
||||||
api = {
|
|
||||||
'base_url': host_url + root._webpath,
|
|
||||||
'version': '0.1',
|
|
||||||
'name': getattr(root, 'name', 'name'),
|
|
||||||
'authority': '',
|
|
||||||
'formats': [
|
|
||||||
'json',
|
|
||||||
'xml'
|
|
||||||
],
|
|
||||||
'methods': methods
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.dumps(api, indent=4)
|
|
||||||
@@ -1,720 +0,0 @@
|
|||||||
# coding=utf-8
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import warnings
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
import six
|
|
||||||
|
|
||||||
from six import u, b
|
|
||||||
|
|
||||||
from webtest import TestApp
|
|
||||||
|
|
||||||
from wsme import WSRoot, Unset
|
|
||||||
from wsme import expose, validate
|
|
||||||
import wsme.types
|
|
||||||
import wsme.utils
|
|
||||||
|
|
||||||
warnings.filterwarnings('ignore', module='webob.dec')
|
|
||||||
|
|
||||||
binarysample = b('\x00\xff\x43')
|
|
||||||
|
|
||||||
try:
|
|
||||||
1 / 0
|
|
||||||
except ZeroDivisionError as e:
|
|
||||||
zerodivisionerrormsg = str(e)
|
|
||||||
|
|
||||||
|
|
||||||
class CallException(RuntimeError):
|
|
||||||
def __init__(self, faultcode, faultstring, debuginfo):
|
|
||||||
self.faultcode = faultcode
|
|
||||||
self.faultstring = faultstring
|
|
||||||
self.debuginfo = debuginfo
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'faultcode=%s, faultstring=%s, debuginfo=%s' % (
|
|
||||||
self.faultcode, self.faultstring, self.debuginfo
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
myenumtype = wsme.types.Enum(wsme.types.bytes, 'v1', 'v2')
|
|
||||||
|
|
||||||
|
|
||||||
class NestedInner(object):
|
|
||||||
aint = int
|
|
||||||
|
|
||||||
def __init__(self, aint=None):
|
|
||||||
self.aint = aint
|
|
||||||
|
|
||||||
|
|
||||||
class NestedOuter(object):
|
|
||||||
inner = NestedInner
|
|
||||||
inner_array = wsme.types.wsattr([NestedInner])
|
|
||||||
inner_dict = {wsme.types.text: NestedInner}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.inner = NestedInner(0)
|
|
||||||
|
|
||||||
|
|
||||||
class NamedAttrsObject(object):
|
|
||||||
def __init__(self, v1=Unset, v2=Unset):
|
|
||||||
self.attr_1 = v1
|
|
||||||
self.attr_2 = v2
|
|
||||||
|
|
||||||
attr_1 = wsme.types.wsattr(int, name='attr.1')
|
|
||||||
attr_2 = wsme.types.wsattr(int, name='attr.2')
|
|
||||||
|
|
||||||
|
|
||||||
class CustomObject(object):
|
|
||||||
aint = int
|
|
||||||
name = wsme.types.text
|
|
||||||
|
|
||||||
|
|
||||||
class ExtendedInt(wsme.types.UserType):
|
|
||||||
basetype = int
|
|
||||||
name = "Extended integer"
|
|
||||||
|
|
||||||
|
|
||||||
class NestedInnerApi(object):
|
|
||||||
@expose(bool)
|
|
||||||
def deepfunction(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class NestedOuterApi(object):
|
|
||||||
inner = NestedInnerApi()
|
|
||||||
|
|
||||||
|
|
||||||
class ReturnTypes(object):
|
|
||||||
@expose(wsme.types.bytes)
|
|
||||||
def getbytes(self):
|
|
||||||
return b("astring")
|
|
||||||
|
|
||||||
@expose(wsme.types.text)
|
|
||||||
def gettext(self):
|
|
||||||
return u('\xe3\x81\xae')
|
|
||||||
|
|
||||||
@expose(int)
|
|
||||||
def getint(self):
|
|
||||||
return 2
|
|
||||||
|
|
||||||
@expose(float)
|
|
||||||
def getfloat(self):
|
|
||||||
return 3.14159265
|
|
||||||
|
|
||||||
@expose(decimal.Decimal)
|
|
||||||
def getdecimal(self):
|
|
||||||
return decimal.Decimal('3.14159265')
|
|
||||||
|
|
||||||
@expose(datetime.date)
|
|
||||||
def getdate(self):
|
|
||||||
return datetime.date(1994, 1, 26)
|
|
||||||
|
|
||||||
@expose(bool)
|
|
||||||
def getbooltrue(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@expose(bool)
|
|
||||||
def getboolfalse(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@expose(datetime.time)
|
|
||||||
def gettime(self):
|
|
||||||
return datetime.time(12, 0, 0)
|
|
||||||
|
|
||||||
@expose(datetime.datetime)
|
|
||||||
def getdatetime(self):
|
|
||||||
return datetime.datetime(1994, 1, 26, 12, 0, 0)
|
|
||||||
|
|
||||||
@expose(wsme.types.binary)
|
|
||||||
def getbinary(self):
|
|
||||||
return binarysample
|
|
||||||
|
|
||||||
@expose(NestedOuter)
|
|
||||||
def getnested(self):
|
|
||||||
n = NestedOuter()
|
|
||||||
return n
|
|
||||||
|
|
||||||
@expose([wsme.types.bytes])
|
|
||||||
def getbytesarray(self):
|
|
||||||
return [b("A"), b("B"), b("C")]
|
|
||||||
|
|
||||||
@expose([NestedOuter])
|
|
||||||
def getnestedarray(self):
|
|
||||||
return [NestedOuter(), NestedOuter()]
|
|
||||||
|
|
||||||
@expose({wsme.types.bytes: NestedOuter})
|
|
||||||
def getnesteddict(self):
|
|
||||||
return {b('a'): NestedOuter(), b('b'): NestedOuter()}
|
|
||||||
|
|
||||||
@expose(NestedOuter)
|
|
||||||
def getobjectarrayattribute(self):
|
|
||||||
obj = NestedOuter()
|
|
||||||
obj.inner_array = [NestedInner(12), NestedInner(13)]
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@expose(NestedOuter)
|
|
||||||
def getobjectdictattribute(self):
|
|
||||||
obj = NestedOuter()
|
|
||||||
obj.inner_dict = {
|
|
||||||
'12': NestedInner(12),
|
|
||||||
'13': NestedInner(13)
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@expose(myenumtype)
|
|
||||||
def getenum(self):
|
|
||||||
return b('v2')
|
|
||||||
|
|
||||||
@expose(NamedAttrsObject)
|
|
||||||
def getnamedattrsobj(self):
|
|
||||||
return NamedAttrsObject(5, 6)
|
|
||||||
|
|
||||||
|
|
||||||
class ArgTypes(object):
|
|
||||||
def assertEqual(self, a, b):
|
|
||||||
if not (a == b):
|
|
||||||
raise AssertionError('%s != %s' % (a, b))
|
|
||||||
|
|
||||||
def assertIsInstance(self, value, v_type):
|
|
||||||
assert isinstance(value, v_type), ("%s is not instance of type %s" %
|
|
||||||
(value, v_type))
|
|
||||||
|
|
||||||
@expose(wsme.types.bytes)
|
|
||||||
@validate(wsme.types.bytes)
|
|
||||||
def setbytes(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), wsme.types.bytes)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(wsme.types.text)
|
|
||||||
@validate(wsme.types.text)
|
|
||||||
def settext(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), wsme.types.text)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(wsme.types.text)
|
|
||||||
@validate(wsme.types.text)
|
|
||||||
def settextnone(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), type(None))
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(bool)
|
|
||||||
@validate(bool)
|
|
||||||
def setbool(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), bool)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(int)
|
|
||||||
@validate(int)
|
|
||||||
def setint(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), int)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(float)
|
|
||||||
@validate(float)
|
|
||||||
def setfloat(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), float)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(decimal.Decimal)
|
|
||||||
@validate(decimal.Decimal)
|
|
||||||
def setdecimal(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), decimal.Decimal)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(datetime.date)
|
|
||||||
@validate(datetime.date)
|
|
||||||
def setdate(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), datetime.date)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(datetime.time)
|
|
||||||
@validate(datetime.time)
|
|
||||||
def settime(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), datetime.time)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(datetime.datetime)
|
|
||||||
@validate(datetime.datetime)
|
|
||||||
def setdatetime(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), datetime.datetime)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(wsme.types.binary)
|
|
||||||
@validate(wsme.types.binary)
|
|
||||||
def setbinary(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), six.binary_type)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose([wsme.types.bytes])
|
|
||||||
@validate([wsme.types.bytes])
|
|
||||||
def setbytesarray(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), list)
|
|
||||||
self.assertEqual(type(value[0]), wsme.types.bytes)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose([wsme.types.text])
|
|
||||||
@validate([wsme.types.text])
|
|
||||||
def settextarray(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), list)
|
|
||||||
self.assertEqual(type(value[0]), wsme.types.text)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose([datetime.datetime])
|
|
||||||
@validate([datetime.datetime])
|
|
||||||
def setdatetimearray(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), list)
|
|
||||||
self.assertEqual(type(value[0]), datetime.datetime)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(NestedOuter)
|
|
||||||
@validate(NestedOuter)
|
|
||||||
def setnested(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), NestedOuter)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose([NestedOuter])
|
|
||||||
@validate([NestedOuter])
|
|
||||||
def setnestedarray(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), list)
|
|
||||||
self.assertEqual(type(value[0]), NestedOuter)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose({wsme.types.bytes: NestedOuter})
|
|
||||||
@validate({wsme.types.bytes: NestedOuter})
|
|
||||||
def setnesteddict(self, value):
|
|
||||||
print(repr(value))
|
|
||||||
self.assertEqual(type(value), dict)
|
|
||||||
self.assertEqual(type(list(value.keys())[0]), wsme.types.bytes)
|
|
||||||
self.assertEqual(type(list(value.values())[0]), NestedOuter)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(myenumtype)
|
|
||||||
@validate(myenumtype)
|
|
||||||
def setenum(self, value):
|
|
||||||
print(value)
|
|
||||||
self.assertEqual(type(value), wsme.types.bytes)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(NamedAttrsObject)
|
|
||||||
@validate(NamedAttrsObject)
|
|
||||||
def setnamedattrsobj(self, value):
|
|
||||||
print(value)
|
|
||||||
self.assertEqual(type(value), NamedAttrsObject)
|
|
||||||
self.assertEqual(value.attr_1, 10)
|
|
||||||
self.assertEqual(value.attr_2, 20)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(CustomObject)
|
|
||||||
@validate(CustomObject)
|
|
||||||
def setcustomobject(self, value):
|
|
||||||
self.assertIsInstance(value, CustomObject)
|
|
||||||
self.assertIsInstance(value.name, wsme.types.text)
|
|
||||||
self.assertIsInstance(value.aint, int)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@expose(ExtendedInt())
|
|
||||||
@validate(ExtendedInt())
|
|
||||||
def setextendedint(self, value):
|
|
||||||
self.assertEqual(isinstance(value, ExtendedInt.basetype), True)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class BodyTypes(object):
|
|
||||||
def assertEqual(self, a, b):
|
|
||||||
if not (a == b):
|
|
||||||
raise AssertionError('%s != %s' % (a, b))
|
|
||||||
|
|
||||||
@expose(int, body={wsme.types.text: int})
|
|
||||||
@validate(int)
|
|
||||||
def setdict(self, body):
|
|
||||||
print(body)
|
|
||||||
self.assertEqual(type(body), dict)
|
|
||||||
self.assertEqual(type(body['test']), int)
|
|
||||||
self.assertEqual(body['test'], 10)
|
|
||||||
return body['test']
|
|
||||||
|
|
||||||
@expose(int, body=[int])
|
|
||||||
@validate(int)
|
|
||||||
def setlist(self, body):
|
|
||||||
print(body)
|
|
||||||
self.assertEqual(type(body), list)
|
|
||||||
self.assertEqual(type(body[0]), int)
|
|
||||||
self.assertEqual(body[0], 10)
|
|
||||||
return body[0]
|
|
||||||
|
|
||||||
|
|
||||||
class WithErrors(object):
|
|
||||||
@expose()
|
|
||||||
def divide_by_zero(self):
|
|
||||||
1 / 0
|
|
||||||
|
|
||||||
|
|
||||||
class MiscFunctions(object):
|
|
||||||
@expose(int)
|
|
||||||
@validate(int, int)
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
|
|
||||||
class WSTestRoot(WSRoot):
|
|
||||||
argtypes = ArgTypes()
|
|
||||||
returntypes = ReturnTypes()
|
|
||||||
bodytypes = BodyTypes()
|
|
||||||
witherrors = WithErrors()
|
|
||||||
nested = NestedOuterApi()
|
|
||||||
misc = MiscFunctions()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self._touched = False
|
|
||||||
|
|
||||||
@expose()
|
|
||||||
def touch(self):
|
|
||||||
self._touched = True
|
|
||||||
|
|
||||||
|
|
||||||
class ProtocolTestCase(unittest.TestCase):
|
|
||||||
protocol_options = {}
|
|
||||||
|
|
||||||
def assertTypedEquals(self, a, b, convert):
|
|
||||||
if isinstance(a, six.string_types):
|
|
||||||
a = convert(a)
|
|
||||||
if isinstance(b, six.string_types):
|
|
||||||
b = convert(b)
|
|
||||||
self.assertEqual(a, b)
|
|
||||||
|
|
||||||
def assertDateEquals(self, a, b):
|
|
||||||
self.assertTypedEquals(a, b, wsme.utils.parse_isodate)
|
|
||||||
|
|
||||||
def assertTimeEquals(self, a, b):
|
|
||||||
self.assertTypedEquals(a, b, wsme.utils.parse_isotime)
|
|
||||||
|
|
||||||
def assertDateTimeEquals(self, a, b):
|
|
||||||
self.assertTypedEquals(a, b, wsme.utils.parse_isodatetime)
|
|
||||||
|
|
||||||
def assertIntEquals(self, a, b):
|
|
||||||
self.assertTypedEquals(a, b, int)
|
|
||||||
|
|
||||||
def assertFloatEquals(self, a, b):
|
|
||||||
self.assertTypedEquals(a, b, float)
|
|
||||||
|
|
||||||
def assertDecimalEquals(self, a, b):
|
|
||||||
self.assertTypedEquals(a, b, decimal.Decimal)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
if self.__class__.__name__ != 'ProtocolTestCase':
|
|
||||||
self.root = WSTestRoot()
|
|
||||||
self.root.getapi()
|
|
||||||
self.root.addprotocol(self.protocol, **self.protocol_options)
|
|
||||||
|
|
||||||
self.app = TestApp(self.root.wsgiapp())
|
|
||||||
|
|
||||||
def test_invalid_path(self):
|
|
||||||
try:
|
|
||||||
res = self.call('invalid_function')
|
|
||||||
print(res)
|
|
||||||
assert "No error raised"
|
|
||||||
except CallException as e:
|
|
||||||
self.assertEqual(e.faultcode, 'Client')
|
|
||||||
self.assertEqual(e.faultstring.lower(),
|
|
||||||
u('unknown function name: invalid_function'))
|
|
||||||
|
|
||||||
def test_serverside_error(self):
|
|
||||||
try:
|
|
||||||
res = self.call('witherrors/divide_by_zero')
|
|
||||||
print(res)
|
|
||||||
assert "No error raised"
|
|
||||||
except CallException as e:
|
|
||||||
self.assertEqual(e.faultcode, 'Server')
|
|
||||||
self.assertEqual(e.faultstring, zerodivisionerrormsg)
|
|
||||||
assert e.debuginfo is not None
|
|
||||||
|
|
||||||
def test_serverside_error_nodebug(self):
|
|
||||||
self.root._debug = False
|
|
||||||
try:
|
|
||||||
res = self.call('witherrors/divide_by_zero')
|
|
||||||
print(res)
|
|
||||||
assert "No error raised"
|
|
||||||
except CallException as e:
|
|
||||||
self.assertEqual(e.faultcode, 'Server')
|
|
||||||
self.assertEqual(e.faultstring, zerodivisionerrormsg)
|
|
||||||
assert e.debuginfo is None
|
|
||||||
|
|
||||||
def test_touch(self):
|
|
||||||
r = self.call('touch')
|
|
||||||
assert r is None, r
|
|
||||||
|
|
||||||
def test_return_bytes(self):
|
|
||||||
r = self.call('returntypes/getbytes', _rt=wsme.types.bytes)
|
|
||||||
self.assertEqual(r, b('astring'))
|
|
||||||
|
|
||||||
def test_return_text(self):
|
|
||||||
r = self.call('returntypes/gettext', _rt=wsme.types.text)
|
|
||||||
self.assertEqual(r, u('\xe3\x81\xae'))
|
|
||||||
|
|
||||||
def test_return_int(self):
|
|
||||||
r = self.call('returntypes/getint')
|
|
||||||
self.assertIntEquals(r, 2)
|
|
||||||
|
|
||||||
def test_return_float(self):
|
|
||||||
r = self.call('returntypes/getfloat')
|
|
||||||
self.assertFloatEquals(r, 3.14159265)
|
|
||||||
|
|
||||||
def test_return_decimal(self):
|
|
||||||
r = self.call('returntypes/getdecimal')
|
|
||||||
self.assertDecimalEquals(r, '3.14159265')
|
|
||||||
|
|
||||||
def test_return_bool_true(self):
|
|
||||||
r = self.call('returntypes/getbooltrue', _rt=bool)
|
|
||||||
assert r
|
|
||||||
|
|
||||||
def test_return_bool_false(self):
|
|
||||||
r = self.call('returntypes/getboolfalse', _rt=bool)
|
|
||||||
assert not r
|
|
||||||
|
|
||||||
def test_return_date(self):
|
|
||||||
r = self.call('returntypes/getdate')
|
|
||||||
self.assertDateEquals(r, datetime.date(1994, 1, 26))
|
|
||||||
|
|
||||||
def test_return_time(self):
|
|
||||||
r = self.call('returntypes/gettime')
|
|
||||||
self.assertTimeEquals(r, datetime.time(12))
|
|
||||||
|
|
||||||
def test_return_datetime(self):
|
|
||||||
r = self.call('returntypes/getdatetime')
|
|
||||||
self.assertDateTimeEquals(r, datetime.datetime(1994, 1, 26, 12))
|
|
||||||
|
|
||||||
def test_return_binary(self):
|
|
||||||
r = self.call('returntypes/getbinary', _rt=wsme.types.binary)
|
|
||||||
self.assertEqual(r, binarysample)
|
|
||||||
|
|
||||||
def test_return_nested(self):
|
|
||||||
r = self.call('returntypes/getnested', _rt=NestedOuter)
|
|
||||||
self.assertEqual(r, {'inner': {'aint': 0}})
|
|
||||||
|
|
||||||
def test_return_bytesarray(self):
|
|
||||||
r = self.call('returntypes/getbytesarray', _rt=[six.binary_type])
|
|
||||||
self.assertEqual(r, [b('A'), b('B'), b('C')])
|
|
||||||
|
|
||||||
def test_return_nestedarray(self):
|
|
||||||
r = self.call('returntypes/getnestedarray', _rt=[NestedOuter])
|
|
||||||
self.assertEqual(r, [{'inner': {'aint': 0}}, {'inner': {'aint': 0}}])
|
|
||||||
|
|
||||||
def test_return_nesteddict(self):
|
|
||||||
r = self.call('returntypes/getnesteddict',
|
|
||||||
_rt={wsme.types.bytes: NestedOuter})
|
|
||||||
self.assertEqual(r, {
|
|
||||||
b('a'): {'inner': {'aint': 0}},
|
|
||||||
b('b'): {'inner': {'aint': 0}}
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_return_objectarrayattribute(self):
|
|
||||||
r = self.call('returntypes/getobjectarrayattribute', _rt=NestedOuter)
|
|
||||||
self.assertEqual(r, {
|
|
||||||
'inner': {'aint': 0},
|
|
||||||
'inner_array': [{'aint': 12}, {'aint': 13}]
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_return_objectdictattribute(self):
|
|
||||||
r = self.call('returntypes/getobjectdictattribute', _rt=NestedOuter)
|
|
||||||
self.assertEqual(r, {
|
|
||||||
'inner': {'aint': 0},
|
|
||||||
'inner_dict': {
|
|
||||||
'12': {'aint': 12},
|
|
||||||
'13': {'aint': 13}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_return_enum(self):
|
|
||||||
r = self.call('returntypes/getenum', _rt=myenumtype)
|
|
||||||
self.assertEqual(r, b('v2'), r)
|
|
||||||
|
|
||||||
def test_return_namedattrsobj(self):
|
|
||||||
r = self.call('returntypes/getnamedattrsobj', _rt=NamedAttrsObject)
|
|
||||||
self.assertEqual(r, {'attr.1': 5, 'attr.2': 6})
|
|
||||||
|
|
||||||
def test_setbytes(self):
|
|
||||||
assert self.call('argtypes/setbytes', value=b('astring'),
|
|
||||||
_rt=wsme.types.bytes) == b('astring')
|
|
||||||
|
|
||||||
def test_settext(self):
|
|
||||||
assert self.call('argtypes/settext', value=u('\xe3\x81\xae'),
|
|
||||||
_rt=wsme.types.text) == u('\xe3\x81\xae')
|
|
||||||
|
|
||||||
def test_settext_empty(self):
|
|
||||||
assert self.call('argtypes/settext', value=u(''),
|
|
||||||
_rt=wsme.types.text) == u('')
|
|
||||||
|
|
||||||
def test_settext_none(self):
|
|
||||||
self.assertEqual(
|
|
||||||
None,
|
|
||||||
self.call('argtypes/settextnone', value=None, _rt=wsme.types.text)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_setint(self):
|
|
||||||
r = self.call('argtypes/setint', value=3, _rt=int)
|
|
||||||
self.assertEqual(r, 3)
|
|
||||||
|
|
||||||
def test_setfloat(self):
|
|
||||||
assert self.call('argtypes/setfloat', value=3.54,
|
|
||||||
_rt=float) == 3.54
|
|
||||||
|
|
||||||
def test_setbool_true(self):
|
|
||||||
r = self.call('argtypes/setbool', value=True, _rt=bool)
|
|
||||||
assert r
|
|
||||||
|
|
||||||
def test_setbool_false(self):
|
|
||||||
r = self.call('argtypes/setbool', value=False, _rt=bool)
|
|
||||||
assert not r
|
|
||||||
|
|
||||||
def test_setdecimal(self):
|
|
||||||
value = decimal.Decimal('3.14')
|
|
||||||
assert self.call('argtypes/setdecimal', value=value,
|
|
||||||
_rt=decimal.Decimal) == value
|
|
||||||
|
|
||||||
def test_setdate(self):
|
|
||||||
value = datetime.date(2008, 4, 6)
|
|
||||||
r = self.call('argtypes/setdate', value=value,
|
|
||||||
_rt=datetime.date)
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_settime(self):
|
|
||||||
value = datetime.time(12, 12, 15)
|
|
||||||
r = self.call('argtypes/settime', value=value,
|
|
||||||
_rt=datetime.time)
|
|
||||||
self.assertEqual(r, datetime.time(12, 12, 15))
|
|
||||||
|
|
||||||
def test_setdatetime(self):
|
|
||||||
value = datetime.datetime(2008, 4, 6, 12, 12, 15)
|
|
||||||
r = self.call('argtypes/setdatetime', value=value,
|
|
||||||
_rt=datetime.datetime)
|
|
||||||
self.assertEqual(r, datetime.datetime(2008, 4, 6, 12, 12, 15))
|
|
||||||
|
|
||||||
def test_setbinary(self):
|
|
||||||
value = binarysample
|
|
||||||
r = self.call('argtypes/setbinary', value=(value, wsme.types.binary),
|
|
||||||
_rt=wsme.types.binary) == value
|
|
||||||
print(r)
|
|
||||||
|
|
||||||
def test_setnested(self):
|
|
||||||
value = {'inner': {'aint': 54}}
|
|
||||||
r = self.call('argtypes/setnested',
|
|
||||||
value=(value, NestedOuter),
|
|
||||||
_rt=NestedOuter)
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_setnested_nullobj(self):
|
|
||||||
value = {'inner': None}
|
|
||||||
r = self.call(
|
|
||||||
'argtypes/setnested',
|
|
||||||
value=(value, NestedOuter),
|
|
||||||
_rt=NestedOuter
|
|
||||||
)
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_setbytesarray(self):
|
|
||||||
value = [b("1"), b("2"), b("three")]
|
|
||||||
r = self.call('argtypes/setbytesarray',
|
|
||||||
value=(value, [wsme.types.bytes]),
|
|
||||||
_rt=[wsme.types.bytes])
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_settextarray(self):
|
|
||||||
value = [u("1")]
|
|
||||||
r = self.call('argtypes/settextarray',
|
|
||||||
value=(value, [wsme.types.text]),
|
|
||||||
_rt=[wsme.types.text])
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_setdatetimearray(self):
|
|
||||||
value = [
|
|
||||||
datetime.datetime(2008, 3, 6, 12, 12, 15),
|
|
||||||
datetime.datetime(2008, 4, 6, 2, 12, 15),
|
|
||||||
]
|
|
||||||
r = self.call('argtypes/setdatetimearray',
|
|
||||||
value=(value, [datetime.datetime]),
|
|
||||||
_rt=[datetime.datetime])
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_setnestedarray(self):
|
|
||||||
value = [
|
|
||||||
{'inner': {'aint': 54}},
|
|
||||||
{'inner': {'aint': 55}},
|
|
||||||
]
|
|
||||||
r = self.call('argtypes/setnestedarray',
|
|
||||||
value=(value, [NestedOuter]),
|
|
||||||
_rt=[NestedOuter])
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_setnesteddict(self):
|
|
||||||
value = {
|
|
||||||
b('o1'): {'inner': {'aint': 54}},
|
|
||||||
b('o2'): {'inner': {'aint': 55}},
|
|
||||||
}
|
|
||||||
r = self.call('argtypes/setnesteddict',
|
|
||||||
value=(value, {six.binary_type: NestedOuter}),
|
|
||||||
_rt={six.binary_type: NestedOuter})
|
|
||||||
print(r)
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_setenum(self):
|
|
||||||
value = b('v1')
|
|
||||||
r = self.call('argtypes/setenum', value=value,
|
|
||||||
_rt=myenumtype)
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_setnamedattrsobj(self):
|
|
||||||
value = {'attr.1': 10, 'attr.2': 20}
|
|
||||||
r = self.call('argtypes/setnamedattrsobj',
|
|
||||||
value=(value, NamedAttrsObject),
|
|
||||||
_rt=NamedAttrsObject)
|
|
||||||
self.assertEqual(r, value)
|
|
||||||
|
|
||||||
def test_nested_api(self):
|
|
||||||
r = self.call('nested/inner/deepfunction', _rt=bool)
|
|
||||||
assert r is True
|
|
||||||
|
|
||||||
def test_missing_argument(self):
|
|
||||||
try:
|
|
||||||
r = self.call('argtypes/setdatetime')
|
|
||||||
print(r)
|
|
||||||
assert "No error raised"
|
|
||||||
except CallException as e:
|
|
||||||
self.assertEqual(e.faultcode, 'Client')
|
|
||||||
self.assertEqual(e.faultstring, u('Missing argument: "value"'))
|
|
||||||
|
|
||||||
def test_misc_multiply(self):
|
|
||||||
self.assertEqual(self.call('misc/multiply', a=5, b=2, _rt=int), 10)
|
|
||||||
|
|
||||||
def test_html_format(self):
|
|
||||||
res = self.call('argtypes/setdatetime', _accept="text/html",
|
|
||||||
_no_result_decode=True)
|
|
||||||
self.assertEqual(res.content_type, 'text/html')
|
|
||||||
|
|
||||||
|
|
||||||
class RestOnlyProtocolTestCase(ProtocolTestCase):
|
|
||||||
def test_body_list(self):
|
|
||||||
r = self.call('bodytypes/setlist', body=([10], [int]), _rt=int)
|
|
||||||
self.assertEqual(r, 10)
|
|
||||||
|
|
||||||
def test_body_dict(self):
|
|
||||||
r = self.call('bodytypes/setdict',
|
|
||||||
body=({'test': 10}, {wsme.types.text: int}),
|
|
||||||
_rt=int)
|
|
||||||
self.assertEqual(r, 10)
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
# encoding=utf8
|
|
||||||
|
|
||||||
from six import b
|
|
||||||
|
|
||||||
try:
|
|
||||||
import unittest2 as unittest
|
|
||||||
except ImportError:
|
|
||||||
import unittest
|
|
||||||
import webtest
|
|
||||||
|
|
||||||
from wsme import WSRoot, expose, validate
|
|
||||||
from wsme.rest import scan_api
|
|
||||||
from wsme import types
|
|
||||||
from wsme import exc
|
|
||||||
import wsme.api as wsme_api
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
from wsme.tests.test_protocols import DummyProtocol
|
|
||||||
|
|
||||||
|
|
||||||
class TestController(unittest.TestCase):
|
|
||||||
def test_expose(self):
|
|
||||||
class MyWS(WSRoot):
|
|
||||||
@expose(int)
|
|
||||||
def getint(self):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
assert MyWS.getint._wsme_definition.return_type == int
|
|
||||||
|
|
||||||
def test_validate(self):
|
|
||||||
class ComplexType(object):
|
|
||||||
attr = int
|
|
||||||
|
|
||||||
class MyWS(object):
|
|
||||||
@expose(int)
|
|
||||||
@validate(int, int, int)
|
|
||||||
def add(self, a, b, c=0):
|
|
||||||
return a + b + c
|
|
||||||
|
|
||||||
@expose(bool)
|
|
||||||
@validate(ComplexType)
|
|
||||||
def setcplx(self, obj):
|
|
||||||
pass
|
|
||||||
|
|
||||||
MyWS.add._wsme_definition.resolve_types(wsme.types.registry)
|
|
||||||
MyWS.setcplx._wsme_definition.resolve_types(wsme.types.registry)
|
|
||||||
args = MyWS.add._wsme_definition.arguments
|
|
||||||
|
|
||||||
assert args[0].name == 'a'
|
|
||||||
assert args[0].datatype == int
|
|
||||||
assert args[0].mandatory
|
|
||||||
assert args[0].default is None
|
|
||||||
|
|
||||||
assert args[1].name == 'b'
|
|
||||||
assert args[1].datatype == int
|
|
||||||
assert args[1].mandatory
|
|
||||||
assert args[1].default is None
|
|
||||||
|
|
||||||
assert args[2].name == 'c'
|
|
||||||
assert args[2].datatype == int
|
|
||||||
assert not args[2].mandatory
|
|
||||||
assert args[2].default == 0
|
|
||||||
|
|
||||||
assert types.iscomplex(ComplexType)
|
|
||||||
|
|
||||||
def test_validate_enum_with_none(self):
|
|
||||||
class Version(object):
|
|
||||||
number = types.Enum(str, 'v1', 'v2', None)
|
|
||||||
|
|
||||||
class MyWS(WSRoot):
|
|
||||||
@expose(str)
|
|
||||||
@validate(Version)
|
|
||||||
def setcplx(self, version):
|
|
||||||
pass
|
|
||||||
|
|
||||||
r = MyWS(['restjson'])
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
res = app.post_json('/setcplx', params={'version': {'number': 'arf'}},
|
|
||||||
expect_errors=True,
|
|
||||||
headers={'Accept': 'application/json'})
|
|
||||||
self.assertTrue(
|
|
||||||
res.json_body['faultstring'].startswith(
|
|
||||||
"Invalid input for field/attribute number. Value: 'arf'. \
|
|
||||||
Value should be one of:"))
|
|
||||||
self.assertIn('v1', res.json_body['faultstring'])
|
|
||||||
self.assertIn('v2', res.json_body['faultstring'])
|
|
||||||
self.assertIn('None', res.json_body['faultstring'])
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_validate_enum_with_wrong_type(self):
|
|
||||||
class Version(object):
|
|
||||||
number = types.Enum(str, 'v1', 'v2', None)
|
|
||||||
|
|
||||||
class MyWS(WSRoot):
|
|
||||||
@expose(str)
|
|
||||||
@validate(Version)
|
|
||||||
def setcplx(self, version):
|
|
||||||
pass
|
|
||||||
|
|
||||||
r = MyWS(['restjson'])
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
res = app.post_json('/setcplx', params={'version': {'number': 1}},
|
|
||||||
expect_errors=True,
|
|
||||||
headers={'Accept': 'application/json'})
|
|
||||||
self.assertTrue(
|
|
||||||
res.json_body['faultstring'].startswith(
|
|
||||||
"Invalid input for field/attribute number. Value: '1'. \
|
|
||||||
Value should be one of:"))
|
|
||||||
self.assertIn('v1', res.json_body['faultstring'])
|
|
||||||
self.assertIn('v2', res.json_body['faultstring'])
|
|
||||||
self.assertIn('None', res.json_body['faultstring'])
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_scan_api(self):
|
|
||||||
class NS(object):
|
|
||||||
@expose(int)
|
|
||||||
@validate(int, int)
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
ns = NS()
|
|
||||||
|
|
||||||
r = MyRoot()
|
|
||||||
|
|
||||||
api = list(scan_api(r))
|
|
||||||
assert len(api) == 1
|
|
||||||
path, fd, args = api[0]
|
|
||||||
assert path == ['ns', 'multiply']
|
|
||||||
assert fd._wsme_definition.name == 'multiply'
|
|
||||||
assert args == []
|
|
||||||
|
|
||||||
def test_scan_subclass(self):
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
class SubClass(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
r = MyRoot()
|
|
||||||
api = list(scan_api(r))
|
|
||||||
|
|
||||||
assert len(api) == 0
|
|
||||||
|
|
||||||
def test_scan_api_too_deep(self):
|
|
||||||
class Loop(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
l = Loop()
|
|
||||||
for i in range(0, 21):
|
|
||||||
nl = Loop()
|
|
||||||
nl.l = l
|
|
||||||
l = nl
|
|
||||||
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
loop = l
|
|
||||||
|
|
||||||
r = MyRoot()
|
|
||||||
|
|
||||||
try:
|
|
||||||
list(scan_api(r))
|
|
||||||
assert False, "ValueError not raised"
|
|
||||||
except ValueError as e:
|
|
||||||
assert str(e).startswith("Path is too long")
|
|
||||||
|
|
||||||
def test_handle_request(self):
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
@expose()
|
|
||||||
def touch(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
p = DummyProtocol()
|
|
||||||
r = MyRoot(protocols=[p])
|
|
||||||
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
|
|
||||||
res = app.get('/')
|
|
||||||
|
|
||||||
assert p.lastreq.path == '/'
|
|
||||||
assert p.hits == 1
|
|
||||||
|
|
||||||
res = app.get('/touch?wsmeproto=dummy')
|
|
||||||
|
|
||||||
assert p.lastreq.path == '/touch'
|
|
||||||
assert p.hits == 2
|
|
||||||
|
|
||||||
class NoPathProto(DummyProtocol):
|
|
||||||
def extract_path(self, request):
|
|
||||||
return None
|
|
||||||
|
|
||||||
p = NoPathProto()
|
|
||||||
r = MyRoot(protocols=[p])
|
|
||||||
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
|
|
||||||
res = app.get('/', expect_errors=True)
|
|
||||||
print(res.status, res.body)
|
|
||||||
assert res.status_int == 400
|
|
||||||
|
|
||||||
def test_no_available_protocol(self):
|
|
||||||
r = WSRoot()
|
|
||||||
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
|
|
||||||
res = app.get('/', expect_errors=True)
|
|
||||||
print(res.status_int)
|
|
||||||
assert res.status_int == 406
|
|
||||||
print(res.body)
|
|
||||||
assert res.body.find(
|
|
||||||
b("None of the following protocols can handle this request")) != -1
|
|
||||||
|
|
||||||
def test_return_content_type_guess(self):
|
|
||||||
class DummierProto(DummyProtocol):
|
|
||||||
content_types = ['text/xml', 'text/plain']
|
|
||||||
|
|
||||||
r = WSRoot([DummierProto()])
|
|
||||||
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
|
|
||||||
res = app.get('/', expect_errors=True, headers={
|
|
||||||
'Accept': 'text/xml,q=0.8'})
|
|
||||||
assert res.status_int == 400
|
|
||||||
assert res.content_type == 'text/xml', res.content_type
|
|
||||||
|
|
||||||
res = app.get('/', expect_errors=True, headers={
|
|
||||||
'Accept': 'text/plain'})
|
|
||||||
assert res.status_int == 400
|
|
||||||
assert res.content_type == 'text/plain', res.content_type
|
|
||||||
|
|
||||||
def test_double_expose(self):
|
|
||||||
try:
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
@expose()
|
|
||||||
@expose()
|
|
||||||
def atest(self):
|
|
||||||
pass
|
|
||||||
assert False, "A ValueError should have been raised"
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_multiple_expose(self):
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
mul_int = expose(int, int, int, wrap=True)(multiply)
|
|
||||||
|
|
||||||
mul_float = expose(
|
|
||||||
float, float, float,
|
|
||||||
wrap=True)(multiply)
|
|
||||||
|
|
||||||
mul_string = expose(
|
|
||||||
wsme.types.text, wsme.types.text, int,
|
|
||||||
wrap=True)(multiply)
|
|
||||||
|
|
||||||
r = MyRoot(['restjson'])
|
|
||||||
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
|
|
||||||
res = app.get('/mul_int?a=2&b=5', headers={
|
|
||||||
'Accept': 'application/json'
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertEqual(res.body, b('10'))
|
|
||||||
|
|
||||||
res = app.get('/mul_float?a=1.2&b=2.9', headers={
|
|
||||||
'Accept': 'application/json'
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertEqual(res.body, b('3.48'))
|
|
||||||
|
|
||||||
res = app.get('/mul_string?a=hello&b=2', headers={
|
|
||||||
'Accept': 'application/json'
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertEqual(res.body, b('"hellohello"'))
|
|
||||||
|
|
||||||
def test_wsattr_mandatory(self):
|
|
||||||
class ComplexType(object):
|
|
||||||
attr = wsme.types.wsattr(int, mandatory=True)
|
|
||||||
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
@expose(int, body=ComplexType)
|
|
||||||
@validate(ComplexType)
|
|
||||||
def clx(self, a):
|
|
||||||
return a.attr
|
|
||||||
|
|
||||||
r = MyRoot(['restjson'])
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
res = app.post_json('/clx', params={}, expect_errors=True,
|
|
||||||
headers={'Accept': 'application/json'})
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_wsattr_readonly(self):
|
|
||||||
class ComplexType(object):
|
|
||||||
attr = wsme.types.wsattr(int, readonly=True)
|
|
||||||
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
@expose(int, body=ComplexType)
|
|
||||||
@validate(ComplexType)
|
|
||||||
def clx(self, a):
|
|
||||||
return a.attr
|
|
||||||
|
|
||||||
r = MyRoot(['restjson'])
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
res = app.post_json('/clx', params={'attr': 1005}, expect_errors=True,
|
|
||||||
headers={'Accept': 'application/json'})
|
|
||||||
self.assertIn('Cannot set read only field.',
|
|
||||||
res.json_body['faultstring'])
|
|
||||||
self.assertIn('1005', res.json_body['faultstring'])
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_wsattr_default(self):
|
|
||||||
class ComplexType(object):
|
|
||||||
attr = wsme.types.wsattr(wsme.types.Enum(str, 'or', 'and'),
|
|
||||||
default='and')
|
|
||||||
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
@expose(int)
|
|
||||||
@validate(ComplexType)
|
|
||||||
def clx(self, a):
|
|
||||||
return a.attr
|
|
||||||
|
|
||||||
r = MyRoot(['restjson'])
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
res = app.post_json('/clx', params={}, expect_errors=True,
|
|
||||||
headers={'Accept': 'application/json'})
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_wsproperty_mandatory(self):
|
|
||||||
class ComplexType(object):
|
|
||||||
def foo(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
attr = wsme.types.wsproperty(int, foo, foo, mandatory=True)
|
|
||||||
|
|
||||||
class MyRoot(WSRoot):
|
|
||||||
@expose(int, body=ComplexType)
|
|
||||||
@validate(ComplexType)
|
|
||||||
def clx(self, a):
|
|
||||||
return a.attr
|
|
||||||
|
|
||||||
r = MyRoot(['restjson'])
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
res = app.post_json('/clx', params={}, expect_errors=True,
|
|
||||||
headers={'Accept': 'application/json'})
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_validate_enum_mandatory(self):
|
|
||||||
class Version(object):
|
|
||||||
number = wsme.types.wsattr(wsme.types.Enum(str, 'v1', 'v2'),
|
|
||||||
mandatory=True)
|
|
||||||
|
|
||||||
class MyWS(WSRoot):
|
|
||||||
@expose(str)
|
|
||||||
@validate(Version)
|
|
||||||
def setcplx(self, version):
|
|
||||||
pass
|
|
||||||
|
|
||||||
r = MyWS(['restjson'])
|
|
||||||
app = webtest.TestApp(r.wsgiapp())
|
|
||||||
res = app.post_json('/setcplx', params={'version': {}},
|
|
||||||
expect_errors=True,
|
|
||||||
headers={'Accept': 'application/json'})
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
|
|
||||||
class TestFunctionDefinition(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_get_arg(self):
|
|
||||||
def myfunc(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
fd = wsme_api.FunctionDefinition(wsme_api.FunctionDefinition)
|
|
||||||
fd.arguments.append(wsme_api.FunctionArgument('a', int, True, None))
|
|
||||||
|
|
||||||
assert fd.get_arg('a').datatype is int
|
|
||||||
assert fd.get_arg('b') is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormatException(unittest.TestCase):
|
|
||||||
|
|
||||||
def _test_format_exception(self, exception, debug=False):
|
|
||||||
fake_exc_info = (None, exception, None)
|
|
||||||
return wsme_api.format_exception(fake_exc_info, debug=debug)
|
|
||||||
|
|
||||||
def test_format_client_exception(self):
|
|
||||||
faultstring = 'boom'
|
|
||||||
ret = self._test_format_exception(exc.ClientSideError(faultstring))
|
|
||||||
self.assertIsNone(ret['debuginfo'])
|
|
||||||
self.assertEqual('Client', ret['faultcode'])
|
|
||||||
self.assertEqual(faultstring, ret['faultstring'])
|
|
||||||
|
|
||||||
def test_format_client_exception_unicode(self):
|
|
||||||
faultstring = u'\xc3\xa3o'
|
|
||||||
ret = self._test_format_exception(exc.ClientSideError(faultstring))
|
|
||||||
self.assertIsNone(ret['debuginfo'])
|
|
||||||
self.assertEqual('Client', ret['faultcode'])
|
|
||||||
self.assertEqual(faultstring, ret['faultstring'])
|
|
||||||
|
|
||||||
def test_format_server_exception(self):
|
|
||||||
faultstring = 'boom'
|
|
||||||
ret = self._test_format_exception(Exception(faultstring))
|
|
||||||
self.assertIsNone(ret['debuginfo'])
|
|
||||||
self.assertEqual('Server', ret['faultcode'])
|
|
||||||
self.assertEqual(faultstring, ret['faultstring'])
|
|
||||||
|
|
||||||
def test_format_server_exception_unicode(self):
|
|
||||||
faultstring = u'\xc3\xa3o'
|
|
||||||
ret = self._test_format_exception(Exception(faultstring))
|
|
||||||
self.assertIsNone(ret['debuginfo'])
|
|
||||||
self.assertEqual('Server', ret['faultcode'])
|
|
||||||
self.assertEqual(faultstring, ret['faultstring'])
|
|
||||||
|
|
||||||
def test_format_server_exception_debug(self):
|
|
||||||
faultstring = 'boom'
|
|
||||||
ret = self._test_format_exception(Exception(faultstring), debug=True)
|
|
||||||
# assert debuginfo is populated
|
|
||||||
self.assertIsNotNone(ret['debuginfo'])
|
|
||||||
self.assertEqual('Server', ret['faultcode'])
|
|
||||||
self.assertEqual(faultstring, ret['faultstring'])
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# encoding=utf8
|
|
||||||
|
|
||||||
from wsme.exc import (ClientSideError, InvalidInput, MissingArgument,
|
|
||||||
UnknownArgument)
|
|
||||||
from six import u
|
|
||||||
|
|
||||||
|
|
||||||
def test_clientside_error():
|
|
||||||
e = ClientSideError("Test")
|
|
||||||
|
|
||||||
assert e.faultstring == u("Test")
|
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_clientside_error():
|
|
||||||
e = ClientSideError(u("\u30d5\u30a1\u30b7\u30ea"))
|
|
||||||
|
|
||||||
assert e.faultstring == u("\u30d5\u30a1\u30b7\u30ea")
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalidinput():
|
|
||||||
e = InvalidInput('field', 'badvalue', "error message")
|
|
||||||
|
|
||||||
assert e.faultstring == u(
|
|
||||||
"Invalid input for field/attribute field. Value: 'badvalue'. "
|
|
||||||
"error message"
|
|
||||||
), e.faultstring
|
|
||||||
|
|
||||||
|
|
||||||
def test_missingargument():
|
|
||||||
e = MissingArgument('argname', "error message")
|
|
||||||
|
|
||||||
assert e.faultstring == \
|
|
||||||
u('Missing argument: "argname": error message'), e.faultstring
|
|
||||||
|
|
||||||
|
|
||||||
def test_unknownargument():
|
|
||||||
e = UnknownArgument('argname', "error message")
|
|
||||||
|
|
||||||
assert e.faultstring == \
|
|
||||||
u('Unknown argument: "argname": error message'), e.faultstring
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# encoding=utf8
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from wsme import WSRoot
|
|
||||||
from wsme.protocol import getprotocol, CallContext, Protocol
|
|
||||||
import wsme.protocol
|
|
||||||
|
|
||||||
|
|
||||||
class DummyProtocol(Protocol):
|
|
||||||
name = 'dummy'
|
|
||||||
content_types = ['', None]
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.hits = 0
|
|
||||||
|
|
||||||
def accept(self, req):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def iter_calls(self, req):
|
|
||||||
yield CallContext(req)
|
|
||||||
|
|
||||||
def extract_path(self, context):
|
|
||||||
return ['touch']
|
|
||||||
|
|
||||||
def read_arguments(self, context):
|
|
||||||
self.lastreq = context.request
|
|
||||||
self.hits += 1
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def encode_result(self, context, result):
|
|
||||||
return str(result)
|
|
||||||
|
|
||||||
def encode_error(self, context, infos):
|
|
||||||
return str(infos)
|
|
||||||
|
|
||||||
|
|
||||||
def test_getprotocol():
|
|
||||||
try:
|
|
||||||
getprotocol('invalid')
|
|
||||||
assert False, "ValueError was not raised"
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestProtocols(unittest.TestCase):
|
|
||||||
def test_register_protocol(self):
|
|
||||||
wsme.protocol.register_protocol(DummyProtocol)
|
|
||||||
assert wsme.protocol.registered_protocols['dummy'] == DummyProtocol
|
|
||||||
|
|
||||||
r = WSRoot()
|
|
||||||
assert len(r.protocols) == 0
|
|
||||||
|
|
||||||
r.addprotocol('dummy')
|
|
||||||
assert len(r.protocols) == 1
|
|
||||||
assert r.protocols[0].__class__ == DummyProtocol
|
|
||||||
|
|
||||||
r = WSRoot(['dummy'])
|
|
||||||
assert len(r.protocols) == 1
|
|
||||||
assert r.protocols[0].__class__ == DummyProtocol
|
|
||||||
|
|
||||||
def test_Protocol(self):
|
|
||||||
p = wsme.protocol.Protocol()
|
|
||||||
assert p.iter_calls(None) is None
|
|
||||||
assert p.extract_path(None) is None
|
|
||||||
assert p.read_arguments(None) is None
|
|
||||||
assert p.encode_result(None, None) is None
|
|
||||||
assert p.encode_sample_value(None, None) == ('none', 'N/A')
|
|
||||||
assert p.encode_sample_params(None) == ('none', 'N/A')
|
|
||||||
assert p.encode_sample_result(None, None) == ('none', 'N/A')
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
# encoding=utf8
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from wsme.api import FunctionArgument, FunctionDefinition
|
|
||||||
from wsme.rest.args import from_param, from_params, args_from_args
|
|
||||||
from wsme.exc import InvalidInput
|
|
||||||
|
|
||||||
from wsme.types import UserType, Unset, ArrayType, DictType, Base
|
|
||||||
|
|
||||||
|
|
||||||
class MyBaseType(Base):
|
|
||||||
test = str
|
|
||||||
|
|
||||||
|
|
||||||
class MyUserType(UserType):
|
|
||||||
basetype = str
|
|
||||||
|
|
||||||
|
|
||||||
class DictBasedUserType(UserType):
|
|
||||||
basetype = DictType(int, int)
|
|
||||||
|
|
||||||
|
|
||||||
class TestProtocolsCommons(unittest.TestCase):
|
|
||||||
def test_from_param_date(self):
|
|
||||||
assert from_param(datetime.date, '2008-02-28') == \
|
|
||||||
datetime.date(2008, 2, 28)
|
|
||||||
|
|
||||||
def test_from_param_time(self):
|
|
||||||
assert from_param(datetime.time, '12:14:56') == \
|
|
||||||
datetime.time(12, 14, 56)
|
|
||||||
|
|
||||||
def test_from_param_datetime(self):
|
|
||||||
assert from_param(datetime.datetime, '2009-12-23T12:14:56') == \
|
|
||||||
datetime.datetime(2009, 12, 23, 12, 14, 56)
|
|
||||||
|
|
||||||
def test_from_param_usertype(self):
|
|
||||||
assert from_param(MyUserType(), 'test') == 'test'
|
|
||||||
|
|
||||||
def test_from_params_empty(self):
|
|
||||||
assert from_params(str, {}, '', set()) is Unset
|
|
||||||
|
|
||||||
def test_from_params_native_array(self):
|
|
||||||
class params(dict):
|
|
||||||
def getall(self, path):
|
|
||||||
return ['1', '2']
|
|
||||||
p = params({'a': []})
|
|
||||||
assert from_params(ArrayType(int), p, 'a', set()) == [1, 2]
|
|
||||||
|
|
||||||
def test_from_params_empty_array(self):
|
|
||||||
assert from_params(ArrayType(int), {}, 'a', set()) is Unset
|
|
||||||
|
|
||||||
def test_from_params_dict(self):
|
|
||||||
value = from_params(
|
|
||||||
DictType(int, str),
|
|
||||||
{'a[2]': 'a2', 'a[3]': 'a3'},
|
|
||||||
'a',
|
|
||||||
set()
|
|
||||||
)
|
|
||||||
assert value == {2: 'a2', 3: 'a3'}, value
|
|
||||||
|
|
||||||
def test_from_params_dict_unset(self):
|
|
||||||
assert from_params(DictType(int, str), {}, 'a', set()) is Unset
|
|
||||||
|
|
||||||
def test_from_params_usertype(self):
|
|
||||||
value = from_params(
|
|
||||||
DictBasedUserType(),
|
|
||||||
{'a[2]': '2'},
|
|
||||||
'a',
|
|
||||||
set()
|
|
||||||
)
|
|
||||||
self.assertEqual(value, {2: 2})
|
|
||||||
|
|
||||||
def test_args_from_args_usertype(self):
|
|
||||||
|
|
||||||
class FakeType(UserType):
|
|
||||||
name = 'fake-type'
|
|
||||||
basetype = int
|
|
||||||
|
|
||||||
fake_type = FakeType()
|
|
||||||
fd = FunctionDefinition(FunctionDefinition)
|
|
||||||
fd.arguments.append(FunctionArgument('fake-arg', fake_type, True, 0))
|
|
||||||
|
|
||||||
new_args = args_from_args(fd, [1], {})
|
|
||||||
self.assertEqual([1], new_args[0])
|
|
||||||
|
|
||||||
# can't convert str to int
|
|
||||||
try:
|
|
||||||
args_from_args(fd, ['invalid-argument'], {})
|
|
||||||
except InvalidInput as e:
|
|
||||||
assert fake_type.name in str(e)
|
|
||||||
else:
|
|
||||||
self.fail('Should have thrown an InvalidInput')
|
|
||||||
|
|
||||||
def test_args_from_args_custom_exc(self):
|
|
||||||
|
|
||||||
class FakeType(UserType):
|
|
||||||
name = 'fake-type'
|
|
||||||
basetype = int
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
if value < 10:
|
|
||||||
raise ValueError('should be greater than 10')
|
|
||||||
|
|
||||||
def frombasetype(self, value):
|
|
||||||
self.validate(value)
|
|
||||||
|
|
||||||
fake_type = FakeType()
|
|
||||||
fd = FunctionDefinition(FunctionDefinition)
|
|
||||||
fd.arguments.append(FunctionArgument('fake-arg', fake_type, True, 0))
|
|
||||||
|
|
||||||
try:
|
|
||||||
args_from_args(fd, [9], {})
|
|
||||||
except InvalidInput as e:
|
|
||||||
assert fake_type.name in str(e)
|
|
||||||
assert 'Error: should be greater than 10' in str(e)
|
|
||||||
else:
|
|
||||||
self.fail('Should have thrown an InvalidInput')
|
|
||||||
|
|
||||||
def test_args_from_args_array_type(self):
|
|
||||||
fake_type = ArrayType(MyBaseType)
|
|
||||||
fd = FunctionDefinition(FunctionDefinition)
|
|
||||||
fd.arguments.append(FunctionArgument('fake-arg', fake_type, True, []))
|
|
||||||
try:
|
|
||||||
args_from_args(fd, [['invalid-argument']], {})
|
|
||||||
except InvalidInput as e:
|
|
||||||
assert ArrayType.__name__ in str(e)
|
|
||||||
else:
|
|
||||||
self.fail('Should have thrown an InvalidInput')
|
|
||||||
|
|
||||||
|
|
||||||
class ArgTypeConversion(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_int_zero(self):
|
|
||||||
self.assertEqual(0, from_param(int, 0))
|
|
||||||
self.assertEqual(0, from_param(int, '0'))
|
|
||||||
|
|
||||||
def test_int_nonzero(self):
|
|
||||||
self.assertEqual(1, from_param(int, 1))
|
|
||||||
self.assertEqual(1, from_param(int, '1'))
|
|
||||||
|
|
||||||
def test_int_none(self):
|
|
||||||
self.assertEqual(None, from_param(int, None))
|
|
||||||
|
|
||||||
def test_float_zero(self):
|
|
||||||
self.assertEqual(0.0, from_param(float, 0))
|
|
||||||
self.assertEqual(0.0, from_param(float, 0.0))
|
|
||||||
self.assertEqual(0.0, from_param(float, '0'))
|
|
||||||
self.assertEqual(0.0, from_param(float, '0.0'))
|
|
||||||
|
|
||||||
def test_float_nonzero(self):
|
|
||||||
self.assertEqual(1.0, from_param(float, 1))
|
|
||||||
self.assertEqual(1.0, from_param(float, 1.0))
|
|
||||||
self.assertEqual(1.0, from_param(float, '1'))
|
|
||||||
self.assertEqual(1.0, from_param(float, '1.0'))
|
|
||||||
|
|
||||||
def test_float_none(self):
|
|
||||||
self.assertEqual(None, from_param(float, None))
|
|
||||||
@@ -1,779 +0,0 @@
|
|||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
|
|
||||||
import wsme.tests.protocol
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except:
|
|
||||||
import json # noqa
|
|
||||||
|
|
||||||
from wsme.rest.json import fromjson, tojson, parse
|
|
||||||
from wsme.utils import parse_isodatetime, parse_isotime, parse_isodate
|
|
||||||
from wsme.types import isarray, isdict, isusertype, register_type
|
|
||||||
from wsme.types import UserType, ArrayType, DictType
|
|
||||||
from wsme.rest import expose, validate
|
|
||||||
from wsme.exc import ClientSideError, InvalidInput
|
|
||||||
|
|
||||||
|
|
||||||
import six
|
|
||||||
from six import b, u
|
|
||||||
|
|
||||||
if six.PY3:
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
else:
|
|
||||||
from urllib import urlencode # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_value(value, datatype):
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
return [prepare_value(item, datatype[0]) for item in value]
|
|
||||||
if isinstance(datatype, dict):
|
|
||||||
key_type, value_type = list(datatype.items())[0]
|
|
||||||
return dict((
|
|
||||||
(prepare_value(item[0], key_type),
|
|
||||||
prepare_value(item[1], value_type))
|
|
||||||
for item in value.items()
|
|
||||||
))
|
|
||||||
if datatype in (datetime.date, datetime.time, datetime.datetime):
|
|
||||||
return value.isoformat()
|
|
||||||
if datatype == decimal.Decimal:
|
|
||||||
return str(value)
|
|
||||||
if datatype == wsme.types.binary:
|
|
||||||
return base64.encodestring(value).decode('ascii')
|
|
||||||
if datatype == wsme.types.bytes:
|
|
||||||
return value.decode('ascii')
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_result(value, datatype):
|
|
||||||
print(value, datatype)
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if datatype == wsme.types.binary:
|
|
||||||
return base64.decodestring(value.encode('ascii'))
|
|
||||||
if isusertype(datatype):
|
|
||||||
datatype = datatype.basetype
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
return [prepare_result(item, datatype[0]) for item in value]
|
|
||||||
if isarray(datatype):
|
|
||||||
return [prepare_result(item, datatype.item_type) for item in value]
|
|
||||||
if isinstance(datatype, dict):
|
|
||||||
return dict((
|
|
||||||
(prepare_result(item[0], list(datatype.keys())[0]),
|
|
||||||
prepare_result(item[1], list(datatype.values())[0]))
|
|
||||||
for item in value.items()
|
|
||||||
))
|
|
||||||
if isdict(datatype):
|
|
||||||
return dict((
|
|
||||||
(prepare_result(item[0], datatype.key_type),
|
|
||||||
prepare_result(item[1], datatype.value_type))
|
|
||||||
for item in value.items()
|
|
||||||
))
|
|
||||||
if datatype == datetime.date:
|
|
||||||
return parse_isodate(value)
|
|
||||||
if datatype == datetime.time:
|
|
||||||
return parse_isotime(value)
|
|
||||||
if datatype == datetime.datetime:
|
|
||||||
return parse_isodatetime(value)
|
|
||||||
if hasattr(datatype, '_wsme_attributes'):
|
|
||||||
for attr in datatype._wsme_attributes:
|
|
||||||
if attr.key not in value:
|
|
||||||
continue
|
|
||||||
value[attr.key] = prepare_result(value[attr.key], attr.datatype)
|
|
||||||
return value
|
|
||||||
if datatype == wsme.types.bytes:
|
|
||||||
return value.encode('ascii')
|
|
||||||
if type(value) != datatype:
|
|
||||||
print(type(value), datatype)
|
|
||||||
return datatype(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class CustomInt(UserType):
|
|
||||||
basetype = int
|
|
||||||
name = "custom integer"
|
|
||||||
|
|
||||||
|
|
||||||
class Obj(wsme.types.Base):
|
|
||||||
id = int
|
|
||||||
name = wsme.types.text
|
|
||||||
|
|
||||||
|
|
||||||
class NestedObj(wsme.types.Base):
|
|
||||||
o = Obj
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDResult(object):
|
|
||||||
data = Obj
|
|
||||||
message = wsme.types.text
|
|
||||||
|
|
||||||
def __init__(self, data=wsme.types.Unset, message=wsme.types.Unset):
|
|
||||||
self.data = data
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class MiniCrud(object):
|
|
||||||
@expose(CRUDResult, method='PUT')
|
|
||||||
@validate(Obj)
|
|
||||||
def create(self, data):
|
|
||||||
print(repr(data))
|
|
||||||
return CRUDResult(data, u('create'))
|
|
||||||
|
|
||||||
@expose(CRUDResult, method='GET', ignore_extra_args=True)
|
|
||||||
@validate(Obj)
|
|
||||||
def read(self, ref):
|
|
||||||
print(repr(ref))
|
|
||||||
if ref.id == 1:
|
|
||||||
ref.name = u('test')
|
|
||||||
return CRUDResult(ref, u('read'))
|
|
||||||
|
|
||||||
@expose(CRUDResult, method='POST')
|
|
||||||
@validate(Obj)
|
|
||||||
def update(self, data):
|
|
||||||
print(repr(data))
|
|
||||||
return CRUDResult(data, u('update'))
|
|
||||||
|
|
||||||
@expose(CRUDResult, wsme.types.text, body=Obj)
|
|
||||||
def update_with_body(self, msg, data):
|
|
||||||
print(repr(data))
|
|
||||||
return CRUDResult(data, msg)
|
|
||||||
|
|
||||||
@expose(CRUDResult, method='DELETE')
|
|
||||||
@validate(Obj)
|
|
||||||
def delete(self, ref):
|
|
||||||
print(repr(ref))
|
|
||||||
if ref.id == 1:
|
|
||||||
ref.name = u('test')
|
|
||||||
return CRUDResult(ref, u('delete'))
|
|
||||||
|
|
||||||
|
|
||||||
wsme.tests.protocol.WSTestRoot.crud = MiniCrud()
|
|
||||||
|
|
||||||
|
|
||||||
class TestRestJson(wsme.tests.protocol.RestOnlyProtocolTestCase):
|
|
||||||
protocol = 'restjson'
|
|
||||||
|
|
||||||
def call(self, fpath, _rt=None, _accept=None, _no_result_decode=False,
|
|
||||||
body=None, **kw):
|
|
||||||
if body:
|
|
||||||
if isinstance(body, tuple):
|
|
||||||
body, datatype = body
|
|
||||||
else:
|
|
||||||
datatype = type(body)
|
|
||||||
body = prepare_value(body, datatype)
|
|
||||||
content = json.dumps(body)
|
|
||||||
else:
|
|
||||||
for key in kw:
|
|
||||||
if isinstance(kw[key], tuple):
|
|
||||||
value, datatype = kw[key]
|
|
||||||
else:
|
|
||||||
value = kw[key]
|
|
||||||
datatype = type(value)
|
|
||||||
kw[key] = prepare_value(value, datatype)
|
|
||||||
content = json.dumps(kw)
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
if _accept is not None:
|
|
||||||
headers["Accept"] = _accept
|
|
||||||
res = self.app.post(
|
|
||||||
'/' + fpath,
|
|
||||||
content,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
print("Received:", res.body)
|
|
||||||
|
|
||||||
if _no_result_decode:
|
|
||||||
return res
|
|
||||||
|
|
||||||
r = json.loads(res.text)
|
|
||||||
if res.status_int == 200:
|
|
||||||
if _rt and r:
|
|
||||||
r = prepare_result(r, _rt)
|
|
||||||
return r
|
|
||||||
else:
|
|
||||||
raise wsme.tests.protocol.CallException(
|
|
||||||
r['faultcode'],
|
|
||||||
r['faultstring'],
|
|
||||||
r.get('debuginfo')
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.loads(res.text)
|
|
||||||
|
|
||||||
def test_fromjson(self):
|
|
||||||
assert fromjson(str, None) is None
|
|
||||||
|
|
||||||
def test_keyargs(self):
|
|
||||||
r = self.app.get('/argtypes/setint.json?value=2')
|
|
||||||
print(r)
|
|
||||||
assert json.loads(r.text) == 2
|
|
||||||
|
|
||||||
nestedarray = 'value[0].inner.aint=54&value[1].inner.aint=55'
|
|
||||||
r = self.app.get('/argtypes/setnestedarray.json?' + nestedarray)
|
|
||||||
print(r)
|
|
||||||
assert json.loads(r.text) == [
|
|
||||||
{'inner': {'aint': 54}},
|
|
||||||
{'inner': {'aint': 55}}]
|
|
||||||
|
|
||||||
def test_form_urlencoded_args(self):
|
|
||||||
params = {
|
|
||||||
'value[0].inner.aint': 54,
|
|
||||||
'value[1].inner.aint': 55
|
|
||||||
}
|
|
||||||
body = urlencode(params)
|
|
||||||
r = self.app.post(
|
|
||||||
'/argtypes/setnestedarray.json',
|
|
||||||
body,
|
|
||||||
headers={'Content-Type': 'application/x-www-form-urlencoded'}
|
|
||||||
)
|
|
||||||
print(r)
|
|
||||||
|
|
||||||
assert json.loads(r.text) == [
|
|
||||||
{'inner': {'aint': 54}},
|
|
||||||
{'inner': {'aint': 55}}]
|
|
||||||
|
|
||||||
def test_body_and_params(self):
|
|
||||||
r = self.app.post('/argtypes/setint.json?value=2', '{"value": 2}',
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
expect_errors=True)
|
|
||||||
print(r)
|
|
||||||
assert r.status_int == 400
|
|
||||||
assert json.loads(r.text)['faultstring'] == \
|
|
||||||
"Parameter value was given several times"
|
|
||||||
|
|
||||||
def test_inline_body(self):
|
|
||||||
params = urlencode({'__body__': '{"value": 4}'})
|
|
||||||
r = self.app.get('/argtypes/setint.json?' + params)
|
|
||||||
print(r)
|
|
||||||
assert json.loads(r.text) == 4
|
|
||||||
|
|
||||||
def test_empty_body(self):
|
|
||||||
params = urlencode({'__body__': ''})
|
|
||||||
r = self.app.get('/returntypes/getint.json?' + params)
|
|
||||||
print(r)
|
|
||||||
assert json.loads(r.text) == 2
|
|
||||||
|
|
||||||
def test_invalid_content_type_body(self):
|
|
||||||
r = self.app.post('/argtypes/setint.json', '{"value": 2}',
|
|
||||||
headers={"Content-Type": "application/invalid"},
|
|
||||||
expect_errors=True)
|
|
||||||
print(r)
|
|
||||||
assert r.status_int == 415
|
|
||||||
assert json.loads(r.text)['faultstring'] == \
|
|
||||||
"Unknown mimetype: application/invalid"
|
|
||||||
|
|
||||||
def test_invalid_json_body(self):
|
|
||||||
r = self.app.post('/argtypes/setint.json', '{"value": 2',
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
expect_errors=True)
|
|
||||||
print(r)
|
|
||||||
assert r.status_int == 400
|
|
||||||
assert json.loads(r.text)['faultstring'] == \
|
|
||||||
"Request is not in valid JSON format"
|
|
||||||
|
|
||||||
def test_unknown_arg(self):
|
|
||||||
r = self.app.post('/returntypes/getint.json', '{"a": 2}',
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
expect_errors=True)
|
|
||||||
print(r)
|
|
||||||
assert r.status_int == 400
|
|
||||||
assert json.loads(r.text)['faultstring'].startswith(
|
|
||||||
"Unknown argument:"
|
|
||||||
)
|
|
||||||
|
|
||||||
r = self.app.get('/returntypes/getint.json?a=2', expect_errors=True)
|
|
||||||
print(r)
|
|
||||||
assert r.status_int == 400
|
|
||||||
assert json.loads(r.text)['faultstring'].startswith(
|
|
||||||
"Unknown argument:"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_set_custom_object(self):
|
|
||||||
r = self.app.post(
|
|
||||||
'/argtypes/setcustomobject',
|
|
||||||
'{"value": {"aint": 2, "name": "test"}}',
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_int, 200)
|
|
||||||
self.assertEqual(r.json, {'aint': 2, 'name': 'test'})
|
|
||||||
|
|
||||||
def test_set_extended_int(self):
|
|
||||||
r = self.app.post(
|
|
||||||
'/argtypes/setextendedint',
|
|
||||||
'{"value": 3}',
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_int, 200)
|
|
||||||
self.assertEqual(r.json, 3)
|
|
||||||
|
|
||||||
def test_unset_attrs(self):
|
|
||||||
class AType(object):
|
|
||||||
attr = int
|
|
||||||
|
|
||||||
wsme.types.register_type(AType)
|
|
||||||
|
|
||||||
j = tojson(AType, AType())
|
|
||||||
assert j == {}
|
|
||||||
|
|
||||||
def test_array_tojson(self):
|
|
||||||
assert tojson([int], None) is None
|
|
||||||
assert tojson([int], []) == []
|
|
||||||
assert tojson([str], ['1', '4']) == ['1', '4']
|
|
||||||
|
|
||||||
def test_dict_tojson(self):
|
|
||||||
assert tojson({int: str}, None) is None
|
|
||||||
assert tojson({int: str}, {5: '5'}) == {5: '5'}
|
|
||||||
|
|
||||||
def test_None_tojson(self):
|
|
||||||
for dt in (datetime.date, datetime.time, datetime.datetime,
|
|
||||||
decimal.Decimal):
|
|
||||||
assert tojson(dt, None) is None
|
|
||||||
|
|
||||||
def test_None_fromjson(self):
|
|
||||||
for dt in (str, int, datetime.date, datetime.time, datetime.datetime,
|
|
||||||
decimal.Decimal, [int], {int: int}):
|
|
||||||
assert fromjson(dt, None) is None
|
|
||||||
|
|
||||||
def test_parse_valid_date(self):
|
|
||||||
j = parse('{"a": "2011-01-01"}', {'a': datetime.date}, False)
|
|
||||||
assert isinstance(j['a'], datetime.date)
|
|
||||||
|
|
||||||
def test_invalid_root_dict_fromjson(self):
|
|
||||||
try:
|
|
||||||
parse('["invalid"]', {'a': ArrayType(str)}, False)
|
|
||||||
assert False
|
|
||||||
except Exception as e:
|
|
||||||
assert isinstance(e, ClientSideError)
|
|
||||||
assert e.msg == "Request must be a JSON dict"
|
|
||||||
|
|
||||||
def test_invalid_list_fromjson(self):
|
|
||||||
jlist = "invalid"
|
|
||||||
try:
|
|
||||||
parse('{"a": "%s"}' % jlist, {'a': ArrayType(str)}, False)
|
|
||||||
assert False
|
|
||||||
except Exception as e:
|
|
||||||
assert isinstance(e, InvalidInput)
|
|
||||||
assert e.fieldname == 'a'
|
|
||||||
assert e.value == jlist
|
|
||||||
assert e.msg == "Value not a valid list: %s" % jlist
|
|
||||||
|
|
||||||
def test_invalid_dict_fromjson(self):
|
|
||||||
jdict = "invalid"
|
|
||||||
try:
|
|
||||||
parse('{"a": "%s"}' % jdict, {'a': DictType(str, str)}, False)
|
|
||||||
assert False
|
|
||||||
except Exception as e:
|
|
||||||
assert isinstance(e, InvalidInput)
|
|
||||||
assert e.fieldname == 'a'
|
|
||||||
assert e.value == jdict
|
|
||||||
assert e.msg == "Value not a valid dict: %s" % jdict
|
|
||||||
|
|
||||||
def test_invalid_date_fromjson(self):
|
|
||||||
jdate = "2015-01-invalid"
|
|
||||||
try:
|
|
||||||
parse('{"a": "%s"}' % jdate, {'a': datetime.date}, False)
|
|
||||||
assert False
|
|
||||||
except Exception as e:
|
|
||||||
assert isinstance(e, InvalidInput)
|
|
||||||
assert e.fieldname == 'a'
|
|
||||||
assert e.value == jdate
|
|
||||||
assert e.msg == "'%s' is not a legal date value" % jdate
|
|
||||||
|
|
||||||
def test_parse_valid_date_bodyarg(self):
|
|
||||||
j = parse('"2011-01-01"', {'a': datetime.date}, True)
|
|
||||||
assert isinstance(j['a'], datetime.date)
|
|
||||||
|
|
||||||
def test_invalid_date_fromjson_bodyarg(self):
|
|
||||||
jdate = "2015-01-invalid"
|
|
||||||
try:
|
|
||||||
parse('"%s"' % jdate, {'a': datetime.date}, True)
|
|
||||||
assert False
|
|
||||||
except Exception as e:
|
|
||||||
assert isinstance(e, InvalidInput)
|
|
||||||
assert e.fieldname == 'a'
|
|
||||||
assert e.value == jdate
|
|
||||||
assert e.msg == "'%s' is not a legal date value" % jdate
|
|
||||||
|
|
||||||
def test_valid_str_to_builtin_fromjson(self):
|
|
||||||
types = six.integer_types + (bool, float)
|
|
||||||
value = '2'
|
|
||||||
for t in types:
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '%s' if ba else '{"a": %s}'
|
|
||||||
i = parse(jd % value, {'a': t}, ba)
|
|
||||||
self.assertEqual(
|
|
||||||
i, {'a': t(value)},
|
|
||||||
"Parsed value does not correspond for %s: "
|
|
||||||
"%s != {'a': %s}" % (
|
|
||||||
t, repr(i), repr(t(value))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertIsInstance(i['a'], t)
|
|
||||||
|
|
||||||
def test_valid_int_fromjson(self):
|
|
||||||
value = 2
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '%d' if ba else '{"a": %d}'
|
|
||||||
i = parse(jd % value, {'a': int}, ba)
|
|
||||||
self.assertEqual(i, {'a': 2})
|
|
||||||
self.assertIsInstance(i['a'], int)
|
|
||||||
|
|
||||||
def test_valid_num_to_float_fromjson(self):
|
|
||||||
values = 2, 2.3
|
|
||||||
for v in values:
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '%f' if ba else '{"a": %f}'
|
|
||||||
i = parse(jd % v, {'a': float}, ba)
|
|
||||||
self.assertEqual(i, {'a': float(v)})
|
|
||||||
self.assertIsInstance(i['a'], float)
|
|
||||||
|
|
||||||
def test_invalid_str_to_buitin_fromjson(self):
|
|
||||||
types = six.integer_types + (float, bool)
|
|
||||||
value = '2a'
|
|
||||||
for t in types:
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '"%s"' if ba else '{"a": "%s"}'
|
|
||||||
try:
|
|
||||||
parse(jd % value, {'a': t}, ba)
|
|
||||||
assert False, (
|
|
||||||
"Value '%s' should not parse correctly for %s." %
|
|
||||||
(value, t)
|
|
||||||
)
|
|
||||||
except ClientSideError as e:
|
|
||||||
self.assertIsInstance(e, InvalidInput)
|
|
||||||
self.assertEqual(e.fieldname, 'a')
|
|
||||||
self.assertEqual(e.value, value)
|
|
||||||
|
|
||||||
def test_ambiguous_to_bool(self):
|
|
||||||
amb_values = ('', 'randomstring', '2', '-32', 'not true')
|
|
||||||
for value in amb_values:
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '"%s"' if ba else '{"a": "%s"}'
|
|
||||||
try:
|
|
||||||
parse(jd % value, {'a': bool}, ba)
|
|
||||||
assert False, (
|
|
||||||
"Value '%s' should not parse correctly for %s." %
|
|
||||||
(value, bool)
|
|
||||||
)
|
|
||||||
except ClientSideError as e:
|
|
||||||
self.assertIsInstance(e, InvalidInput)
|
|
||||||
self.assertEqual(e.fieldname, 'a')
|
|
||||||
self.assertEqual(e.value, value)
|
|
||||||
|
|
||||||
def test_true_strings_to_bool(self):
|
|
||||||
true_values = ('true', 't', 'yes', 'y', 'on', '1')
|
|
||||||
for value in true_values:
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '"%s"' if ba else '{"a": "%s"}'
|
|
||||||
i = parse(jd % value, {'a': bool}, ba)
|
|
||||||
self.assertIsInstance(i['a'], bool)
|
|
||||||
self.assertTrue(i['a'])
|
|
||||||
|
|
||||||
def test_false_strings_to_bool(self):
|
|
||||||
false_values = ('false', 'f', 'no', 'n', 'off', '0')
|
|
||||||
for value in false_values:
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '"%s"' if ba else '{"a": "%s"}'
|
|
||||||
i = parse(jd % value, {'a': bool}, ba)
|
|
||||||
self.assertIsInstance(i['a'], bool)
|
|
||||||
self.assertFalse(i['a'])
|
|
||||||
|
|
||||||
def test_true_ints_to_bool(self):
|
|
||||||
true_values = (1, 5, -3)
|
|
||||||
for value in true_values:
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '%d' if ba else '{"a": %d}'
|
|
||||||
i = parse(jd % value, {'a': bool}, ba)
|
|
||||||
self.assertIsInstance(i['a'], bool)
|
|
||||||
self.assertTrue(i['a'])
|
|
||||||
|
|
||||||
def test_false_ints_to_bool(self):
|
|
||||||
value = 0
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '%d' if ba else '{"a": %d}'
|
|
||||||
i = parse(jd % value, {'a': bool}, ba)
|
|
||||||
self.assertIsInstance(i['a'], bool)
|
|
||||||
self.assertFalse(i['a'])
|
|
||||||
|
|
||||||
def test_valid_simple_custom_type_fromjson(self):
|
|
||||||
value = 2
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '"%d"' if ba else '{"a": "%d"}'
|
|
||||||
i = parse(jd % value, {'a': CustomInt()}, ba)
|
|
||||||
self.assertEqual(i, {'a': 2})
|
|
||||||
self.assertIsInstance(i['a'], int)
|
|
||||||
|
|
||||||
def test_invalid_simple_custom_type_fromjson(self):
|
|
||||||
value = '2b'
|
|
||||||
for ba in True, False:
|
|
||||||
jd = '"%s"' if ba else '{"a": "%s"}'
|
|
||||||
try:
|
|
||||||
i = parse(jd % value, {'a': CustomInt()}, ba)
|
|
||||||
self.assertEqual(i, {'a': 2})
|
|
||||||
except ClientSideError as e:
|
|
||||||
self.assertIsInstance(e, InvalidInput)
|
|
||||||
self.assertEqual(e.fieldname, 'a')
|
|
||||||
self.assertEqual(e.value, value)
|
|
||||||
self.assertEqual(
|
|
||||||
e.msg,
|
|
||||||
"invalid literal for int() with base 10: '%s'" % value
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_parse_unexpected_attribute(self):
|
|
||||||
o = {
|
|
||||||
"id": "1",
|
|
||||||
"name": "test",
|
|
||||||
"other": "unknown",
|
|
||||||
"other2": "still unknown",
|
|
||||||
}
|
|
||||||
for ba in True, False:
|
|
||||||
jd = o if ba else {"o": o}
|
|
||||||
try:
|
|
||||||
parse(json.dumps(jd), {'o': Obj}, ba)
|
|
||||||
raise AssertionError("Object should not parse correcty.")
|
|
||||||
except wsme.exc.UnknownAttribute as e:
|
|
||||||
self.assertEqual(e.attributes, set(['other', 'other2']))
|
|
||||||
|
|
||||||
def test_parse_unexpected_nested_attribute(self):
|
|
||||||
no = {
|
|
||||||
"o": {
|
|
||||||
"id": "1",
|
|
||||||
"name": "test",
|
|
||||||
"other": "unknown",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for ba in False, True:
|
|
||||||
jd = no if ba else {"no": no}
|
|
||||||
try:
|
|
||||||
parse(json.dumps(jd), {'no': NestedObj}, ba)
|
|
||||||
except wsme.exc.UnknownAttribute as e:
|
|
||||||
self.assertEqual(e.attributes, set(['other']))
|
|
||||||
self.assertEqual(e.fieldname, "no.o")
|
|
||||||
|
|
||||||
def test_nest_result(self):
|
|
||||||
self.root.protocols[0].nest_result = True
|
|
||||||
r = self.app.get('/returntypes/getint.json')
|
|
||||||
print(r)
|
|
||||||
assert json.loads(r.text) == {"result": 2}
|
|
||||||
|
|
||||||
def test_encode_sample_value(self):
|
|
||||||
class MyType(object):
|
|
||||||
aint = int
|
|
||||||
astr = str
|
|
||||||
|
|
||||||
register_type(MyType)
|
|
||||||
|
|
||||||
v = MyType()
|
|
||||||
v.aint = 4
|
|
||||||
v.astr = 's'
|
|
||||||
|
|
||||||
r = wsme.rest.json.encode_sample_value(MyType, v, True)
|
|
||||||
print(r)
|
|
||||||
assert r[0] == ('javascript')
|
|
||||||
assert r[1] == json.dumps({'aint': 4, 'astr': 's'}, ensure_ascii=False,
|
|
||||||
indent=4, sort_keys=True)
|
|
||||||
|
|
||||||
def test_bytes_tojson(self):
|
|
||||||
assert tojson(wsme.types.bytes, None) is None
|
|
||||||
assert tojson(wsme.types.bytes, b('ascii')) == u('ascii')
|
|
||||||
|
|
||||||
def test_encode_sample_params(self):
|
|
||||||
r = wsme.rest.json.encode_sample_params(
|
|
||||||
[('a', int, 2)], True
|
|
||||||
)
|
|
||||||
assert r[0] == 'javascript', r[0]
|
|
||||||
assert r[1] == '''{
|
|
||||||
"a": 2
|
|
||||||
}''', r[1]
|
|
||||||
|
|
||||||
def test_encode_sample_result(self):
|
|
||||||
r = wsme.rest.json.encode_sample_result(
|
|
||||||
int, 2, True
|
|
||||||
)
|
|
||||||
assert r[0] == 'javascript', r[0]
|
|
||||||
assert r[1] == '''2'''
|
|
||||||
|
|
||||||
def test_PUT(self):
|
|
||||||
data = {"id": 1, "name": u("test")}
|
|
||||||
content = json.dumps(dict(data=data))
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
res = self.app.put(
|
|
||||||
'/crud',
|
|
||||||
content,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
result = json.loads(res.text)
|
|
||||||
print(result)
|
|
||||||
assert result['data']['id'] == 1
|
|
||||||
assert result['data']['name'] == u("test")
|
|
||||||
assert result['message'] == "create"
|
|
||||||
|
|
||||||
def test_GET(self):
|
|
||||||
headers = {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
}
|
|
||||||
res = self.app.get(
|
|
||||||
'/crud?ref.id=1',
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
result = json.loads(res.text)
|
|
||||||
print(result)
|
|
||||||
assert result['data']['id'] == 1
|
|
||||||
assert result['data']['name'] == u("test")
|
|
||||||
|
|
||||||
def test_GET_complex_accept(self):
|
|
||||||
headers = {
|
|
||||||
'Accept': 'text/html,application/xml;q=0.9,*/*;q=0.8'
|
|
||||||
}
|
|
||||||
res = self.app.get(
|
|
||||||
'/crud?ref.id=1',
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
result = json.loads(res.text)
|
|
||||||
print(result)
|
|
||||||
assert result['data']['id'] == 1
|
|
||||||
assert result['data']['name'] == u("test")
|
|
||||||
|
|
||||||
def test_GET_complex_choose_xml(self):
|
|
||||||
headers = {
|
|
||||||
'Accept': 'text/html,text/xml;q=0.9,*/*;q=0.8'
|
|
||||||
}
|
|
||||||
res = self.app.get(
|
|
||||||
'/crud?ref.id=1',
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
assert res.content_type == 'text/xml'
|
|
||||||
|
|
||||||
def test_GET_complex_accept_no_match(self):
|
|
||||||
headers = {
|
|
||||||
'Accept': 'text/html,application/xml;q=0.9'
|
|
||||||
}
|
|
||||||
res = self.app.get(
|
|
||||||
'/crud?ref.id=1',
|
|
||||||
headers=headers,
|
|
||||||
status=406)
|
|
||||||
print("Received:", res.body)
|
|
||||||
assert res.body == b("Unacceptable Accept type: "
|
|
||||||
"text/html, application/xml;q=0.9 not in "
|
|
||||||
"['application/json', 'text/javascript', "
|
|
||||||
"'application/javascript', 'text/xml']")
|
|
||||||
|
|
||||||
def test_GET_bad_simple_accept(self):
|
|
||||||
headers = {
|
|
||||||
'Accept': 'text/plain',
|
|
||||||
}
|
|
||||||
res = self.app.get(
|
|
||||||
'/crud?ref.id=1',
|
|
||||||
headers=headers,
|
|
||||||
status=406)
|
|
||||||
print("Received:", res.body)
|
|
||||||
assert res.body == b("Unacceptable Accept type: text/plain not in "
|
|
||||||
"['application/json', 'text/javascript', "
|
|
||||||
"'application/javascript', 'text/xml']")
|
|
||||||
|
|
||||||
def test_POST(self):
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
res = self.app.post(
|
|
||||||
'/crud',
|
|
||||||
json.dumps(dict(data=dict(id=1, name=u('test')))),
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
result = json.loads(res.text)
|
|
||||||
print(result)
|
|
||||||
assert result['data']['id'] == 1
|
|
||||||
assert result['data']['name'] == u("test")
|
|
||||||
assert result['message'] == "update"
|
|
||||||
|
|
||||||
def test_POST_bad_content_type(self):
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'text/plain',
|
|
||||||
}
|
|
||||||
res = self.app.post(
|
|
||||||
'/crud',
|
|
||||||
json.dumps(dict(data=dict(id=1, name=u('test')))),
|
|
||||||
headers=headers,
|
|
||||||
status=415)
|
|
||||||
print("Received:", res.body)
|
|
||||||
assert res.body == b("Unacceptable Content-Type: text/plain not in "
|
|
||||||
"['application/json', 'text/javascript', "
|
|
||||||
"'application/javascript', 'text/xml']")
|
|
||||||
|
|
||||||
def test_DELETE(self):
|
|
||||||
res = self.app.delete(
|
|
||||||
'/crud.json?ref.id=1',
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
result = json.loads(res.text)
|
|
||||||
print(result)
|
|
||||||
assert result['data']['id'] == 1
|
|
||||||
assert result['data']['name'] == u("test")
|
|
||||||
assert result['message'] == "delete"
|
|
||||||
|
|
||||||
def test_extra_arguments(self):
|
|
||||||
headers = {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
}
|
|
||||||
res = self.app.get(
|
|
||||||
'/crud?ref.id=1&extraarg=foo',
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
result = json.loads(res.text)
|
|
||||||
print(result)
|
|
||||||
assert result['data']['id'] == 1
|
|
||||||
assert result['data']['name'] == u("test")
|
|
||||||
assert result['message'] == "read"
|
|
||||||
|
|
||||||
def test_unexpected_extra_arg(self):
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {"id": 1, "name": "test"}
|
|
||||||
content = json.dumps({"data": data, "other": "unexpected"})
|
|
||||||
res = self.app.put(
|
|
||||||
'/crud',
|
|
||||||
content,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_unexpected_extra_attribute(self):
|
|
||||||
"""Expect a failure if we send an unexpected object attribute."""
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {"id": 1, "name": "test", "other": "unexpected"}
|
|
||||||
content = json.dumps({"data": data})
|
|
||||||
res = self.app.put(
|
|
||||||
'/crud',
|
|
||||||
content,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
self.assertEqual(res.status_int, 400)
|
|
||||||
|
|
||||||
def test_body_arg(self):
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
res = self.app.post(
|
|
||||||
'/crud/update_with_body?msg=hello',
|
|
||||||
json.dumps(dict(id=1, name=u('test'))),
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=False)
|
|
||||||
print("Received:", res.body)
|
|
||||||
result = json.loads(res.text)
|
|
||||||
print(result)
|
|
||||||
assert result['data']['id'] == 1
|
|
||||||
assert result['data']['name'] == u("test")
|
|
||||||
assert result['message'] == "hello"
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
import decimal
|
|
||||||
import datetime
|
|
||||||
import base64
|
|
||||||
|
|
||||||
from six import u, b
|
|
||||||
import six
|
|
||||||
|
|
||||||
import wsme.tests.protocol
|
|
||||||
from wsme.utils import parse_isodatetime, parse_isodate, parse_isotime
|
|
||||||
from wsme.types import isarray, isdict, isusertype, register_type
|
|
||||||
|
|
||||||
from wsme.rest.xml import fromxml, toxml
|
|
||||||
|
|
||||||
try:
|
|
||||||
import xml.etree.ElementTree as et
|
|
||||||
except:
|
|
||||||
import cElementTree as et # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def dumpxml(key, obj, datatype=None):
|
|
||||||
el = et.Element(key)
|
|
||||||
if isinstance(obj, tuple):
|
|
||||||
obj, datatype = obj
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
for item in obj:
|
|
||||||
el.append(dumpxml('item', item, datatype[0]))
|
|
||||||
elif isinstance(datatype, dict):
|
|
||||||
key_type, value_type = list(datatype.items())[0]
|
|
||||||
for item in obj.items():
|
|
||||||
node = et.SubElement(el, 'item')
|
|
||||||
node.append(dumpxml('key', item[0], key_type))
|
|
||||||
node.append(dumpxml('value', item[1], value_type))
|
|
||||||
elif datatype == wsme.types.binary:
|
|
||||||
el.text = base64.encodestring(obj).decode('ascii')
|
|
||||||
elif isinstance(obj, wsme.types.bytes):
|
|
||||||
el.text = obj.decode('ascii')
|
|
||||||
elif isinstance(obj, wsme.types.text):
|
|
||||||
el.text = obj
|
|
||||||
elif type(obj) in (int, float, bool, decimal.Decimal):
|
|
||||||
el.text = six.text_type(obj)
|
|
||||||
elif type(obj) in (datetime.date, datetime.time, datetime.datetime):
|
|
||||||
el.text = obj.isoformat()
|
|
||||||
elif isinstance(obj, type(None)):
|
|
||||||
el.set('nil', 'true')
|
|
||||||
elif hasattr(datatype, '_wsme_attributes'):
|
|
||||||
for attr in datatype._wsme_attributes:
|
|
||||||
name = attr.name
|
|
||||||
if name not in obj:
|
|
||||||
continue
|
|
||||||
o = obj[name]
|
|
||||||
el.append(dumpxml(name, o, attr.datatype))
|
|
||||||
elif type(obj) == dict:
|
|
||||||
for name, value in obj.items():
|
|
||||||
el.append(dumpxml(name, value))
|
|
||||||
print(obj, datatype, et.tostring(el))
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
def loadxml(el, datatype):
|
|
||||||
print(el, datatype, len(el))
|
|
||||||
if el.get('nil') == 'true':
|
|
||||||
return None
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
return [loadxml(item, datatype[0]) for item in el.findall('item')]
|
|
||||||
elif isarray(datatype):
|
|
||||||
return [
|
|
||||||
loadxml(item, datatype.item_type) for item in el.findall('item')
|
|
||||||
]
|
|
||||||
elif isinstance(datatype, dict):
|
|
||||||
key_type, value_type = list(datatype.items())[0]
|
|
||||||
return dict((
|
|
||||||
(loadxml(item.find('key'), key_type),
|
|
||||||
loadxml(item.find('value'), value_type))
|
|
||||||
for item in el.findall('item')
|
|
||||||
))
|
|
||||||
elif isdict(datatype):
|
|
||||||
return dict((
|
|
||||||
(loadxml(item.find('key'), datatype.key_type),
|
|
||||||
loadxml(item.find('value'), datatype.value_type))
|
|
||||||
for item in el.findall('item')
|
|
||||||
))
|
|
||||||
elif isdict(datatype):
|
|
||||||
return dict((
|
|
||||||
(loadxml(item.find('key'), datatype.key_type),
|
|
||||||
loadxml(item.find('value'), datatype.value_type))
|
|
||||||
for item in el.findall('item')
|
|
||||||
))
|
|
||||||
elif len(el):
|
|
||||||
d = {}
|
|
||||||
for attr in datatype._wsme_attributes:
|
|
||||||
name = attr.name
|
|
||||||
child = el.find(name)
|
|
||||||
print(name, attr, child)
|
|
||||||
if child is not None:
|
|
||||||
d[name] = loadxml(child, attr.datatype)
|
|
||||||
print(d)
|
|
||||||
return d
|
|
||||||
else:
|
|
||||||
if datatype == wsme.types.binary:
|
|
||||||
return base64.decodestring(el.text.encode('ascii'))
|
|
||||||
if isusertype(datatype):
|
|
||||||
datatype = datatype.basetype
|
|
||||||
if datatype == datetime.date:
|
|
||||||
return parse_isodate(el.text)
|
|
||||||
if datatype == datetime.time:
|
|
||||||
return parse_isotime(el.text)
|
|
||||||
if datatype == datetime.datetime:
|
|
||||||
return parse_isodatetime(el.text)
|
|
||||||
if datatype == wsme.types.text:
|
|
||||||
return datatype(el.text if el.text else u(''))
|
|
||||||
if datatype == bool:
|
|
||||||
return el.text.lower() != 'false'
|
|
||||||
if datatype is None:
|
|
||||||
return el.text
|
|
||||||
if datatype is wsme.types.bytes:
|
|
||||||
return el.text.encode('ascii')
|
|
||||||
return datatype(el.text)
|
|
||||||
|
|
||||||
|
|
||||||
class TestRestXML(wsme.tests.protocol.RestOnlyProtocolTestCase):
|
|
||||||
protocol = 'restxml'
|
|
||||||
|
|
||||||
def call(self, fpath, _rt=None, _accept=None, _no_result_decode=False,
|
|
||||||
body=None, **kw):
|
|
||||||
if body:
|
|
||||||
el = dumpxml('body', body)
|
|
||||||
else:
|
|
||||||
el = dumpxml('parameters', kw)
|
|
||||||
content = et.tostring(el)
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'text/xml',
|
|
||||||
}
|
|
||||||
if _accept is not None:
|
|
||||||
headers['Accept'] = _accept
|
|
||||||
res = self.app.post(
|
|
||||||
'/' + fpath,
|
|
||||||
content,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
print("Received:", res.body)
|
|
||||||
|
|
||||||
if _no_result_decode:
|
|
||||||
return res
|
|
||||||
|
|
||||||
el = et.fromstring(res.body)
|
|
||||||
if el.tag == 'error':
|
|
||||||
raise wsme.tests.protocol.CallException(
|
|
||||||
el.find('faultcode').text,
|
|
||||||
el.find('faultstring').text,
|
|
||||||
el.find('debuginfo') is not None and
|
|
||||||
el.find('debuginfo').text or None
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return loadxml(et.fromstring(res.body), _rt)
|
|
||||||
|
|
||||||
def test_encode_sample_value(self):
|
|
||||||
class MyType(object):
|
|
||||||
aint = int
|
|
||||||
atext = wsme.types.text
|
|
||||||
|
|
||||||
register_type(MyType)
|
|
||||||
|
|
||||||
value = MyType()
|
|
||||||
value.aint = 5
|
|
||||||
value.atext = u('test')
|
|
||||||
|
|
||||||
language, sample = wsme.rest.xml.encode_sample_value(
|
|
||||||
MyType, value, True)
|
|
||||||
print(language, sample)
|
|
||||||
|
|
||||||
assert language == 'xml'
|
|
||||||
assert sample == b("""<value>
|
|
||||||
<aint>5</aint>
|
|
||||||
<atext>test</atext>
|
|
||||||
</value>""")
|
|
||||||
|
|
||||||
def test_encode_sample_params(self):
|
|
||||||
lang, content = wsme.rest.xml.encode_sample_params(
|
|
||||||
[('a', int, 2)], True)
|
|
||||||
assert lang == 'xml', lang
|
|
||||||
assert content == b('<parameters>\n <a>2</a>\n</parameters>'), content
|
|
||||||
|
|
||||||
def test_encode_sample_result(self):
|
|
||||||
lang, content = wsme.rest.xml.encode_sample_result(int, 2, True)
|
|
||||||
assert lang == 'xml', lang
|
|
||||||
assert content == b('<result>2</result>'), content
|
|
||||||
|
|
||||||
def test_nil_fromxml(self):
|
|
||||||
for dt in (
|
|
||||||
str, [int], {int: str}, bool,
|
|
||||||
datetime.date, datetime.time, datetime.datetime):
|
|
||||||
e = et.Element('value', nil='true')
|
|
||||||
assert fromxml(dt, e) is None
|
|
||||||
|
|
||||||
def test_nil_toxml(self):
|
|
||||||
for dt in (
|
|
||||||
wsme.types.bytes,
|
|
||||||
[int], {int: str}, bool,
|
|
||||||
datetime.date, datetime.time, datetime.datetime):
|
|
||||||
x = et.tostring(toxml(dt, 'value', None))
|
|
||||||
assert x == b('<value nil="true" />'), x
|
|
||||||
|
|
||||||
def test_unset_attrs(self):
|
|
||||||
class AType(object):
|
|
||||||
someattr = wsme.types.bytes
|
|
||||||
|
|
||||||
wsme.types.register_type(AType)
|
|
||||||
|
|
||||||
x = et.tostring(toxml(AType, 'value', AType()))
|
|
||||||
assert x == b('<value />'), x
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# encoding=utf8
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from wsme import WSRoot
|
|
||||||
import wsme.protocol
|
|
||||||
import wsme.rest.protocol
|
|
||||||
from wsme.root import default_prepare_response_body
|
|
||||||
|
|
||||||
from six import b, u
|
|
||||||
from webob import Request
|
|
||||||
|
|
||||||
|
|
||||||
class TestRoot(unittest.TestCase):
|
|
||||||
def test_default_transaction(self):
|
|
||||||
import transaction
|
|
||||||
root = WSRoot(transaction=True)
|
|
||||||
assert root._transaction is transaction
|
|
||||||
|
|
||||||
txn = root.begin()
|
|
||||||
txn.abort()
|
|
||||||
|
|
||||||
def test_default_prepare_response_body(self):
|
|
||||||
default_prepare_response_body(None, [b('a')]) == b('a')
|
|
||||||
default_prepare_response_body(None, [b('a'), b('b')]) == b('a\nb')
|
|
||||||
default_prepare_response_body(None, [u('a')]) == u('a')
|
|
||||||
default_prepare_response_body(None, [u('a'), u('b')]) == u('a\nb')
|
|
||||||
|
|
||||||
def test_protocol_selection_error(self):
|
|
||||||
class P(wsme.protocol.Protocol):
|
|
||||||
name = "test"
|
|
||||||
|
|
||||||
def accept(self, r):
|
|
||||||
raise Exception('test')
|
|
||||||
|
|
||||||
root = WSRoot()
|
|
||||||
root.addprotocol(P())
|
|
||||||
|
|
||||||
from webob import Request
|
|
||||||
req = Request.blank('/test?check=a&check=b&name=Bob')
|
|
||||||
res = root._handle_request(req)
|
|
||||||
assert res.status_int == 500
|
|
||||||
assert res.content_type == 'text/plain'
|
|
||||||
assert (res.text ==
|
|
||||||
'Unexpected error while selecting protocol: test'), req.text
|
|
||||||
|
|
||||||
def test_protocol_selection_accept_mismatch(self):
|
|
||||||
"""Verify that we get a 406 error on wrong Accept header."""
|
|
||||||
class P(wsme.protocol.Protocol):
|
|
||||||
name = "test"
|
|
||||||
|
|
||||||
def accept(self, r):
|
|
||||||
return False
|
|
||||||
|
|
||||||
root = WSRoot()
|
|
||||||
root.addprotocol(wsme.rest.protocol.RestProtocol())
|
|
||||||
root.addprotocol(P())
|
|
||||||
|
|
||||||
req = Request.blank('/test?check=a&check=b&name=Bob')
|
|
||||||
req.method = 'GET'
|
|
||||||
res = root._handle_request(req)
|
|
||||||
assert res.status_int == 406
|
|
||||||
assert res.content_type == 'text/plain'
|
|
||||||
assert res.text.startswith(
|
|
||||||
'None of the following protocols can handle this request'
|
|
||||||
), req.text
|
|
||||||
|
|
||||||
def test_protocol_selection_content_type_mismatch(self):
|
|
||||||
"""Verify that we get a 415 error on wrong Content-Type header."""
|
|
||||||
class P(wsme.protocol.Protocol):
|
|
||||||
name = "test"
|
|
||||||
|
|
||||||
def accept(self, r):
|
|
||||||
return False
|
|
||||||
|
|
||||||
root = WSRoot()
|
|
||||||
root.addprotocol(wsme.rest.protocol.RestProtocol())
|
|
||||||
root.addprotocol(P())
|
|
||||||
|
|
||||||
req = Request.blank('/test?check=a&check=b&name=Bob')
|
|
||||||
req.method = 'POST'
|
|
||||||
req.headers['Content-Type'] = "test/unsupported"
|
|
||||||
res = root._handle_request(req)
|
|
||||||
assert res.status_int == 415
|
|
||||||
assert res.content_type == 'text/plain'
|
|
||||||
assert res.text.startswith(
|
|
||||||
'Unacceptable Content-Type: test/unsupported not in'
|
|
||||||
), req.text
|
|
||||||
|
|
||||||
def test_protocol_selection_get_method(self):
|
|
||||||
class P(wsme.protocol.Protocol):
|
|
||||||
name = "test"
|
|
||||||
|
|
||||||
def accept(self, r):
|
|
||||||
return True
|
|
||||||
|
|
||||||
root = WSRoot()
|
|
||||||
root.addprotocol(wsme.rest.protocol.RestProtocol())
|
|
||||||
root.addprotocol(P())
|
|
||||||
|
|
||||||
req = Request.blank('/test?check=a&check=b&name=Bob')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.headers['Accept'] = 'test/fake'
|
|
||||||
p = root._select_protocol(req)
|
|
||||||
assert p.name == "test"
|
|
||||||
|
|
||||||
def test_protocol_selection_post_method(self):
|
|
||||||
class P(wsme.protocol.Protocol):
|
|
||||||
name = "test"
|
|
||||||
|
|
||||||
def accept(self, r):
|
|
||||||
return True
|
|
||||||
|
|
||||||
root = WSRoot()
|
|
||||||
root.addprotocol(wsme.rest.protocol.RestProtocol())
|
|
||||||
root.addprotocol(P())
|
|
||||||
|
|
||||||
req = Request.blank('/test?check=a&check=b&name=Bob')
|
|
||||||
req.headers['Content-Type'] = 'test/fake'
|
|
||||||
req.method = 'POST'
|
|
||||||
p = root._select_protocol(req)
|
|
||||||
assert p.name == "test"
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
from wsme.tests.protocol import WSTestRoot
|
|
||||||
import wsme.tests.test_restjson
|
|
||||||
import wsme.spore
|
|
||||||
|
|
||||||
|
|
||||||
class TestSpore(unittest.TestCase):
|
|
||||||
def test_spore(self):
|
|
||||||
spore = wsme.spore.getdesc(WSTestRoot())
|
|
||||||
|
|
||||||
print(spore)
|
|
||||||
|
|
||||||
spore = json.loads(spore)
|
|
||||||
|
|
||||||
assert len(spore['methods']) == 51, str(len(spore['methods']))
|
|
||||||
|
|
||||||
m = spore['methods']['argtypes_setbytesarray']
|
|
||||||
assert m['path'] == 'argtypes/setbytesarray', m['path']
|
|
||||||
assert m['optional_params'] == ['value']
|
|
||||||
assert m['method'] == 'POST'
|
|
||||||
|
|
||||||
m = spore['methods']['argtypes_setdecimal']
|
|
||||||
assert m['path'] == 'argtypes/setdecimal'
|
|
||||||
assert m['required_params'] == ['value']
|
|
||||||
assert m['method'] == 'GET'
|
|
||||||
|
|
||||||
m = spore['methods']['crud_create']
|
|
||||||
assert m['path'] == 'crud'
|
|
||||||
assert m['method'] == 'PUT'
|
|
||||||
assert m['optional_params'] == ['data']
|
|
||||||
|
|
||||||
m = spore['methods']['crud_read']
|
|
||||||
assert m['path'] == 'crud'
|
|
||||||
assert m['method'] == 'GET'
|
|
||||||
assert m['required_params'] == ['ref']
|
|
||||||
|
|
||||||
m = spore['methods']['crud_update']
|
|
||||||
assert m['path'] == 'crud'
|
|
||||||
assert m['method'] == 'POST'
|
|
||||||
assert m['optional_params'] == ['data']
|
|
||||||
|
|
||||||
m = spore['methods']['crud_delete']
|
|
||||||
assert m['path'] == 'crud'
|
|
||||||
assert m['method'] == 'DELETE'
|
|
||||||
assert m['optional_params'] == ['ref']
|
|
||||||
@@ -1,667 +0,0 @@
|
|||||||
import re
|
|
||||||
try:
|
|
||||||
import unittest2 as unittest
|
|
||||||
except ImportError:
|
|
||||||
import unittest
|
|
||||||
import six
|
|
||||||
|
|
||||||
from wsme import exc
|
|
||||||
from wsme import types
|
|
||||||
|
|
||||||
|
|
||||||
def gen_class():
|
|
||||||
d = {}
|
|
||||||
exec('''class tmp(object): pass''', d)
|
|
||||||
return d['tmp']
|
|
||||||
|
|
||||||
|
|
||||||
class TestTypes(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
types.registry = types.Registry()
|
|
||||||
|
|
||||||
def test_default_usertype(self):
|
|
||||||
class MyType(types.UserType):
|
|
||||||
basetype = str
|
|
||||||
|
|
||||||
My = MyType()
|
|
||||||
|
|
||||||
assert My.validate('a') == 'a'
|
|
||||||
assert My.tobasetype('a') == 'a'
|
|
||||||
assert My.frombasetype('a') == 'a'
|
|
||||||
|
|
||||||
def test_unset(self):
|
|
||||||
u = types.Unset
|
|
||||||
|
|
||||||
assert not u
|
|
||||||
|
|
||||||
def test_flat_type(self):
|
|
||||||
class Flat(object):
|
|
||||||
aint = int
|
|
||||||
abytes = six.binary_type
|
|
||||||
atext = six.text_type
|
|
||||||
afloat = float
|
|
||||||
|
|
||||||
types.register_type(Flat)
|
|
||||||
|
|
||||||
assert len(Flat._wsme_attributes) == 4
|
|
||||||
attrs = Flat._wsme_attributes
|
|
||||||
print(attrs)
|
|
||||||
|
|
||||||
assert attrs[0].key == 'aint'
|
|
||||||
assert attrs[0].name == 'aint'
|
|
||||||
assert isinstance(attrs[0], types.wsattr)
|
|
||||||
assert attrs[0].datatype == int
|
|
||||||
assert attrs[0].mandatory is False
|
|
||||||
assert attrs[1].key == 'abytes'
|
|
||||||
assert attrs[1].name == 'abytes'
|
|
||||||
assert attrs[2].key == 'atext'
|
|
||||||
assert attrs[2].name == 'atext'
|
|
||||||
assert attrs[3].key == 'afloat'
|
|
||||||
assert attrs[3].name == 'afloat'
|
|
||||||
|
|
||||||
def test_private_attr(self):
|
|
||||||
class WithPrivateAttrs(object):
|
|
||||||
_private = 12
|
|
||||||
|
|
||||||
types.register_type(WithPrivateAttrs)
|
|
||||||
|
|
||||||
assert len(WithPrivateAttrs._wsme_attributes) == 0
|
|
||||||
|
|
||||||
def test_attribute_order(self):
|
|
||||||
class ForcedOrder(object):
|
|
||||||
_wsme_attr_order = ('a2', 'a1', 'a3')
|
|
||||||
a1 = int
|
|
||||||
a2 = int
|
|
||||||
a3 = int
|
|
||||||
|
|
||||||
types.register_type(ForcedOrder)
|
|
||||||
|
|
||||||
print(ForcedOrder._wsme_attributes)
|
|
||||||
assert ForcedOrder._wsme_attributes[0].key == 'a2'
|
|
||||||
assert ForcedOrder._wsme_attributes[1].key == 'a1'
|
|
||||||
assert ForcedOrder._wsme_attributes[2].key == 'a3'
|
|
||||||
|
|
||||||
c = gen_class()
|
|
||||||
print(c)
|
|
||||||
types.register_type(c)
|
|
||||||
del c._wsme_attributes
|
|
||||||
|
|
||||||
c.a2 = int
|
|
||||||
c.a1 = int
|
|
||||||
c.a3 = int
|
|
||||||
|
|
||||||
types.register_type(c)
|
|
||||||
|
|
||||||
assert c._wsme_attributes[0].key == 'a1', c._wsme_attributes[0].key
|
|
||||||
assert c._wsme_attributes[1].key == 'a2'
|
|
||||||
assert c._wsme_attributes[2].key == 'a3'
|
|
||||||
|
|
||||||
def test_wsproperty(self):
|
|
||||||
class WithWSProp(object):
|
|
||||||
def __init__(self):
|
|
||||||
self._aint = 0
|
|
||||||
|
|
||||||
def get_aint(self):
|
|
||||||
return self._aint
|
|
||||||
|
|
||||||
def set_aint(self, value):
|
|
||||||
self._aint = value
|
|
||||||
|
|
||||||
aint = types.wsproperty(int, get_aint, set_aint, mandatory=True)
|
|
||||||
|
|
||||||
types.register_type(WithWSProp)
|
|
||||||
|
|
||||||
print(WithWSProp._wsme_attributes)
|
|
||||||
assert len(WithWSProp._wsme_attributes) == 1
|
|
||||||
a = WithWSProp._wsme_attributes[0]
|
|
||||||
assert a.key == 'aint'
|
|
||||||
assert a.datatype == int
|
|
||||||
assert a.mandatory
|
|
||||||
|
|
||||||
o = WithWSProp()
|
|
||||||
o.aint = 12
|
|
||||||
|
|
||||||
assert o.aint == 12
|
|
||||||
|
|
||||||
def test_nested(self):
|
|
||||||
class Inner(object):
|
|
||||||
aint = int
|
|
||||||
|
|
||||||
class Outer(object):
|
|
||||||
inner = Inner
|
|
||||||
|
|
||||||
types.register_type(Outer)
|
|
||||||
|
|
||||||
assert hasattr(Inner, '_wsme_attributes')
|
|
||||||
assert len(Inner._wsme_attributes) == 1
|
|
||||||
|
|
||||||
def test_inspect_with_inheritance(self):
|
|
||||||
class Parent(object):
|
|
||||||
parent_attribute = int
|
|
||||||
|
|
||||||
class Child(Parent):
|
|
||||||
child_attribute = int
|
|
||||||
|
|
||||||
types.register_type(Parent)
|
|
||||||
types.register_type(Child)
|
|
||||||
|
|
||||||
assert len(Child._wsme_attributes) == 2
|
|
||||||
|
|
||||||
def test_selfreftype(self):
|
|
||||||
class SelfRefType(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
SelfRefType.parent = SelfRefType
|
|
||||||
|
|
||||||
types.register_type(SelfRefType)
|
|
||||||
|
|
||||||
def test_inspect_with_property(self):
|
|
||||||
class AType(object):
|
|
||||||
@property
|
|
||||||
def test(self):
|
|
||||||
return 'test'
|
|
||||||
|
|
||||||
types.register_type(AType)
|
|
||||||
|
|
||||||
assert len(AType._wsme_attributes) == 0
|
|
||||||
assert AType().test == 'test'
|
|
||||||
|
|
||||||
def test_enum(self):
|
|
||||||
aenum = types.Enum(str, 'v1', 'v2')
|
|
||||||
assert aenum.basetype is str
|
|
||||||
|
|
||||||
class AType(object):
|
|
||||||
a = aenum
|
|
||||||
|
|
||||||
types.register_type(AType)
|
|
||||||
|
|
||||||
assert AType.a.datatype is aenum
|
|
||||||
|
|
||||||
obj = AType()
|
|
||||||
obj.a = 'v1'
|
|
||||||
assert obj.a == 'v1', repr(obj.a)
|
|
||||||
|
|
||||||
self.assertRaisesRegexp(exc.InvalidInput,
|
|
||||||
"Invalid input for field/attribute a. \
|
|
||||||
Value: 'v3'. Value should be one of: v., v.",
|
|
||||||
setattr,
|
|
||||||
obj,
|
|
||||||
'a',
|
|
||||||
'v3')
|
|
||||||
|
|
||||||
def test_attribute_validation(self):
|
|
||||||
class AType(object):
|
|
||||||
alist = [int]
|
|
||||||
aint = int
|
|
||||||
|
|
||||||
types.register_type(AType)
|
|
||||||
|
|
||||||
obj = AType()
|
|
||||||
|
|
||||||
obj.alist = [1, 2, 3]
|
|
||||||
assert obj.alist == [1, 2, 3]
|
|
||||||
obj.aint = 5
|
|
||||||
assert obj.aint == 5
|
|
||||||
|
|
||||||
self.assertRaises(exc.InvalidInput, setattr, obj, 'alist', 12)
|
|
||||||
self.assertRaises(exc.InvalidInput, setattr, obj, 'alist', [2, 'a'])
|
|
||||||
|
|
||||||
def test_attribute_validation_minimum(self):
|
|
||||||
class ATypeInt(object):
|
|
||||||
attr = types.IntegerType(minimum=1, maximum=5)
|
|
||||||
|
|
||||||
types.register_type(ATypeInt)
|
|
||||||
|
|
||||||
obj = ATypeInt()
|
|
||||||
obj.attr = 2
|
|
||||||
|
|
||||||
# comparison between 'zero' value and intger minimum (1) raises a
|
|
||||||
# TypeError which must be wrapped into an InvalidInput exception
|
|
||||||
self.assertRaises(exc.InvalidInput, setattr, obj, 'attr', 'zero')
|
|
||||||
|
|
||||||
def test_text_attribute_conversion(self):
|
|
||||||
class SType(object):
|
|
||||||
atext = types.text
|
|
||||||
abytes = types.bytes
|
|
||||||
|
|
||||||
types.register_type(SType)
|
|
||||||
|
|
||||||
obj = SType()
|
|
||||||
|
|
||||||
obj.atext = six.b('somebytes')
|
|
||||||
assert obj.atext == six.u('somebytes')
|
|
||||||
assert isinstance(obj.atext, types.text)
|
|
||||||
|
|
||||||
obj.abytes = six.u('sometext')
|
|
||||||
assert obj.abytes == six.b('sometext')
|
|
||||||
assert isinstance(obj.abytes, types.bytes)
|
|
||||||
|
|
||||||
def test_named_attribute(self):
|
|
||||||
class ABCDType(object):
|
|
||||||
a_list = types.wsattr([int], name='a.list')
|
|
||||||
astr = str
|
|
||||||
|
|
||||||
types.register_type(ABCDType)
|
|
||||||
|
|
||||||
assert len(ABCDType._wsme_attributes) == 2
|
|
||||||
attrs = ABCDType._wsme_attributes
|
|
||||||
|
|
||||||
assert attrs[0].key == 'a_list', attrs[0].key
|
|
||||||
assert attrs[0].name == 'a.list', attrs[0].name
|
|
||||||
assert attrs[1].key == 'astr', attrs[1].key
|
|
||||||
assert attrs[1].name == 'astr', attrs[1].name
|
|
||||||
|
|
||||||
def test_wsattr_del(self):
|
|
||||||
class MyType(object):
|
|
||||||
a = types.wsattr(int)
|
|
||||||
|
|
||||||
types.register_type(MyType)
|
|
||||||
|
|
||||||
value = MyType()
|
|
||||||
|
|
||||||
value.a = 5
|
|
||||||
assert value.a == 5
|
|
||||||
del value.a
|
|
||||||
assert value.a is types.Unset
|
|
||||||
|
|
||||||
def test_validate_dict(self):
|
|
||||||
assert types.validate_value({int: str}, {1: '1', 5: '5'})
|
|
||||||
|
|
||||||
self.assertRaises(ValueError, types.validate_value,
|
|
||||||
{int: str}, [])
|
|
||||||
|
|
||||||
assert types.validate_value({int: str}, {'1': '1', 5: '5'})
|
|
||||||
|
|
||||||
self.assertRaises(ValueError, types.validate_value,
|
|
||||||
{int: str}, {1: 1, 5: '5'})
|
|
||||||
|
|
||||||
def test_validate_list_valid(self):
|
|
||||||
assert types.validate_value([int], [1, 2])
|
|
||||||
assert types.validate_value([int], ['5'])
|
|
||||||
|
|
||||||
def test_validate_list_empty(self):
|
|
||||||
assert types.validate_value([int], []) == []
|
|
||||||
|
|
||||||
def test_validate_list_none(self):
|
|
||||||
v = types.ArrayType(int)
|
|
||||||
assert v.validate(None) is None
|
|
||||||
|
|
||||||
def test_validate_list_invalid_member(self):
|
|
||||||
self.assertRaises(ValueError, types.validate_value, [int],
|
|
||||||
['not-a-number'])
|
|
||||||
|
|
||||||
def test_validate_list_invalid_type(self):
|
|
||||||
self.assertRaises(ValueError, types.validate_value, [int], 1)
|
|
||||||
|
|
||||||
def test_validate_float(self):
|
|
||||||
self.assertEqual(types.validate_value(float, 1), 1.0)
|
|
||||||
self.assertEqual(types.validate_value(float, '1'), 1.0)
|
|
||||||
self.assertEqual(types.validate_value(float, 1.1), 1.1)
|
|
||||||
self.assertRaises(ValueError, types.validate_value, float, [])
|
|
||||||
self.assertRaises(ValueError, types.validate_value, float,
|
|
||||||
'not-a-float')
|
|
||||||
|
|
||||||
def test_validate_int(self):
|
|
||||||
self.assertEqual(types.validate_value(int, 1), 1)
|
|
||||||
self.assertEqual(types.validate_value(int, '1'), 1)
|
|
||||||
self.assertEqual(types.validate_value(int, six.u('1')), 1)
|
|
||||||
self.assertRaises(ValueError, types.validate_value, int, 1.1)
|
|
||||||
|
|
||||||
def test_validate_integer_type(self):
|
|
||||||
v = types.IntegerType(minimum=1, maximum=10)
|
|
||||||
v.validate(1)
|
|
||||||
v.validate(5)
|
|
||||||
v.validate(10)
|
|
||||||
self.assertRaises(ValueError, v.validate, 0)
|
|
||||||
self.assertRaises(ValueError, v.validate, 11)
|
|
||||||
|
|
||||||
def test_validate_string_type(self):
|
|
||||||
v = types.StringType(min_length=1, max_length=10,
|
|
||||||
pattern='^[a-zA-Z0-9]*$')
|
|
||||||
v.validate('1')
|
|
||||||
v.validate('12345')
|
|
||||||
v.validate('1234567890')
|
|
||||||
self.assertRaises(ValueError, v.validate, '')
|
|
||||||
self.assertRaises(ValueError, v.validate, '12345678901')
|
|
||||||
|
|
||||||
# Test a pattern validation
|
|
||||||
v.validate('a')
|
|
||||||
v.validate('A')
|
|
||||||
self.assertRaises(ValueError, v.validate, '_')
|
|
||||||
|
|
||||||
def test_validate_string_type_precompile(self):
|
|
||||||
precompile = re.compile('^[a-zA-Z0-9]*$')
|
|
||||||
v = types.StringType(min_length=1, max_length=10,
|
|
||||||
pattern=precompile)
|
|
||||||
|
|
||||||
# Test a pattern validation
|
|
||||||
v.validate('a')
|
|
||||||
v.validate('A')
|
|
||||||
self.assertRaises(ValueError, v.validate, '_')
|
|
||||||
|
|
||||||
def test_validate_string_type_pattern_exception_message(self):
|
|
||||||
regex = '^[a-zA-Z0-9]*$'
|
|
||||||
v = types.StringType(pattern=regex)
|
|
||||||
try:
|
|
||||||
v.validate('_')
|
|
||||||
self.assertFail()
|
|
||||||
except ValueError as e:
|
|
||||||
self.assertIn(regex, str(e))
|
|
||||||
|
|
||||||
def test_validate_ipv4_address_type(self):
|
|
||||||
v = types.IPv4AddressType()
|
|
||||||
self.assertEqual(v.validate('127.0.0.1'), '127.0.0.1')
|
|
||||||
self.assertEqual(v.validate('192.168.0.1'), '192.168.0.1')
|
|
||||||
self.assertEqual(v.validate(u'8.8.1.1'), u'8.8.1.1')
|
|
||||||
self.assertRaises(ValueError, v.validate, '')
|
|
||||||
self.assertRaises(ValueError, v.validate, 'foo')
|
|
||||||
self.assertRaises(ValueError, v.validate,
|
|
||||||
'2001:0db8:bd05:01d2:288a:1fc0:0001:10ee')
|
|
||||||
self.assertRaises(ValueError, v.validate, '1.2.3')
|
|
||||||
|
|
||||||
def test_validate_ipv6_address_type(self):
|
|
||||||
v = types.IPv6AddressType()
|
|
||||||
self.assertEqual(v.validate('0:0:0:0:0:0:0:1'),
|
|
||||||
'0:0:0:0:0:0:0:1')
|
|
||||||
self.assertEqual(v.validate(u'0:0:0:0:0:0:0:1'), u'0:0:0:0:0:0:0:1')
|
|
||||||
self.assertEqual(v.validate('2001:0db8:bd05:01d2:288a:1fc0:0001:10ee'),
|
|
||||||
'2001:0db8:bd05:01d2:288a:1fc0:0001:10ee')
|
|
||||||
self.assertRaises(ValueError, v.validate, '')
|
|
||||||
self.assertRaises(ValueError, v.validate, 'foo')
|
|
||||||
self.assertRaises(ValueError, v.validate, '192.168.0.1')
|
|
||||||
self.assertRaises(ValueError, v.validate, '0:0:0:0:0:0:1')
|
|
||||||
|
|
||||||
def test_validate_uuid_type(self):
|
|
||||||
v = types.UuidType()
|
|
||||||
self.assertEqual(v.validate('6a0a707c-45ef-4758-b533-e55adddba8ce'),
|
|
||||||
'6a0a707c-45ef-4758-b533-e55adddba8ce')
|
|
||||||
self.assertEqual(v.validate('6a0a707c45ef4758b533e55adddba8ce'),
|
|
||||||
'6a0a707c-45ef-4758-b533-e55adddba8ce')
|
|
||||||
self.assertRaises(ValueError, v.validate, '')
|
|
||||||
self.assertRaises(ValueError, v.validate, 'foo')
|
|
||||||
self.assertRaises(ValueError, v.validate,
|
|
||||||
'6a0a707c-45ef-4758-b533-e55adddba8ce-a')
|
|
||||||
|
|
||||||
def test_register_invalid_array(self):
|
|
||||||
self.assertRaises(ValueError, types.register_type, [])
|
|
||||||
self.assertRaises(ValueError, types.register_type, [int, str])
|
|
||||||
self.assertRaises(AttributeError, types.register_type, [1])
|
|
||||||
|
|
||||||
def test_register_invalid_dict(self):
|
|
||||||
self.assertRaises(ValueError, types.register_type, {})
|
|
||||||
self.assertRaises(ValueError, types.register_type,
|
|
||||||
{int: str, str: int})
|
|
||||||
self.assertRaises(ValueError, types.register_type,
|
|
||||||
{types.Unset: str})
|
|
||||||
|
|
||||||
def test_list_attribute_no_auto_register(self):
|
|
||||||
class MyType(object):
|
|
||||||
aint = int
|
|
||||||
|
|
||||||
assert not hasattr(MyType, '_wsme_attributes')
|
|
||||||
|
|
||||||
self.assertRaises(TypeError, types.list_attributes, MyType)
|
|
||||||
|
|
||||||
assert not hasattr(MyType, '_wsme_attributes')
|
|
||||||
|
|
||||||
def test_list_of_complextypes(self):
|
|
||||||
class A(object):
|
|
||||||
bs = types.wsattr(['B'])
|
|
||||||
|
|
||||||
class B(object):
|
|
||||||
i = int
|
|
||||||
|
|
||||||
types.register_type(A)
|
|
||||||
types.register_type(B)
|
|
||||||
|
|
||||||
assert A.bs.datatype.item_type is B
|
|
||||||
|
|
||||||
def test_cross_referenced_types(self):
|
|
||||||
class A(object):
|
|
||||||
b = types.wsattr('B')
|
|
||||||
|
|
||||||
class B(object):
|
|
||||||
a = A
|
|
||||||
|
|
||||||
types.register_type(A)
|
|
||||||
types.register_type(B)
|
|
||||||
|
|
||||||
assert A.b.datatype is B
|
|
||||||
|
|
||||||
def test_base(self):
|
|
||||||
class B1(types.Base):
|
|
||||||
b2 = types.wsattr('B2')
|
|
||||||
|
|
||||||
class B2(types.Base):
|
|
||||||
b2 = types.wsattr('B2')
|
|
||||||
|
|
||||||
assert B1.b2.datatype is B2, repr(B1.b2.datatype)
|
|
||||||
assert B2.b2.datatype is B2
|
|
||||||
|
|
||||||
def test_base_init(self):
|
|
||||||
class C1(types.Base):
|
|
||||||
s = six.text_type
|
|
||||||
|
|
||||||
c = C1(s=six.u('test'))
|
|
||||||
assert c.s == six.u('test')
|
|
||||||
|
|
||||||
def test_array_eq(self):
|
|
||||||
l = [types.ArrayType(str)]
|
|
||||||
assert types.ArrayType(str) in l
|
|
||||||
|
|
||||||
def test_array_sample(self):
|
|
||||||
s = types.ArrayType(str).sample()
|
|
||||||
assert isinstance(s, list)
|
|
||||||
assert s
|
|
||||||
assert s[0] == ''
|
|
||||||
|
|
||||||
def test_dict_sample(self):
|
|
||||||
s = types.DictType(str, str).sample()
|
|
||||||
assert isinstance(s, dict)
|
|
||||||
assert s
|
|
||||||
assert s == {'': ''}
|
|
||||||
|
|
||||||
def test_binary_to_base(self):
|
|
||||||
import base64
|
|
||||||
assert types.binary.tobasetype(None) is None
|
|
||||||
expected = base64.encodestring(six.b('abcdef'))
|
|
||||||
assert types.binary.tobasetype(six.b('abcdef')) == expected
|
|
||||||
|
|
||||||
def test_binary_from_base(self):
|
|
||||||
import base64
|
|
||||||
assert types.binary.frombasetype(None) is None
|
|
||||||
encoded = base64.encodestring(six.b('abcdef'))
|
|
||||||
assert types.binary.frombasetype(encoded) == six.b('abcdef')
|
|
||||||
|
|
||||||
def test_wsattr_weakref_datatype(self):
|
|
||||||
# If the datatype inside the wsattr ends up a weakref, it
|
|
||||||
# should be converted to the real type when accessed again by
|
|
||||||
# the property getter.
|
|
||||||
import weakref
|
|
||||||
a = types.wsattr(int)
|
|
||||||
a.datatype = weakref.ref(int)
|
|
||||||
assert a.datatype is int
|
|
||||||
|
|
||||||
def test_wsattr_list_datatype(self):
|
|
||||||
# If the datatype inside the wsattr ends up a list of weakrefs
|
|
||||||
# to types, it should be converted to the real types when
|
|
||||||
# accessed again by the property getter.
|
|
||||||
import weakref
|
|
||||||
a = types.wsattr(int)
|
|
||||||
a.datatype = [weakref.ref(int)]
|
|
||||||
assert isinstance(a.datatype, list)
|
|
||||||
assert a.datatype[0] is int
|
|
||||||
|
|
||||||
def test_file_get_content_by_reading(self):
|
|
||||||
class buffer:
|
|
||||||
def read(self):
|
|
||||||
return 'abcdef'
|
|
||||||
f = types.File(file=buffer())
|
|
||||||
assert f.content == 'abcdef'
|
|
||||||
|
|
||||||
def test_file_content_overrides_file(self):
|
|
||||||
class buffer:
|
|
||||||
def read(self):
|
|
||||||
return 'from-file'
|
|
||||||
f = types.File(content='from-content', file=buffer())
|
|
||||||
assert f.content == 'from-content'
|
|
||||||
|
|
||||||
def test_file_setting_content_discards_file(self):
|
|
||||||
class buffer:
|
|
||||||
def read(self):
|
|
||||||
return 'from-file'
|
|
||||||
f = types.File(file=buffer())
|
|
||||||
f.content = 'from-content'
|
|
||||||
assert f.content == 'from-content'
|
|
||||||
|
|
||||||
def test_file_field_storage(self):
|
|
||||||
class buffer:
|
|
||||||
def read(self):
|
|
||||||
return 'from-file'
|
|
||||||
|
|
||||||
class fieldstorage:
|
|
||||||
filename = 'static.json'
|
|
||||||
file = buffer()
|
|
||||||
type = 'application/json'
|
|
||||||
f = types.File(fieldstorage=fieldstorage)
|
|
||||||
assert f.content == 'from-file'
|
|
||||||
|
|
||||||
def test_file_field_storage_value(self):
|
|
||||||
class buffer:
|
|
||||||
def read(self):
|
|
||||||
return 'from-file'
|
|
||||||
|
|
||||||
class fieldstorage:
|
|
||||||
filename = 'static.json'
|
|
||||||
file = None
|
|
||||||
type = 'application/json'
|
|
||||||
value = 'from-value'
|
|
||||||
f = types.File(fieldstorage=fieldstorage)
|
|
||||||
assert f.content == 'from-value'
|
|
||||||
|
|
||||||
def test_file_property_file(self):
|
|
||||||
class buffer:
|
|
||||||
def read(self):
|
|
||||||
return 'from-file'
|
|
||||||
buf = buffer()
|
|
||||||
f = types.File(file=buf)
|
|
||||||
assert f.file is buf
|
|
||||||
|
|
||||||
def test_file_property_content(self):
|
|
||||||
class buffer:
|
|
||||||
def read(self):
|
|
||||||
return 'from-file'
|
|
||||||
f = types.File(content=six.b('from-content'))
|
|
||||||
assert f.file.read() == six.b('from-content')
|
|
||||||
|
|
||||||
def test_unregister(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
types.registry.register(TempType)
|
|
||||||
v = types.registry.lookup('TempType')
|
|
||||||
self.assertIs(v, TempType)
|
|
||||||
types.registry._unregister(TempType)
|
|
||||||
after = types.registry.lookup('TempType')
|
|
||||||
self.assertIs(after, None)
|
|
||||||
|
|
||||||
def test_unregister_twice(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
types.registry.register(TempType)
|
|
||||||
v = types.registry.lookup('TempType')
|
|
||||||
self.assertIs(v, TempType)
|
|
||||||
types.registry._unregister(TempType)
|
|
||||||
# Second call should not raise an exception
|
|
||||||
types.registry._unregister(TempType)
|
|
||||||
after = types.registry.lookup('TempType')
|
|
||||||
self.assertIs(after, None)
|
|
||||||
|
|
||||||
def test_unregister_array_type(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
t = [TempType]
|
|
||||||
types.registry.register(t)
|
|
||||||
self.assertNotEqual(types.registry.array_types, set())
|
|
||||||
types.registry._unregister(t)
|
|
||||||
self.assertEqual(types.registry.array_types, set())
|
|
||||||
|
|
||||||
def test_unregister_array_type_twice(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
t = [TempType]
|
|
||||||
types.registry.register(t)
|
|
||||||
self.assertNotEqual(types.registry.array_types, set())
|
|
||||||
types.registry._unregister(t)
|
|
||||||
# Second call should not raise an exception
|
|
||||||
types.registry._unregister(t)
|
|
||||||
self.assertEqual(types.registry.array_types, set())
|
|
||||||
|
|
||||||
def test_unregister_dict_type(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
t = {str: TempType}
|
|
||||||
types.registry.register(t)
|
|
||||||
self.assertNotEqual(types.registry.dict_types, set())
|
|
||||||
types.registry._unregister(t)
|
|
||||||
self.assertEqual(types.registry.dict_types, set())
|
|
||||||
|
|
||||||
def test_unregister_dict_type_twice(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
t = {str: TempType}
|
|
||||||
types.registry.register(t)
|
|
||||||
self.assertNotEqual(types.registry.dict_types, set())
|
|
||||||
types.registry._unregister(t)
|
|
||||||
# Second call should not raise an exception
|
|
||||||
types.registry._unregister(t)
|
|
||||||
self.assertEqual(types.registry.dict_types, set())
|
|
||||||
|
|
||||||
def test_reregister(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
types.registry.register(TempType)
|
|
||||||
v = types.registry.lookup('TempType')
|
|
||||||
self.assertIs(v, TempType)
|
|
||||||
types.registry.reregister(TempType)
|
|
||||||
after = types.registry.lookup('TempType')
|
|
||||||
self.assertIs(after, TempType)
|
|
||||||
|
|
||||||
def test_reregister_and_add_attr(self):
|
|
||||||
class TempType(object):
|
|
||||||
pass
|
|
||||||
types.registry.register(TempType)
|
|
||||||
attrs = types.list_attributes(TempType)
|
|
||||||
self.assertEqual(attrs, [])
|
|
||||||
TempType.one = str
|
|
||||||
types.registry.reregister(TempType)
|
|
||||||
after = types.list_attributes(TempType)
|
|
||||||
self.assertNotEqual(after, [])
|
|
||||||
|
|
||||||
def test_dynamicbase_add_attributes(self):
|
|
||||||
class TempType(types.DynamicBase):
|
|
||||||
pass
|
|
||||||
types.registry.register(TempType)
|
|
||||||
attrs = types.list_attributes(TempType)
|
|
||||||
self.assertEqual(attrs, [])
|
|
||||||
TempType.add_attributes(one=str)
|
|
||||||
after = types.list_attributes(TempType)
|
|
||||||
self.assertEqual(len(after), 1)
|
|
||||||
|
|
||||||
def test_dynamicbase_add_attributes_second(self):
|
|
||||||
class TempType(types.DynamicBase):
|
|
||||||
pass
|
|
||||||
types.registry.register(TempType)
|
|
||||||
attrs = types.list_attributes(TempType)
|
|
||||||
self.assertEqual(attrs, [])
|
|
||||||
TempType.add_attributes(one=str)
|
|
||||||
TempType.add_attributes(two=int)
|
|
||||||
after = types.list_attributes(TempType)
|
|
||||||
self.assertEqual(len(after), 2)
|
|
||||||
|
|
||||||
def test_non_registered_complex_type(self):
|
|
||||||
class TempType(types.Base):
|
|
||||||
__registry__ = None
|
|
||||||
|
|
||||||
self.assertFalse(types.iscomplex(TempType))
|
|
||||||
types.registry.register(TempType)
|
|
||||||
self.assertTrue(types.iscomplex(TempType))
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import unittest
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
from wsme import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
|
||||||
def test_parse_isodate(self):
|
|
||||||
good_dates = [
|
|
||||||
('2008-02-01', datetime.date(2008, 2, 1)),
|
|
||||||
('2009-01-04', datetime.date(2009, 1, 4)),
|
|
||||||
]
|
|
||||||
ill_formatted_dates = [
|
|
||||||
'24-12-2004'
|
|
||||||
]
|
|
||||||
out_of_range_dates = [
|
|
||||||
'0000-00-00',
|
|
||||||
'2012-02-30',
|
|
||||||
]
|
|
||||||
for s, d in good_dates:
|
|
||||||
assert utils.parse_isodate(s) == d
|
|
||||||
for s in ill_formatted_dates + out_of_range_dates:
|
|
||||||
self.assertRaises(ValueError, utils.parse_isodate, s)
|
|
||||||
|
|
||||||
def test_parse_isotime(self):
|
|
||||||
good_times = [
|
|
||||||
('12:03:54', datetime.time(12, 3, 54)),
|
|
||||||
('23:59:59.000004', datetime.time(23, 59, 59, 4)),
|
|
||||||
('01:02:03+00:00', datetime.time(1, 2, 3, 0, pytz.UTC)),
|
|
||||||
('01:02:03+23:59', datetime.time(1, 2, 3, 0,
|
|
||||||
pytz.FixedOffset(1439))),
|
|
||||||
('01:02:03-23:59', datetime.time(1, 2, 3, 0,
|
|
||||||
pytz.FixedOffset(-1439))),
|
|
||||||
]
|
|
||||||
ill_formatted_times = [
|
|
||||||
'24-12-2004'
|
|
||||||
]
|
|
||||||
out_of_range_times = [
|
|
||||||
'32:12:00',
|
|
||||||
'00:54:60',
|
|
||||||
'01:02:03-24:00',
|
|
||||||
'01:02:03+24:00',
|
|
||||||
]
|
|
||||||
for s, t in good_times:
|
|
||||||
assert utils.parse_isotime(s) == t
|
|
||||||
for s in ill_formatted_times + out_of_range_times:
|
|
||||||
self.assertRaises(ValueError, utils.parse_isotime, s)
|
|
||||||
|
|
||||||
def test_parse_isodatetime(self):
|
|
||||||
good_datetimes = [
|
|
||||||
('2008-02-12T12:03:54',
|
|
||||||
datetime.datetime(2008, 2, 12, 12, 3, 54)),
|
|
||||||
('2012-05-14T23:59:59.000004',
|
|
||||||
datetime.datetime(2012, 5, 14, 23, 59, 59, 4)),
|
|
||||||
('1856-07-10T01:02:03+00:00',
|
|
||||||
datetime.datetime(1856, 7, 10, 1, 2, 3, 0, pytz.UTC)),
|
|
||||||
('1856-07-10T01:02:03+23:59',
|
|
||||||
datetime.datetime(1856, 7, 10, 1, 2, 3, 0,
|
|
||||||
pytz.FixedOffset(1439))),
|
|
||||||
('1856-07-10T01:02:03-23:59',
|
|
||||||
datetime.datetime(1856, 7, 10, 1, 2, 3, 0,
|
|
||||||
pytz.FixedOffset(-1439))),
|
|
||||||
]
|
|
||||||
ill_formatted_datetimes = [
|
|
||||||
'24-32-2004',
|
|
||||||
'1856-07-10+33:00'
|
|
||||||
]
|
|
||||||
out_of_range_datetimes = [
|
|
||||||
'2008-02-12T32:12:00',
|
|
||||||
'2012-13-12T00:54:60',
|
|
||||||
]
|
|
||||||
for s, t in good_datetimes:
|
|
||||||
assert utils.parse_isodatetime(s) == t
|
|
||||||
for s in ill_formatted_datetimes + out_of_range_datetimes:
|
|
||||||
self.assertRaises(ValueError, utils.parse_isodatetime, s)
|
|
||||||
|
|
||||||
def test_validator_with_valid_code(self):
|
|
||||||
valid_code = 404
|
|
||||||
self.assertTrue(
|
|
||||||
utils.is_valid_code(valid_code),
|
|
||||||
"Valid status code not detected"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_validator_with_invalid_int_code(self):
|
|
||||||
invalid_int_code = 648
|
|
||||||
self.assertFalse(
|
|
||||||
utils.is_valid_code(invalid_int_code),
|
|
||||||
"Invalid status code not detected"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_validator_with_invalid_str_code(self):
|
|
||||||
invalid_str_code = '404'
|
|
||||||
self.assertFalse(
|
|
||||||
utils.is_valid_code(invalid_str_code),
|
|
||||||
"Invalid status code not detected"
|
|
||||||
)
|
|
||||||
840
wsme/types.py
840
wsme/types.py
@@ -1,840 +0,0 @@
|
|||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
import inspect
|
|
||||||
import logging
|
|
||||||
import netaddr
|
|
||||||
import re
|
|
||||||
import six
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
import weakref
|
|
||||||
|
|
||||||
from wsme import exc
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
#: The 'str' (python 2) or 'bytes' (python 3) type.
|
|
||||||
#: Its use should be restricted to
|
|
||||||
#: pure ascii strings as the protocols will generally not be
|
|
||||||
#: be able to send non-unicode strings.
|
|
||||||
#: To transmit binary strings, use the :class:`binary` type
|
|
||||||
bytes = six.binary_type
|
|
||||||
|
|
||||||
#: Unicode string.
|
|
||||||
text = six.text_type
|
|
||||||
|
|
||||||
|
|
||||||
class ArrayType(object):
|
|
||||||
def __init__(self, item_type):
|
|
||||||
if iscomplex(item_type):
|
|
||||||
self._item_type = weakref.ref(item_type)
|
|
||||||
else:
|
|
||||||
self._item_type = item_type
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.item_type)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return isinstance(other, ArrayType) \
|
|
||||||
and self.item_type == other.item_type
|
|
||||||
|
|
||||||
def sample(self):
|
|
||||||
return [getattr(self.item_type, 'sample', self.item_type)()]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def item_type(self):
|
|
||||||
if isinstance(self._item_type, weakref.ref):
|
|
||||||
return self._item_type()
|
|
||||||
else:
|
|
||||||
return self._item_type
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
if value is None:
|
|
||||||
return
|
|
||||||
if not isinstance(value, list):
|
|
||||||
raise ValueError("Wrong type. Expected '[%s]', got '%s'" % (
|
|
||||||
self.item_type, type(value)
|
|
||||||
))
|
|
||||||
return [
|
|
||||||
validate_value(self.item_type, item)
|
|
||||||
for item in value
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class DictType(object):
|
|
||||||
def __init__(self, key_type, value_type):
|
|
||||||
if key_type not in pod_types:
|
|
||||||
raise ValueError("Dictionaries key can only be a pod type")
|
|
||||||
self.key_type = key_type
|
|
||||||
if iscomplex(value_type):
|
|
||||||
self._value_type = weakref.ref(value_type)
|
|
||||||
else:
|
|
||||||
self._value_type = value_type
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.key_type, self.value_type))
|
|
||||||
|
|
||||||
def sample(self):
|
|
||||||
key = getattr(self.key_type, 'sample', self.key_type)()
|
|
||||||
value = getattr(self.value_type, 'sample', self.value_type)()
|
|
||||||
return {key: value}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value_type(self):
|
|
||||||
if isinstance(self._value_type, weakref.ref):
|
|
||||||
return self._value_type()
|
|
||||||
else:
|
|
||||||
return self._value_type
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
raise ValueError("Wrong type. Expected '{%s: %s}', got '%s'" % (
|
|
||||||
self.key_type, self.value_type, type(value)
|
|
||||||
))
|
|
||||||
return dict((
|
|
||||||
(
|
|
||||||
validate_value(self.key_type, key),
|
|
||||||
validate_value(self.value_type, v)
|
|
||||||
) for key, v in value.items()
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
class UserType(object):
|
|
||||||
basetype = None
|
|
||||||
name = None
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
def tobasetype(self, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
def frombasetype(self, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def isusertype(class_):
|
|
||||||
return isinstance(class_, UserType)
|
|
||||||
|
|
||||||
|
|
||||||
class BinaryType(UserType):
|
|
||||||
"""
|
|
||||||
A user type that use base64 strings to carry binary data.
|
|
||||||
"""
|
|
||||||
basetype = bytes
|
|
||||||
name = 'binary'
|
|
||||||
|
|
||||||
def tobasetype(self, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return base64.encodestring(value)
|
|
||||||
|
|
||||||
def frombasetype(self, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return base64.decodestring(value)
|
|
||||||
|
|
||||||
|
|
||||||
#: The binary almost-native type
|
|
||||||
binary = BinaryType()
|
|
||||||
|
|
||||||
|
|
||||||
class IntegerType(UserType):
|
|
||||||
"""
|
|
||||||
A simple integer type. Can validate a value range.
|
|
||||||
|
|
||||||
:param minimum: Possible minimum value
|
|
||||||
:param maximum: Possible maximum value
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
Price = IntegerType(minimum=1)
|
|
||||||
|
|
||||||
"""
|
|
||||||
basetype = int
|
|
||||||
name = "integer"
|
|
||||||
|
|
||||||
def __init__(self, minimum=None, maximum=None):
|
|
||||||
self.minimum = minimum
|
|
||||||
self.maximum = maximum
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def frombasetype(value):
|
|
||||||
return int(value) if value is not None else None
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
if self.minimum is not None and value < self.minimum:
|
|
||||||
error = 'Value should be greater or equal to %s' % self.minimum
|
|
||||||
raise ValueError(error)
|
|
||||||
|
|
||||||
if self.maximum is not None and value > self.maximum:
|
|
||||||
error = 'Value should be lower or equal to %s' % self.maximum
|
|
||||||
raise ValueError(error)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class StringType(UserType):
|
|
||||||
"""
|
|
||||||
A simple string type. Can validate a length and a pattern.
|
|
||||||
|
|
||||||
:param min_length: Possible minimum length
|
|
||||||
:param max_length: Possible maximum length
|
|
||||||
:param pattern: Possible string pattern
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
Name = StringType(min_length=1, pattern='^[a-zA-Z ]*$')
|
|
||||||
|
|
||||||
"""
|
|
||||||
basetype = six.string_types
|
|
||||||
name = "string"
|
|
||||||
|
|
||||||
def __init__(self, min_length=None, max_length=None, pattern=None):
|
|
||||||
self.min_length = min_length
|
|
||||||
self.max_length = max_length
|
|
||||||
if isinstance(pattern, six.string_types):
|
|
||||||
self.pattern = re.compile(pattern)
|
|
||||||
else:
|
|
||||||
self.pattern = pattern
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
if not isinstance(value, self.basetype):
|
|
||||||
error = 'Value should be string'
|
|
||||||
raise ValueError(error)
|
|
||||||
|
|
||||||
if self.min_length is not None and len(value) < self.min_length:
|
|
||||||
error = 'Value should have a minimum character requirement of %s' \
|
|
||||||
% self.min_length
|
|
||||||
raise ValueError(error)
|
|
||||||
|
|
||||||
if self.max_length is not None and len(value) > self.max_length:
|
|
||||||
error = 'Value should have a maximum character requirement of %s' \
|
|
||||||
% self.max_length
|
|
||||||
raise ValueError(error)
|
|
||||||
|
|
||||||
if self.pattern is not None and not self.pattern.search(value):
|
|
||||||
error = 'Value should match the pattern %s' % self.pattern.pattern
|
|
||||||
raise ValueError(error)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class IPv4AddressType(UserType):
|
|
||||||
"""
|
|
||||||
A simple IPv4 type.
|
|
||||||
"""
|
|
||||||
basetype = six.string_types
|
|
||||||
name = "ipv4address"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate(value):
|
|
||||||
try:
|
|
||||||
netaddr.IPAddress(value, version=4, flags=netaddr.INET_PTON)
|
|
||||||
except netaddr.AddrFormatError:
|
|
||||||
error = 'Value should be IPv4 format'
|
|
||||||
raise ValueError(error)
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class IPv6AddressType(UserType):
|
|
||||||
"""
|
|
||||||
A simple IPv6 type.
|
|
||||||
|
|
||||||
This type represents IPv6 addresses in the short format.
|
|
||||||
"""
|
|
||||||
basetype = six.string_types
|
|
||||||
name = "ipv6address"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate(value):
|
|
||||||
try:
|
|
||||||
netaddr.IPAddress(value, version=6, flags=netaddr.INET_PTON)
|
|
||||||
except netaddr.AddrFormatError:
|
|
||||||
error = 'Value should be IPv6 format'
|
|
||||||
raise ValueError(error)
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class UuidType(UserType):
|
|
||||||
"""
|
|
||||||
A simple UUID type.
|
|
||||||
|
|
||||||
This type allows not only UUID having dashes but also UUID not
|
|
||||||
having dashes. For example, '6a0a707c-45ef-4758-b533-e55adddba8ce'
|
|
||||||
and '6a0a707c45ef4758b533e55adddba8ce' are distinguished as valid.
|
|
||||||
"""
|
|
||||||
basetype = six.string_types
|
|
||||||
name = "uuid"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate(value):
|
|
||||||
try:
|
|
||||||
return six.text_type((uuid.UUID(value)))
|
|
||||||
except (TypeError, ValueError, AttributeError):
|
|
||||||
error = 'Value should be UUID format'
|
|
||||||
raise ValueError(error)
|
|
||||||
|
|
||||||
|
|
||||||
class Enum(UserType):
|
|
||||||
"""
|
|
||||||
A simple enumeration type. Can be based on any non-complex type.
|
|
||||||
|
|
||||||
:param basetype: The actual data type
|
|
||||||
:param values: A set of possible values
|
|
||||||
|
|
||||||
If nullable, 'None' should be added the values set.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
Gender = Enum(str, 'male', 'female')
|
|
||||||
Specie = Enum(str, 'cat', 'dog')
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, basetype, *values, **kw):
|
|
||||||
self.basetype = basetype
|
|
||||||
self.values = set(values)
|
|
||||||
name = kw.pop('name', None)
|
|
||||||
if name is None:
|
|
||||||
name = "Enum(%s)" % ', '.join((six.text_type(v) for v in values))
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
if value not in self.values:
|
|
||||||
raise ValueError("Value should be one of: %s" %
|
|
||||||
', '.join(map(six.text_type, self.values)))
|
|
||||||
return value
|
|
||||||
|
|
||||||
def tobasetype(self, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
def frombasetype(self, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class UnsetType(object):
|
|
||||||
if sys.version < '3':
|
|
||||||
def __nonzero__(self):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
def __bool__(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Unset'
|
|
||||||
|
|
||||||
|
|
||||||
Unset = UnsetType()
|
|
||||||
|
|
||||||
#: A special type that corresponds to the host framework request object.
|
|
||||||
#: It can only be used in the function parameters, and if so the request object
|
|
||||||
#: of the host framework will be passed to the function.
|
|
||||||
HostRequest = object()
|
|
||||||
|
|
||||||
|
|
||||||
pod_types = six.integer_types + (
|
|
||||||
bytes, text, float, bool)
|
|
||||||
dt_types = (datetime.date, datetime.time, datetime.datetime)
|
|
||||||
extra_types = (binary, decimal.Decimal)
|
|
||||||
native_types = pod_types + dt_types + extra_types
|
|
||||||
# The types for which we allow promotion to certain numbers.
|
|
||||||
_promotable_types = six.integer_types + (text, bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def iscomplex(datatype):
|
|
||||||
return inspect.isclass(datatype) \
|
|
||||||
and '_wsme_attributes' in datatype.__dict__
|
|
||||||
|
|
||||||
|
|
||||||
def isarray(datatype):
|
|
||||||
return isinstance(datatype, ArrayType)
|
|
||||||
|
|
||||||
|
|
||||||
def isdict(datatype):
|
|
||||||
return isinstance(datatype, DictType)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_value(datatype, value):
|
|
||||||
if value in (Unset, None):
|
|
||||||
return value
|
|
||||||
|
|
||||||
# Try to promote the data type to one of our complex types.
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
datatype = ArrayType(datatype[0])
|
|
||||||
elif isinstance(datatype, dict):
|
|
||||||
datatype = DictType(*list(datatype.items())[0])
|
|
||||||
|
|
||||||
# If the datatype has its own validator, use that.
|
|
||||||
if hasattr(datatype, 'validate'):
|
|
||||||
return datatype.validate(value)
|
|
||||||
|
|
||||||
# Do type promotion/conversion and data validation for builtin
|
|
||||||
# types.
|
|
||||||
v_type = type(value)
|
|
||||||
if datatype in six.integer_types:
|
|
||||||
if v_type in _promotable_types:
|
|
||||||
try:
|
|
||||||
# Try to turn the value into an int
|
|
||||||
value = datatype(value)
|
|
||||||
except ValueError:
|
|
||||||
# An error is raised at the end of the function
|
|
||||||
# when the types don't match.
|
|
||||||
pass
|
|
||||||
elif datatype is float and v_type in _promotable_types:
|
|
||||||
try:
|
|
||||||
value = float(value)
|
|
||||||
except ValueError:
|
|
||||||
# An error is raised at the end of the function
|
|
||||||
# when the types don't match.
|
|
||||||
pass
|
|
||||||
elif datatype is text and isinstance(value, bytes):
|
|
||||||
value = value.decode()
|
|
||||||
elif datatype is bytes and isinstance(value, text):
|
|
||||||
value = value.encode()
|
|
||||||
|
|
||||||
if not isinstance(value, datatype):
|
|
||||||
raise ValueError(
|
|
||||||
"Wrong type. Expected '%s', got '%s'" % (
|
|
||||||
datatype, v_type
|
|
||||||
))
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class wsproperty(property):
|
|
||||||
"""
|
|
||||||
A specialised :class:`property` to define typed-property on complex types.
|
|
||||||
Example::
|
|
||||||
|
|
||||||
class MyComplexType(wsme.types.Base):
|
|
||||||
def get_aint(self):
|
|
||||||
return self._aint
|
|
||||||
|
|
||||||
def set_aint(self, value):
|
|
||||||
assert avalue < 10 # Dummy input validation
|
|
||||||
self._aint = value
|
|
||||||
|
|
||||||
aint = wsproperty(int, get_aint, set_aint, mandatory=True)
|
|
||||||
"""
|
|
||||||
def __init__(self, datatype, fget, fset=None,
|
|
||||||
mandatory=False, doc=None, name=None):
|
|
||||||
property.__init__(self, fget, fset)
|
|
||||||
#: The property name in the parent python class
|
|
||||||
self.key = None
|
|
||||||
#: The attribute name on the public of the api.
|
|
||||||
#: Defaults to :attr:`key`
|
|
||||||
self.name = name
|
|
||||||
#: property data type
|
|
||||||
self.datatype = datatype
|
|
||||||
#: True if the property is mandatory
|
|
||||||
self.mandatory = mandatory
|
|
||||||
|
|
||||||
|
|
||||||
class wsattr(object):
|
|
||||||
"""
|
|
||||||
Complex type attribute definition.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
class MyComplexType(wsme.types.Base):
|
|
||||||
optionalvalue = int
|
|
||||||
mandatoryvalue = wsattr(int, mandatory=True)
|
|
||||||
named_value = wsattr(int, name='named.value')
|
|
||||||
|
|
||||||
After inspection, the non-wsattr attributes will be replaced, and
|
|
||||||
the above class will be equivalent to::
|
|
||||||
|
|
||||||
class MyComplexType(wsme.types.Base):
|
|
||||||
optionalvalue = wsattr(int)
|
|
||||||
mandatoryvalue = wsattr(int, mandatory=True)
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, datatype, mandatory=False, name=None, default=Unset,
|
|
||||||
readonly=False):
|
|
||||||
#: The attribute name in the parent python class.
|
|
||||||
#: Set by :func:`inspect_class`
|
|
||||||
self.key = None # will be set by class inspection
|
|
||||||
#: The attribute name on the public of the api.
|
|
||||||
#: Defaults to :attr:`key`
|
|
||||||
self.name = name
|
|
||||||
self._datatype = (datatype,)
|
|
||||||
#: True if the attribute is mandatory
|
|
||||||
self.mandatory = mandatory
|
|
||||||
#: Default value. The attribute will return this instead
|
|
||||||
#: of :data:`Unset` if no value has been set.
|
|
||||||
self.default = default
|
|
||||||
#: If True value cannot be set from json/xml input data
|
|
||||||
self.readonly = readonly
|
|
||||||
|
|
||||||
self.complextype = None
|
|
||||||
|
|
||||||
def _get_dataholder(self, instance):
|
|
||||||
dataholder = getattr(instance, '_wsme_dataholder', None)
|
|
||||||
if dataholder is None:
|
|
||||||
dataholder = instance._wsme_DataHolderClass()
|
|
||||||
instance._wsme_dataholder = dataholder
|
|
||||||
return dataholder
|
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
|
||||||
if instance is None:
|
|
||||||
return self
|
|
||||||
return getattr(
|
|
||||||
self._get_dataholder(instance),
|
|
||||||
self.key,
|
|
||||||
self.default
|
|
||||||
)
|
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
|
||||||
try:
|
|
||||||
value = validate_value(self.datatype, value)
|
|
||||||
except (ValueError, TypeError) as e:
|
|
||||||
raise exc.InvalidInput(self.name, value, six.text_type(e))
|
|
||||||
dataholder = self._get_dataholder(instance)
|
|
||||||
if value is Unset:
|
|
||||||
if hasattr(dataholder, self.key):
|
|
||||||
delattr(dataholder, self.key)
|
|
||||||
else:
|
|
||||||
setattr(dataholder, self.key, value)
|
|
||||||
|
|
||||||
def __delete__(self, instance):
|
|
||||||
self.__set__(instance, Unset)
|
|
||||||
|
|
||||||
def _get_datatype(self):
|
|
||||||
if isinstance(self._datatype, tuple):
|
|
||||||
self._datatype = \
|
|
||||||
self.complextype().__registry__.resolve_type(self._datatype[0])
|
|
||||||
if isinstance(self._datatype, weakref.ref):
|
|
||||||
return self._datatype()
|
|
||||||
if isinstance(self._datatype, list):
|
|
||||||
return [
|
|
||||||
item() if isinstance(item, weakref.ref) else item
|
|
||||||
for item in self._datatype
|
|
||||||
]
|
|
||||||
return self._datatype
|
|
||||||
|
|
||||||
def _set_datatype(self, datatype):
|
|
||||||
self._datatype = datatype
|
|
||||||
|
|
||||||
#: attribute data type. Can be either an actual type,
|
|
||||||
#: or a type name, in which case the actual type will be
|
|
||||||
#: determined when needed (generally just before scanning the api).
|
|
||||||
datatype = property(_get_datatype, _set_datatype)
|
|
||||||
|
|
||||||
|
|
||||||
def iswsattr(attr):
|
|
||||||
if inspect.isfunction(attr) or inspect.ismethod(attr):
|
|
||||||
return False
|
|
||||||
if isinstance(attr, property) and not isinstance(attr, wsproperty):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def sort_attributes(class_, attributes):
|
|
||||||
"""Sort a class attributes list.
|
|
||||||
|
|
||||||
3 mechanisms are attempted :
|
|
||||||
|
|
||||||
#. Look for a _wsme_attr_order attribute on the class_. This allow
|
|
||||||
to define an arbitrary order of the attributes (useful for
|
|
||||||
generated types).
|
|
||||||
|
|
||||||
#. Access the object source code to find the declaration order.
|
|
||||||
|
|
||||||
#. Sort by alphabetically"""
|
|
||||||
|
|
||||||
if not len(attributes):
|
|
||||||
return
|
|
||||||
|
|
||||||
attrs = dict((a.key, a) for a in attributes)
|
|
||||||
|
|
||||||
if hasattr(class_, '_wsme_attr_order'):
|
|
||||||
names_order = class_._wsme_attr_order
|
|
||||||
else:
|
|
||||||
names = attrs.keys()
|
|
||||||
names_order = []
|
|
||||||
try:
|
|
||||||
lines = []
|
|
||||||
for cls in inspect.getmro(class_):
|
|
||||||
if cls is object:
|
|
||||||
continue
|
|
||||||
lines[len(lines):] = inspect.getsourcelines(cls)[0]
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip().replace(" ", "")
|
|
||||||
if '=' in line:
|
|
||||||
aname = line[:line.index('=')]
|
|
||||||
if aname in names and aname not in names_order:
|
|
||||||
names_order.append(aname)
|
|
||||||
if len(names_order) < len(names):
|
|
||||||
names_order.extend((
|
|
||||||
name for name in names if name not in names_order))
|
|
||||||
assert len(names_order) == len(names)
|
|
||||||
except (TypeError, IOError):
|
|
||||||
names_order = list(names)
|
|
||||||
names_order.sort()
|
|
||||||
|
|
||||||
attributes[:] = [attrs[name] for name in names_order]
|
|
||||||
|
|
||||||
|
|
||||||
def inspect_class(class_):
|
|
||||||
"""Extract a list of (name, wsattr|wsproperty) for the given class_"""
|
|
||||||
attributes = []
|
|
||||||
for name, attr in inspect.getmembers(class_, iswsattr):
|
|
||||||
if name.startswith('_'):
|
|
||||||
continue
|
|
||||||
if inspect.isroutine(attr):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(attr, (wsattr, wsproperty)):
|
|
||||||
attrdef = attr
|
|
||||||
else:
|
|
||||||
if attr not in native_types and (
|
|
||||||
inspect.isclass(attr) or
|
|
||||||
isinstance(attr, (list, dict))):
|
|
||||||
register_type(attr)
|
|
||||||
attrdef = getattr(class_, '__wsattrclass__', wsattr)(attr)
|
|
||||||
|
|
||||||
attrdef.key = name
|
|
||||||
if attrdef.name is None:
|
|
||||||
attrdef.name = name
|
|
||||||
attrdef.complextype = weakref.ref(class_)
|
|
||||||
attributes.append(attrdef)
|
|
||||||
setattr(class_, name, attrdef)
|
|
||||||
|
|
||||||
sort_attributes(class_, attributes)
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
|
|
||||||
def list_attributes(class_):
|
|
||||||
"""
|
|
||||||
Returns a list of a complex type attributes.
|
|
||||||
"""
|
|
||||||
if not iscomplex(class_):
|
|
||||||
raise TypeError("%s is not a registered type")
|
|
||||||
return class_._wsme_attributes
|
|
||||||
|
|
||||||
|
|
||||||
def make_dataholder(class_):
|
|
||||||
# the slots are computed outside the class scope to avoid
|
|
||||||
# 'attr' to pullute the class namespace, which leads to weird
|
|
||||||
# things if one of the slots is named 'attr'.
|
|
||||||
slots = [attr.key for attr in class_._wsme_attributes]
|
|
||||||
|
|
||||||
class DataHolder(object):
|
|
||||||
__slots__ = slots
|
|
||||||
|
|
||||||
DataHolder.__name__ = class_.__name__ + 'DataHolder'
|
|
||||||
return DataHolder
|
|
||||||
|
|
||||||
|
|
||||||
class Registry(object):
|
|
||||||
def __init__(self):
|
|
||||||
self._complex_types = []
|
|
||||||
self.array_types = set()
|
|
||||||
self.dict_types = set()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def complex_types(self):
|
|
||||||
return [t() for t in self._complex_types if t()]
|
|
||||||
|
|
||||||
def register(self, class_):
|
|
||||||
"""
|
|
||||||
Make sure a type is registered.
|
|
||||||
|
|
||||||
It is automatically called by :class:`expose() <wsme.expose>`
|
|
||||||
and :class:`validate() <wsme.validate>`.
|
|
||||||
Unless you want to control when the class inspection is done there
|
|
||||||
is no need to call it.
|
|
||||||
"""
|
|
||||||
if class_ is None or \
|
|
||||||
class_ in native_types or \
|
|
||||||
isusertype(class_) or iscomplex(class_) or \
|
|
||||||
isarray(class_) or isdict(class_):
|
|
||||||
return class_
|
|
||||||
|
|
||||||
if isinstance(class_, list):
|
|
||||||
if len(class_) != 1:
|
|
||||||
raise ValueError("Cannot register type %s" % repr(class_))
|
|
||||||
dt = ArrayType(class_[0])
|
|
||||||
self.register(dt.item_type)
|
|
||||||
self.array_types.add(dt)
|
|
||||||
return dt
|
|
||||||
|
|
||||||
if isinstance(class_, dict):
|
|
||||||
if len(class_) != 1:
|
|
||||||
raise ValueError("Cannot register type %s" % repr(class_))
|
|
||||||
dt = DictType(*list(class_.items())[0])
|
|
||||||
self.register(dt.value_type)
|
|
||||||
self.dict_types.add(dt)
|
|
||||||
return dt
|
|
||||||
|
|
||||||
class_._wsme_attributes = None
|
|
||||||
class_._wsme_attributes = inspect_class(class_)
|
|
||||||
class_._wsme_DataHolderClass = make_dataholder(class_)
|
|
||||||
|
|
||||||
class_.__registry__ = self
|
|
||||||
self._complex_types.append(weakref.ref(class_))
|
|
||||||
return class_
|
|
||||||
|
|
||||||
def reregister(self, class_):
|
|
||||||
"""Register a type which may already have been registered.
|
|
||||||
"""
|
|
||||||
self._unregister(class_)
|
|
||||||
return self.register(class_)
|
|
||||||
|
|
||||||
def _unregister(self, class_):
|
|
||||||
"""Remove a previously registered type.
|
|
||||||
"""
|
|
||||||
# Clear the existing attribute reference so it is rebuilt if
|
|
||||||
# the class is registered again later.
|
|
||||||
if hasattr(class_, '_wsme_attributes'):
|
|
||||||
del class_._wsme_attributes
|
|
||||||
# FIXME(dhellmann): This method does not recurse through the
|
|
||||||
# types like register() does. Should it?
|
|
||||||
if isinstance(class_, list):
|
|
||||||
at = ArrayType(class_[0])
|
|
||||||
try:
|
|
||||||
self.array_types.remove(at)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
elif isinstance(class_, dict):
|
|
||||||
key_type, value_type = list(class_.items())[0]
|
|
||||||
self.dict_types = set(
|
|
||||||
dt for dt in self.dict_types
|
|
||||||
if (dt.key_type, dt.value_type) != (key_type, value_type)
|
|
||||||
)
|
|
||||||
# We can't use remove() here because the items in
|
|
||||||
# _complex_types are weakref objects pointing to the classes,
|
|
||||||
# so we can't compare with them directly.
|
|
||||||
self._complex_types = [
|
|
||||||
ct for ct in self._complex_types
|
|
||||||
if ct() is not class_
|
|
||||||
]
|
|
||||||
|
|
||||||
def lookup(self, typename):
|
|
||||||
log.debug('Lookup %s' % typename)
|
|
||||||
modname = None
|
|
||||||
if '.' in typename:
|
|
||||||
modname, typename = typename.rsplit('.', 1)
|
|
||||||
for ct in self._complex_types:
|
|
||||||
ct = ct()
|
|
||||||
if ct is not None and typename == ct.__name__ and (
|
|
||||||
modname is None or modname == ct.__module__):
|
|
||||||
return ct
|
|
||||||
|
|
||||||
def resolve_type(self, type_):
|
|
||||||
if isinstance(type_, six.string_types):
|
|
||||||
return self.lookup(type_)
|
|
||||||
if isinstance(type_, list):
|
|
||||||
type_ = ArrayType(type_[0])
|
|
||||||
if isinstance(type_, dict):
|
|
||||||
type_ = DictType(list(type_.keys())[0], list(type_.values())[0])
|
|
||||||
if isinstance(type_, ArrayType):
|
|
||||||
type_ = ArrayType(self.resolve_type(type_.item_type))
|
|
||||||
self.array_types.add(type_)
|
|
||||||
elif isinstance(type_, DictType):
|
|
||||||
type_ = DictType(
|
|
||||||
type_.key_type,
|
|
||||||
self.resolve_type(type_.value_type)
|
|
||||||
)
|
|
||||||
self.dict_types.add(type_)
|
|
||||||
else:
|
|
||||||
type_ = self.register(type_)
|
|
||||||
return type_
|
|
||||||
|
|
||||||
|
|
||||||
# Default type registry
|
|
||||||
registry = Registry()
|
|
||||||
|
|
||||||
|
|
||||||
def register_type(class_):
|
|
||||||
return registry.register(class_)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMeta(type):
|
|
||||||
def __new__(cls, name, bases, dct):
|
|
||||||
if bases and bases[0] is not object and '__registry__' not in dct:
|
|
||||||
dct['__registry__'] = registry
|
|
||||||
return type.__new__(cls, name, bases, dct)
|
|
||||||
|
|
||||||
def __init__(cls, name, bases, dct):
|
|
||||||
if bases and bases[0] is not object and cls.__registry__:
|
|
||||||
cls.__registry__.register(cls)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(six.with_metaclass(BaseMeta)):
|
|
||||||
"""Base type for complex types"""
|
|
||||||
def __init__(self, **kw):
|
|
||||||
for key, value in kw.items():
|
|
||||||
if hasattr(self, key):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
|
|
||||||
class File(Base):
|
|
||||||
"""A complex type that represents a file.
|
|
||||||
|
|
||||||
In the particular case of protocol accepting form encoded data as
|
|
||||||
input, File can be loaded from a form file field.
|
|
||||||
"""
|
|
||||||
#: The file name
|
|
||||||
filename = wsattr(text)
|
|
||||||
|
|
||||||
#: Mime type of the content
|
|
||||||
contenttype = wsattr(text)
|
|
||||||
|
|
||||||
def _get_content(self):
|
|
||||||
if self._content is None and self._file:
|
|
||||||
self._content = self._file.read()
|
|
||||||
return self._content
|
|
||||||
|
|
||||||
def _set_content(self, value):
|
|
||||||
self._content = value
|
|
||||||
self._file = None
|
|
||||||
|
|
||||||
#: File content
|
|
||||||
content = wsproperty(binary, _get_content, _set_content)
|
|
||||||
|
|
||||||
def __init__(self, filename=None, file=None, content=None,
|
|
||||||
contenttype=None, fieldstorage=None):
|
|
||||||
self.filename = filename
|
|
||||||
self.contenttype = contenttype
|
|
||||||
self._file = file
|
|
||||||
self._content = content
|
|
||||||
|
|
||||||
if fieldstorage is not None:
|
|
||||||
if fieldstorage.file:
|
|
||||||
self._file = fieldstorage.file
|
|
||||||
self.filename = fieldstorage.filename
|
|
||||||
self.contenttype = text(fieldstorage.type)
|
|
||||||
else:
|
|
||||||
self._content = fieldstorage.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def file(self):
|
|
||||||
if self._file is None and self._content:
|
|
||||||
self._file = six.BytesIO(self._content)
|
|
||||||
return self._file
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicBase(Base):
|
|
||||||
"""Base type for complex types for which all attributes are not
|
|
||||||
defined when the class is constructed.
|
|
||||||
|
|
||||||
This class is meant to be used as a base for types that have
|
|
||||||
properties added after the main class is created, such as by
|
|
||||||
loading plugins.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_attributes(cls, **attrs):
|
|
||||||
"""Add more attributes
|
|
||||||
|
|
||||||
The arguments should be valid Python attribute names
|
|
||||||
associated with a type for the new attribute.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for n, t in attrs.items():
|
|
||||||
setattr(cls, n, t)
|
|
||||||
cls.__registry__.reregister(cls)
|
|
||||||
118
wsme/utils.py
118
wsme/utils.py
@@ -1,118 +0,0 @@
|
|||||||
import decimal
|
|
||||||
import datetime
|
|
||||||
import pytz
|
|
||||||
import re
|
|
||||||
from six.moves import builtins, http_client
|
|
||||||
|
|
||||||
try:
|
|
||||||
import dateutil.parser
|
|
||||||
except:
|
|
||||||
dateutil = None # noqa
|
|
||||||
|
|
||||||
date_re = r'(?P<year>-?\d{4,})-(?P<month>\d{2})-(?P<day>\d{2})'
|
|
||||||
time_re = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})' + \
|
|
||||||
r'(\.(?P<sec_frac>\d+))?'
|
|
||||||
tz_re = r'((?P<tz_sign>[+-])(?P<tz_hour>\d{2}):(?P<tz_min>\d{2}))' + \
|
|
||||||
r'|(?P<tz_z>Z)'
|
|
||||||
|
|
||||||
datetime_re = re.compile(
|
|
||||||
'%sT%s(%s)?' % (date_re, time_re, tz_re))
|
|
||||||
date_re = re.compile(date_re)
|
|
||||||
time_re = re.compile('%s(%s)?' % (time_re, tz_re))
|
|
||||||
|
|
||||||
|
|
||||||
if hasattr(builtins, '_'):
|
|
||||||
_ = builtins._
|
|
||||||
else:
|
|
||||||
def _(s):
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def parse_isodate(value):
|
|
||||||
m = date_re.match(value)
|
|
||||||
if m is None:
|
|
||||||
raise ValueError("'%s' is not a legal date value" % (value))
|
|
||||||
try:
|
|
||||||
return datetime.date(
|
|
||||||
int(m.group('year')),
|
|
||||||
int(m.group('month')),
|
|
||||||
int(m.group('day')))
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError("'%s' is a out-of-range date" % (value))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_isotime(value):
|
|
||||||
m = time_re.match(value)
|
|
||||||
if m is None:
|
|
||||||
raise ValueError("'%s' is not a legal time value" % (value))
|
|
||||||
try:
|
|
||||||
ms = 0
|
|
||||||
if m.group('sec_frac') is not None:
|
|
||||||
f = decimal.Decimal('0.' + m.group('sec_frac'))
|
|
||||||
f = str(f.quantize(decimal.Decimal('0.000001')))
|
|
||||||
ms = int(f[2:])
|
|
||||||
tz = _parse_tzparts(m.groupdict())
|
|
||||||
return datetime.time(
|
|
||||||
int(m.group('hour')),
|
|
||||||
int(m.group('min')),
|
|
||||||
int(m.group('sec')),
|
|
||||||
ms,
|
|
||||||
tz)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError("'%s' is a out-of-range time" % (value))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_isodatetime(value):
|
|
||||||
if dateutil:
|
|
||||||
return dateutil.parser.parse(value)
|
|
||||||
m = datetime_re.match(value)
|
|
||||||
if m is None:
|
|
||||||
raise ValueError("'%s' is not a legal datetime value" % (value))
|
|
||||||
try:
|
|
||||||
ms = 0
|
|
||||||
if m.group('sec_frac') is not None:
|
|
||||||
f = decimal.Decimal('0.' + m.group('sec_frac'))
|
|
||||||
f = f.quantize(decimal.Decimal('0.000001'))
|
|
||||||
ms = int(str(f)[2:])
|
|
||||||
tz = _parse_tzparts(m.groupdict())
|
|
||||||
return datetime.datetime(
|
|
||||||
int(m.group('year')),
|
|
||||||
int(m.group('month')),
|
|
||||||
int(m.group('day')),
|
|
||||||
int(m.group('hour')),
|
|
||||||
int(m.group('min')),
|
|
||||||
int(m.group('sec')),
|
|
||||||
ms,
|
|
||||||
tz)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError("'%s' is a out-of-range datetime" % (value))
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_tzparts(parts):
|
|
||||||
if 'tz_z' in parts and parts['tz_z'] == 'Z':
|
|
||||||
return pytz.UTC
|
|
||||||
if 'tz_min' not in parts or not parts['tz_min']:
|
|
||||||
return None
|
|
||||||
|
|
||||||
tz_minute_offset = (int(parts['tz_hour']) * 60 + int(parts['tz_min']))
|
|
||||||
tz_multiplier = -1 if parts['tz_sign'] == '-' else 1
|
|
||||||
|
|
||||||
return pytz.FixedOffset(tz_multiplier * tz_minute_offset)
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_code(code_value):
|
|
||||||
"""
|
|
||||||
This function checks if incoming value in http response codes range.
|
|
||||||
"""
|
|
||||||
return code_value in http_client.responses
|
|
||||||
|
|
||||||
|
|
||||||
def is_client_error(code):
|
|
||||||
""" Checks client error code (RFC 2616)."""
|
|
||||||
return 400 <= code < 500
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from collections import OrderedDict
|
|
||||||
except ImportError:
|
|
||||||
from ordereddict import OrderedDict # noqa
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import pkg_resources
|
|
||||||
pkg_resources.declare_namespace(__name__)
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
"""
|
|
||||||
WSME for cornice
|
|
||||||
|
|
||||||
|
|
||||||
Activate it::
|
|
||||||
|
|
||||||
config.include('wsme.cornice')
|
|
||||||
|
|
||||||
|
|
||||||
And use it::
|
|
||||||
|
|
||||||
@hello.get()
|
|
||||||
@wsexpose(Message, wsme.types.text)
|
|
||||||
def get_hello(who=u'World'):
|
|
||||||
return Message(text='Hello %s' % who)
|
|
||||||
"""
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import wsme
|
|
||||||
from wsme.rest import json as restjson
|
|
||||||
from wsme.rest import xml as restxml
|
|
||||||
import wsme.runtime
|
|
||||||
import wsme.api
|
|
||||||
import functools
|
|
||||||
|
|
||||||
from wsme.rest.args import (
|
|
||||||
args_from_args, args_from_params, args_from_body, combine_args
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WSMEJsonRenderer(object):
|
|
||||||
def __init__(self, info):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __call__(self, data, context):
|
|
||||||
response = context['request'].response
|
|
||||||
response.content_type = 'application/json'
|
|
||||||
if 'faultcode' in data:
|
|
||||||
if 'orig_code' in data:
|
|
||||||
response.status_code = data['orig_code']
|
|
||||||
elif data['faultcode'] == 'Client':
|
|
||||||
response.status_code = 400
|
|
||||||
else:
|
|
||||||
response.status_code = 500
|
|
||||||
return restjson.encode_error(None, data)
|
|
||||||
obj = data['result']
|
|
||||||
if isinstance(obj, wsme.api.Response):
|
|
||||||
response.status_code = obj.status_code
|
|
||||||
if obj.error:
|
|
||||||
return restjson.encode_error(None, obj.error)
|
|
||||||
obj = obj.obj
|
|
||||||
return restjson.encode_result(obj, data['datatype'])
|
|
||||||
|
|
||||||
|
|
||||||
class WSMEXmlRenderer(object):
|
|
||||||
def __init__(self, info):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __call__(self, data, context):
|
|
||||||
response = context['request'].response
|
|
||||||
if 'faultcode' in data:
|
|
||||||
if data['faultcode'] == 'Client':
|
|
||||||
response.status_code = 400
|
|
||||||
else:
|
|
||||||
response.status_code = 500
|
|
||||||
return restxml.encode_error(None, data)
|
|
||||||
response.content_type = 'text/xml'
|
|
||||||
return restxml.encode_result(data['result'], data['datatype'])
|
|
||||||
|
|
||||||
|
|
||||||
def get_outputformat(request):
|
|
||||||
df = None
|
|
||||||
if 'Accept' in request.headers:
|
|
||||||
if 'application/json' in request.headers['Accept']:
|
|
||||||
df = 'json'
|
|
||||||
elif 'text/xml' in request.headers['Accept']:
|
|
||||||
df = 'xml'
|
|
||||||
if df is None and 'Content-Type' in request.headers:
|
|
||||||
if 'application/json' in request.headers['Content-Type']:
|
|
||||||
df = 'json'
|
|
||||||
elif 'text/xml' in request.headers['Content-Type']:
|
|
||||||
df = 'xml'
|
|
||||||
return df if df else 'json'
|
|
||||||
|
|
||||||
|
|
||||||
def signature(*args, **kwargs):
|
|
||||||
sig = wsme.signature(*args, **kwargs)
|
|
||||||
|
|
||||||
def decorate(f):
|
|
||||||
args = inspect.getargspec(f)[0]
|
|
||||||
with_self = args[0] == 'self' if args else False
|
|
||||||
f = sig(f)
|
|
||||||
funcdef = wsme.api.FunctionDefinition.get(f)
|
|
||||||
funcdef.resolve_types(wsme.types.registry)
|
|
||||||
|
|
||||||
@functools.wraps(f)
|
|
||||||
def callfunction(*args):
|
|
||||||
if with_self:
|
|
||||||
if len(args) == 1:
|
|
||||||
self = args[0]
|
|
||||||
request = self.request
|
|
||||||
elif len(args) == 2:
|
|
||||||
self, request = args
|
|
||||||
else:
|
|
||||||
raise ValueError("Cannot do anything with these arguments")
|
|
||||||
else:
|
|
||||||
request = args[0]
|
|
||||||
request.override_renderer = 'wsme' + get_outputformat(request)
|
|
||||||
try:
|
|
||||||
args, kwargs = combine_args(funcdef, (
|
|
||||||
args_from_args(funcdef, (), request.matchdict),
|
|
||||||
args_from_params(funcdef, request.params),
|
|
||||||
args_from_body(funcdef, request.body, request.content_type)
|
|
||||||
))
|
|
||||||
wsme.runtime.check_arguments(funcdef, args, kwargs)
|
|
||||||
if funcdef.pass_request:
|
|
||||||
kwargs[funcdef.pass_request] = request
|
|
||||||
if with_self:
|
|
||||||
args.insert(0, self)
|
|
||||||
|
|
||||||
result = f(*args, **kwargs)
|
|
||||||
return {
|
|
||||||
'datatype': funcdef.return_type,
|
|
||||||
'result': result
|
|
||||||
}
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
exception_info = sys.exc_info()
|
|
||||||
orig_exception = exception_info[1]
|
|
||||||
orig_code = getattr(orig_exception, 'code', None)
|
|
||||||
data = wsme.api.format_exception(exception_info)
|
|
||||||
if orig_code is not None:
|
|
||||||
data['orig_code'] = orig_code
|
|
||||||
return data
|
|
||||||
finally:
|
|
||||||
del exception_info
|
|
||||||
|
|
||||||
callfunction.wsme_func = f
|
|
||||||
return callfunction
|
|
||||||
return decorate
|
|
||||||
|
|
||||||
|
|
||||||
def scan_api(root=None):
|
|
||||||
from cornice.service import get_services
|
|
||||||
for service in get_services():
|
|
||||||
for method, func, options in service.definitions:
|
|
||||||
wsme_func = getattr(func, 'wsme_func')
|
|
||||||
basepath = service.path.split('/')
|
|
||||||
if basepath and not basepath[0]:
|
|
||||||
del basepath[0]
|
|
||||||
if wsme_func:
|
|
||||||
yield (
|
|
||||||
basepath + [method.lower()],
|
|
||||||
wsme_func._wsme_definition
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
import pyramid.wsgi
|
|
||||||
wsroot = wsme.WSRoot(scan_api=scan_api, webpath='/ws')
|
|
||||||
wsroot.addprotocol('extdirect')
|
|
||||||
config.add_renderer('wsmejson', WSMEJsonRenderer)
|
|
||||||
config.add_renderer('wsmexml', WSMEXmlRenderer)
|
|
||||||
config.add_route('wsme', '/ws/*path')
|
|
||||||
config.add_view(pyramid.wsgi.wsgiapp(wsroot.wsgiapp()), route_name='wsme')
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from wsmeext.extdirect.protocol import ExtDirectProtocol # noqa
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import wsme
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class ReadResultBase(wsme.types.Base):
|
|
||||||
total = int
|
|
||||||
success = bool
|
|
||||||
message = wsme.types.text
|
|
||||||
|
|
||||||
|
|
||||||
def make_readresult(datatype):
|
|
||||||
ReadResult = type(
|
|
||||||
datatype.__name__ + 'ReadResult',
|
|
||||||
(ReadResultBase,), {
|
|
||||||
'data': [datatype]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return ReadResult
|
|
||||||
|
|
||||||
|
|
||||||
class DataStoreControllerMeta(type):
|
|
||||||
def __init__(cls, name, bases, dct):
|
|
||||||
if cls.__datatype__ is None:
|
|
||||||
return
|
|
||||||
if getattr(cls, '__readresulttype__', None) is None:
|
|
||||||
cls.__readresulttype__ = make_readresult(cls.__datatype__)
|
|
||||||
|
|
||||||
cls.create = wsme.expose(
|
|
||||||
cls.__readresulttype__,
|
|
||||||
extdirect_params_notation='positional')(cls.create)
|
|
||||||
cls.create = wsme.validate(cls.__datatype__)(cls.create)
|
|
||||||
|
|
||||||
cls.read = wsme.expose(
|
|
||||||
cls.__readresulttype__,
|
|
||||||
extdirect_params_notation='named')(cls.read)
|
|
||||||
cls.read = wsme.validate(str, str, int, int, int)(cls.read)
|
|
||||||
|
|
||||||
cls.update = wsme.expose(
|
|
||||||
cls.__readresulttype__,
|
|
||||||
extdirect_params_notation='positional')(cls.update)
|
|
||||||
cls.update = wsme.validate(cls.__datatype__)(cls.update)
|
|
||||||
|
|
||||||
cls.destroy = wsme.expose(
|
|
||||||
cls.__readresulttype__,
|
|
||||||
extdirect_params_notation='positional')(cls.destroy)
|
|
||||||
cls.destroy = wsme.validate(cls.__idtype__)(cls.destroy)
|
|
||||||
|
|
||||||
|
|
||||||
class DataStoreControllerMixin(object):
|
|
||||||
__datatype__ = None
|
|
||||||
__idtype__ = int
|
|
||||||
|
|
||||||
__readresulttype__ = None
|
|
||||||
|
|
||||||
def create(self, obj):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def read(self, query=None, sort=None, page=None, start=None, limit=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update(self, obj):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def destroy(self, obj_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def model(self):
|
|
||||||
tpl = """
|
|
||||||
Ext.define('%(appns)s.model.%(classname)s', {
|
|
||||||
extend: 'Ext.data.Model',
|
|
||||||
fields: %(fields)s,
|
|
||||||
|
|
||||||
proxy: {
|
|
||||||
type: 'direct',
|
|
||||||
api: {
|
|
||||||
create: %(appns)s.%(controllerns)s.create,
|
|
||||||
read: %(appns)s.%(controllerns)s.read,
|
|
||||||
update: %(appns)s.%(controllerns)s.update,
|
|
||||||
destroy: %(appns)s.%(controllerns)s.destroy
|
|
||||||
},
|
|
||||||
reader: {
|
|
||||||
root: 'data'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
fields = [
|
|
||||||
attr.name for attr in self.__datatype__._wsme_attributes
|
|
||||||
]
|
|
||||||
d = {
|
|
||||||
'appns': 'Demo',
|
|
||||||
'controllerns': 'stores.' + self.__datatype__.__name__.lower(),
|
|
||||||
'classname': self.__datatype__.__name__,
|
|
||||||
'fields': json.dumps(fields)
|
|
||||||
}
|
|
||||||
return tpl % d
|
|
||||||
|
|
||||||
def store(self):
|
|
||||||
tpl = """
|
|
||||||
Ext.define('%(appns)s.store.%(classname)s', {
|
|
||||||
extend: 'Ext.data.Store',
|
|
||||||
model: '%(appns)s.model.%(classname)s'
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
d = {
|
|
||||||
'appns': 'Demo',
|
|
||||||
'classname': self.__datatype__.__name__,
|
|
||||||
}
|
|
||||||
|
|
||||||
return tpl % d
|
|
||||||
|
|
||||||
|
|
||||||
DataStoreController = DataStoreControllerMeta(
|
|
||||||
'DataStoreController',
|
|
||||||
(DataStoreControllerMixin,), {}
|
|
||||||
)
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
|
|
||||||
from simplegeneric import generic
|
|
||||||
|
|
||||||
from wsme.exc import ClientSideError
|
|
||||||
from wsme.protocol import CallContext, Protocol, expose
|
|
||||||
from wsme.utils import parse_isodate, parse_isodatetime, parse_isotime
|
|
||||||
from wsme.rest.args import from_params
|
|
||||||
from wsme.types import iscomplex, isusertype, list_attributes, Unset
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json # noqa
|
|
||||||
|
|
||||||
from six import u
|
|
||||||
|
|
||||||
|
|
||||||
class APIDefinitionGenerator(object):
|
|
||||||
tpl = """\
|
|
||||||
Ext.ns("%(rootns)s");
|
|
||||||
|
|
||||||
if (!%(rootns)s.wsroot) {
|
|
||||||
%(rootns)s.wsroot = "%(webpath)s.
|
|
||||||
}
|
|
||||||
|
|
||||||
%(descriptors)s
|
|
||||||
|
|
||||||
Ext.syncRequire(['Ext.direct.*'], function() {
|
|
||||||
%(providers)s
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
descriptor_tpl = """\
|
|
||||||
Ext.ns("%(fullns)s");
|
|
||||||
|
|
||||||
%(fullns)s.Descriptor = {
|
|
||||||
"url": %(rootns)s.wsroot + "extdirect/router/%(ns)s",
|
|
||||||
"namespace": "%(fullns)s",
|
|
||||||
"type": "remoting",
|
|
||||||
"actions": %(actions)s
|
|
||||||
"enableBuffer": true
|
|
||||||
};
|
|
||||||
"""
|
|
||||||
provider_tpl = """\
|
|
||||||
Ext.direct.Manager.addProvider(%(fullns)s.Descriptor);
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def render(self, rootns, webpath, namespaces, fullns):
|
|
||||||
descriptors = u('')
|
|
||||||
for ns in sorted(namespaces):
|
|
||||||
descriptors += self.descriptor_tpl % {
|
|
||||||
'ns': ns,
|
|
||||||
'rootns': rootns,
|
|
||||||
'fullns': fullns(ns),
|
|
||||||
'actions': '\n'.join((
|
|
||||||
' ' * 4 + line
|
|
||||||
for line
|
|
||||||
in json.dumps(namespaces[ns], indent=4).split('\n')
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
providers = u('')
|
|
||||||
for ns in sorted(namespaces):
|
|
||||||
providers += self.provider_tpl % {
|
|
||||||
'fullns': fullns(ns)
|
|
||||||
}
|
|
||||||
|
|
||||||
r = self.tpl % {
|
|
||||||
'rootns': rootns,
|
|
||||||
'webpath': webpath,
|
|
||||||
'descriptors': descriptors,
|
|
||||||
'providers': providers,
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if iscomplex(datatype):
|
|
||||||
newvalue = datatype()
|
|
||||||
for attrdef in list_attributes(datatype):
|
|
||||||
if attrdef.name in value:
|
|
||||||
setattr(newvalue, attrdef.key,
|
|
||||||
fromjson(attrdef.datatype, value[attrdef.name]))
|
|
||||||
value = newvalue
|
|
||||||
elif isusertype(datatype):
|
|
||||||
value = datatype.frombasetype(fromjson(datatype.basetype, value))
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
if iscomplex(datatype):
|
|
||||||
d = {}
|
|
||||||
for attrdef in list_attributes(datatype):
|
|
||||||
attrvalue = getattr(value, attrdef.key)
|
|
||||||
if attrvalue is not Unset:
|
|
||||||
d[attrdef.name] = tojson(attrdef.datatype, attrvalue)
|
|
||||||
value = d
|
|
||||||
elif isusertype(datatype):
|
|
||||||
value = tojson(datatype.basetype, datatype.tobasetype(value))
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_type(wsme.types.ArrayType)
|
|
||||||
def array_fromjson(datatype, value):
|
|
||||||
return [fromjson(datatype.item_type, item) for item in value]
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_type(wsme.types.ArrayType)
|
|
||||||
def array_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return [tojson(datatype.item_type, item) for item in value]
|
|
||||||
|
|
||||||
|
|
||||||
@fromjson.when_type(wsme.types.DictType)
|
|
||||||
def dict_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return dict((
|
|
||||||
(fromjson(datatype.key_type, key),
|
|
||||||
fromjson(datatype.value_type, value))
|
|
||||||
for key, value in value.items()
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_type(wsme.types.DictType)
|
|
||||||
def dict_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return dict((
|
|
||||||
(tojson(datatype.key_type, key),
|
|
||||||
tojson(datatype.value_type, value))
|
|
||||||
for key, value in value.items()
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(wsme.types.bytes)
|
|
||||||
def bytes_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return value.decode('ascii')
|
|
||||||
|
|
||||||
|
|
||||||
# raw strings
|
|
||||||
@fromjson.when_object(wsme.types.bytes)
|
|
||||||
def bytes_fromjson(datatype, value):
|
|
||||||
if value is not None:
|
|
||||||
value = value.encode('ascii')
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
# unicode strings
|
|
||||||
|
|
||||||
@fromjson.when_object(wsme.types.text)
|
|
||||||
def text_fromjson(datatype, value):
|
|
||||||
if isinstance(value, wsme.types.bytes):
|
|
||||||
return value.decode('utf-8')
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
# datetime.time
|
|
||||||
|
|
||||||
@fromjson.when_object(datetime.time)
|
|
||||||
def time_fromjson(datatype, value):
|
|
||||||
if value is None or value == '':
|
|
||||||
return None
|
|
||||||
return parse_isotime(value)
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(datetime.time)
|
|
||||||
def time_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return value.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
# datetime.date
|
|
||||||
|
|
||||||
@fromjson.when_object(datetime.date)
|
|
||||||
def date_fromjson(datatype, value):
|
|
||||||
if value is None or value == '':
|
|
||||||
return None
|
|
||||||
return parse_isodate(value)
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(datetime.date)
|
|
||||||
def date_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return value.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
# datetime.datetime
|
|
||||||
|
|
||||||
@fromjson.when_object(datetime.datetime)
|
|
||||||
def datetime_fromjson(datatype, value):
|
|
||||||
if value is None or value == '':
|
|
||||||
return None
|
|
||||||
return parse_isodatetime(value)
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(datetime.datetime)
|
|
||||||
def datetime_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return value.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
# decimal.Decimal
|
|
||||||
|
|
||||||
@fromjson.when_object(decimal.Decimal)
|
|
||||||
def decimal_fromjson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return decimal.Decimal(value)
|
|
||||||
|
|
||||||
|
|
||||||
@tojson.when_object(decimal.Decimal)
|
|
||||||
def decimal_tojson(datatype, value):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
|
|
||||||
class ExtCallContext(CallContext):
|
|
||||||
def __init__(self, request, namespace, calldata):
|
|
||||||
super(ExtCallContext, self).__init__(request)
|
|
||||||
self.namespace = namespace
|
|
||||||
|
|
||||||
self.tid = calldata['tid']
|
|
||||||
self.action = calldata['action']
|
|
||||||
self.method = calldata['method']
|
|
||||||
self.params = calldata['data']
|
|
||||||
|
|
||||||
|
|
||||||
class FormExtCallContext(CallContext):
|
|
||||||
def __init__(self, request, namespace):
|
|
||||||
super(FormExtCallContext, self).__init__(request)
|
|
||||||
self.namespace = namespace
|
|
||||||
|
|
||||||
self.tid = request.params['extTID']
|
|
||||||
self.action = request.params['extAction']
|
|
||||||
self.method = request.params['extMethod']
|
|
||||||
self.params = []
|
|
||||||
|
|
||||||
|
|
||||||
class ExtDirectProtocol(Protocol):
|
|
||||||
"""
|
|
||||||
ExtDirect protocol.
|
|
||||||
|
|
||||||
For more detail on the protocol, see
|
|
||||||
http://www.sencha.com/products/extjs/extdirect.
|
|
||||||
|
|
||||||
.. autoattribute:: name
|
|
||||||
.. autoattribute:: content_types
|
|
||||||
"""
|
|
||||||
name = 'extdirect'
|
|
||||||
displayname = 'ExtDirect'
|
|
||||||
content_types = ['application/json', 'text/javascript']
|
|
||||||
|
|
||||||
def __init__(self, namespace='', params_notation='named', nsfolder=None):
|
|
||||||
self.namespace = namespace
|
|
||||||
self.appns, self.apins = namespace.rsplit('.', 2) \
|
|
||||||
if '.' in namespace else (namespace, '')
|
|
||||||
self.default_params_notation = params_notation
|
|
||||||
self.appnsfolder = nsfolder
|
|
||||||
|
|
||||||
@property
|
|
||||||
def api_alias(self):
|
|
||||||
if self.appnsfolder:
|
|
||||||
alias = '/%s/%s.js' % (
|
|
||||||
self.appnsfolder,
|
|
||||||
self.apins.replace('.', '/'))
|
|
||||||
return alias
|
|
||||||
|
|
||||||
def accept(self, req):
|
|
||||||
path = req.path
|
|
||||||
assert path.startswith(self.root._webpath)
|
|
||||||
path = path[len(self.root._webpath):]
|
|
||||||
|
|
||||||
return (
|
|
||||||
path == self.api_alias or
|
|
||||||
path == "/extdirect/api" or
|
|
||||||
path.startswith("/extdirect/router")
|
|
||||||
)
|
|
||||||
|
|
||||||
def iter_calls(self, req):
|
|
||||||
path = req.path
|
|
||||||
|
|
||||||
assert path.startswith(self.root._webpath)
|
|
||||||
path = path[len(self.root._webpath):].strip()
|
|
||||||
|
|
||||||
assert path.startswith('/extdirect/router'), path
|
|
||||||
path = path[17:].strip('/')
|
|
||||||
|
|
||||||
if path:
|
|
||||||
namespace = path.split('.')
|
|
||||||
else:
|
|
||||||
namespace = []
|
|
||||||
|
|
||||||
if 'extType' in req.params:
|
|
||||||
req.wsme_extdirect_batchcall = False
|
|
||||||
yield FormExtCallContext(req, namespace)
|
|
||||||
else:
|
|
||||||
data = json.loads(req.body.decode('utf8'))
|
|
||||||
req.wsme_extdirect_batchcall = isinstance(data, list)
|
|
||||||
if not req.wsme_extdirect_batchcall:
|
|
||||||
data = [data]
|
|
||||||
req.callcount = len(data)
|
|
||||||
|
|
||||||
for call in data:
|
|
||||||
yield ExtCallContext(req, namespace, call)
|
|
||||||
|
|
||||||
def extract_path(self, context):
|
|
||||||
path = list(context.namespace)
|
|
||||||
|
|
||||||
if context.action:
|
|
||||||
path.append(context.action)
|
|
||||||
|
|
||||||
path.append(context.method)
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
def read_std_arguments(self, context):
|
|
||||||
funcdef = context.funcdef
|
|
||||||
notation = funcdef.extra_options.get('extdirect_params_notation',
|
|
||||||
self.default_params_notation)
|
|
||||||
args = context.params
|
|
||||||
if notation == 'positional':
|
|
||||||
kw = dict(
|
|
||||||
(argdef.name, fromjson(argdef.datatype, arg))
|
|
||||||
for argdef, arg in zip(funcdef.arguments, args)
|
|
||||||
)
|
|
||||||
elif notation == 'named':
|
|
||||||
if len(args) == 0:
|
|
||||||
args = [{}]
|
|
||||||
elif len(args) > 1:
|
|
||||||
raise ClientSideError(
|
|
||||||
"Named arguments: takes a single object argument")
|
|
||||||
args = args[0]
|
|
||||||
kw = dict(
|
|
||||||
(argdef.name, fromjson(argdef.datatype, args[argdef.name]))
|
|
||||||
for argdef in funcdef.arguments if argdef.name in args
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid notation: %s" % notation)
|
|
||||||
return kw
|
|
||||||
|
|
||||||
def read_form_arguments(self, context):
|
|
||||||
kw = {}
|
|
||||||
for argdef in context.funcdef.arguments:
|
|
||||||
value = from_params(argdef.datatype, context.request.params,
|
|
||||||
argdef.name, set())
|
|
||||||
if value is not Unset:
|
|
||||||
kw[argdef.name] = value
|
|
||||||
return kw
|
|
||||||
|
|
||||||
def read_arguments(self, context):
|
|
||||||
if isinstance(context, ExtCallContext):
|
|
||||||
kwargs = self.read_std_arguments(context)
|
|
||||||
elif isinstance(context, FormExtCallContext):
|
|
||||||
kwargs = self.read_form_arguments(context)
|
|
||||||
wsme.runtime.check_arguments(context.funcdef, (), kwargs)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def encode_result(self, context, result):
|
|
||||||
return json.dumps({
|
|
||||||
'type': 'rpc',
|
|
||||||
'tid': context.tid,
|
|
||||||
'action': context.action,
|
|
||||||
'method': context.method,
|
|
||||||
'result': tojson(context.funcdef.return_type, result)
|
|
||||||
})
|
|
||||||
|
|
||||||
def encode_error(self, context, infos):
|
|
||||||
return json.dumps({
|
|
||||||
'type': 'exception',
|
|
||||||
'tid': context.tid,
|
|
||||||
'action': context.action,
|
|
||||||
'method': context.method,
|
|
||||||
'message': '%(faultcode)s: %(faultstring)s' % infos,
|
|
||||||
'where': infos['debuginfo']})
|
|
||||||
|
|
||||||
def prepare_response_body(self, request, results):
|
|
||||||
r = ",\n".join(results)
|
|
||||||
if request.wsme_extdirect_batchcall:
|
|
||||||
return "[\n%s\n]" % r
|
|
||||||
else:
|
|
||||||
return r
|
|
||||||
|
|
||||||
def get_response_status(self, request):
|
|
||||||
return 200
|
|
||||||
|
|
||||||
def get_response_contenttype(self, request):
|
|
||||||
return "text/javascript"
|
|
||||||
|
|
||||||
def fullns(self, ns):
|
|
||||||
return ns and '%s.%s' % (self.namespace, ns) or self.namespace
|
|
||||||
|
|
||||||
@expose('/extdirect/api', "text/javascript")
|
|
||||||
@expose('${api_alias}', "text/javascript")
|
|
||||||
def api(self):
|
|
||||||
namespaces = {}
|
|
||||||
for path, funcdef in self.root.getapi():
|
|
||||||
if len(path) > 1:
|
|
||||||
namespace = '.'.join(path[:-2])
|
|
||||||
action = path[-2]
|
|
||||||
else:
|
|
||||||
namespace = ''
|
|
||||||
action = ''
|
|
||||||
if namespace not in namespaces:
|
|
||||||
namespaces[namespace] = {}
|
|
||||||
if action not in namespaces[namespace]:
|
|
||||||
namespaces[namespace][action] = []
|
|
||||||
notation = funcdef.extra_options.get('extdirect_params_notation',
|
|
||||||
self.default_params_notation)
|
|
||||||
method = {
|
|
||||||
'name': funcdef.name}
|
|
||||||
|
|
||||||
if funcdef.extra_options.get('extdirect_formhandler', False):
|
|
||||||
method['formHandler'] = True
|
|
||||||
method['len'] = 1 if notation == 'named' \
|
|
||||||
else len(funcdef.arguments)
|
|
||||||
namespaces[namespace][action].append(method)
|
|
||||||
webpath = self.root._webpath
|
|
||||||
if webpath and not webpath.endswith('/'):
|
|
||||||
webpath += '/'
|
|
||||||
return APIDefinitionGenerator().render(
|
|
||||||
namespaces=namespaces,
|
|
||||||
webpath=webpath,
|
|
||||||
rootns=self.namespace,
|
|
||||||
fullns=self.fullns,
|
|
||||||
)
|
|
||||||
|
|
||||||
def encode_sample_value(self, datatype, value, format=False):
|
|
||||||
r = tojson(datatype, value)
|
|
||||||
content = json.dumps(r, ensure_ascii=False, indent=4 if format else 0,
|
|
||||||
sort_keys=format)
|
|
||||||
return ('javascript', content)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from wsmeext.extdirect import datastore
|
|
||||||
|
|
||||||
|
|
||||||
class SADataStoreController(datastore.DataStoreController):
|
|
||||||
__dbsession__ = None
|
|
||||||
__datatype__ = None
|
|
||||||
|
|
||||||
def read(self, query=None, sort=None, page=None, start=None, limit=None):
|
|
||||||
q = self.__dbsession__.query(self.__datatype__.__saclass__)
|
|
||||||
total = q.count()
|
|
||||||
if start is not None and limit is not None:
|
|
||||||
q = q.slice(start, limit)
|
|
||||||
return self.__readresulttype__(
|
|
||||||
data=[
|
|
||||||
self.__datatype__(o) for o in q
|
|
||||||
],
|
|
||||||
success=True,
|
|
||||||
total=total
|
|
||||||
)
|
|
||||||
108
wsmeext/flask.py
108
wsmeext/flask.py
@@ -1,108 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
import wsme
|
|
||||||
import wsme.api
|
|
||||||
import wsme.rest.json
|
|
||||||
import wsme.rest.xml
|
|
||||||
import wsme.rest.args
|
|
||||||
from wsme.utils import is_valid_code
|
|
||||||
|
|
||||||
import flask
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
TYPES = {
|
|
||||||
'application/json': wsme.rest.json,
|
|
||||||
'application/xml': wsme.rest.xml,
|
|
||||||
'text/xml': wsme.rest.xml
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_dataformat():
|
|
||||||
if 'Accept' in flask.request.headers:
|
|
||||||
for t in TYPES:
|
|
||||||
if t in flask.request.headers['Accept']:
|
|
||||||
return TYPES[t]
|
|
||||||
|
|
||||||
# Look for the wanted data format in the request.
|
|
||||||
req_dataformat = getattr(flask.request, 'response_type', None)
|
|
||||||
if req_dataformat in TYPES:
|
|
||||||
return TYPES[req_dataformat]
|
|
||||||
|
|
||||||
log.info('''Could not determine what format is wanted by the
|
|
||||||
caller, falling back to json''')
|
|
||||||
return wsme.rest.json
|
|
||||||
|
|
||||||
|
|
||||||
def signature(*args, **kw):
|
|
||||||
sig = wsme.signature(*args, **kw)
|
|
||||||
|
|
||||||
def decorator(f):
|
|
||||||
args = inspect.getargspec(f)[0]
|
|
||||||
ismethod = args and args[0] == 'self'
|
|
||||||
sig(f)
|
|
||||||
funcdef = wsme.api.FunctionDefinition.get(f)
|
|
||||||
funcdef.resolve_types(wsme.types.registry)
|
|
||||||
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if ismethod:
|
|
||||||
self, args = args[0], args[1:]
|
|
||||||
args, kwargs = wsme.rest.args.get_args(
|
|
||||||
funcdef, args, kwargs,
|
|
||||||
flask.request.args, flask.request.form,
|
|
||||||
flask.request.data,
|
|
||||||
flask.request.mimetype
|
|
||||||
)
|
|
||||||
|
|
||||||
if funcdef.pass_request:
|
|
||||||
kwargs[funcdef.pass_request] = flask.request
|
|
||||||
|
|
||||||
dataformat = get_dataformat()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if ismethod:
|
|
||||||
args = [self] + list(args)
|
|
||||||
result = f(*args, **kwargs)
|
|
||||||
|
|
||||||
# NOTE: Support setting of status_code with default 20
|
|
||||||
status_code = funcdef.status_code
|
|
||||||
if isinstance(result, wsme.api.Response):
|
|
||||||
status_code = result.status_code
|
|
||||||
result = result.obj
|
|
||||||
|
|
||||||
res = flask.make_response(
|
|
||||||
dataformat.encode_result(
|
|
||||||
result,
|
|
||||||
funcdef.return_type
|
|
||||||
)
|
|
||||||
)
|
|
||||||
res.mimetype = dataformat.content_type
|
|
||||||
res.status_code = status_code
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
exception_info = sys.exc_info()
|
|
||||||
orig_exception = exception_info[1]
|
|
||||||
orig_code = getattr(orig_exception, 'code', None)
|
|
||||||
data = wsme.api.format_exception(exception_info)
|
|
||||||
finally:
|
|
||||||
del exception_info
|
|
||||||
|
|
||||||
res = flask.make_response(dataformat.encode_error(None, data))
|
|
||||||
if data['faultcode'] == 'client':
|
|
||||||
res.status_code = 400
|
|
||||||
elif orig_code and is_valid_code(orig_code):
|
|
||||||
res.status_code = orig_code
|
|
||||||
else:
|
|
||||||
res.status_code = 500
|
|
||||||
return res
|
|
||||||
|
|
||||||
wrapper.wsme_func = f
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
142
wsmeext/pecan.py
142
wsmeext/pecan.py
@@ -1,142 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import inspect
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import wsme
|
|
||||||
import wsme.rest.args
|
|
||||||
import wsme.rest.json
|
|
||||||
import wsme.rest.xml
|
|
||||||
|
|
||||||
import pecan
|
|
||||||
|
|
||||||
from wsme.utils import is_valid_code
|
|
||||||
|
|
||||||
|
|
||||||
class JSonRenderer(object):
|
|
||||||
@staticmethod
|
|
||||||
def __init__(path, extra_vars):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def render(template_path, namespace):
|
|
||||||
if 'faultcode' in namespace:
|
|
||||||
return wsme.rest.json.encode_error(None, namespace)
|
|
||||||
return wsme.rest.json.encode_result(
|
|
||||||
namespace['result'],
|
|
||||||
namespace['datatype']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class XMLRenderer(object):
|
|
||||||
@staticmethod
|
|
||||||
def __init__(path, extra_vars):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def render(template_path, namespace):
|
|
||||||
if 'faultcode' in namespace:
|
|
||||||
return wsme.rest.xml.encode_error(None, namespace)
|
|
||||||
return wsme.rest.xml.encode_result(
|
|
||||||
namespace['result'],
|
|
||||||
namespace['datatype']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
pecan.templating._builtin_renderers['wsmejson'] = JSonRenderer
|
|
||||||
pecan.templating._builtin_renderers['wsmexml'] = XMLRenderer
|
|
||||||
|
|
||||||
pecan_json_decorate = pecan.expose(
|
|
||||||
template='wsmejson:',
|
|
||||||
content_type='application/json',
|
|
||||||
generic=False)
|
|
||||||
pecan_xml_decorate = pecan.expose(
|
|
||||||
template='wsmexml:',
|
|
||||||
content_type='application/xml',
|
|
||||||
generic=False
|
|
||||||
)
|
|
||||||
pecan_text_xml_decorate = pecan.expose(
|
|
||||||
template='wsmexml:',
|
|
||||||
content_type='text/xml',
|
|
||||||
generic=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def wsexpose(*args, **kwargs):
|
|
||||||
sig = wsme.signature(*args, **kwargs)
|
|
||||||
|
|
||||||
def decorate(f):
|
|
||||||
sig(f)
|
|
||||||
funcdef = wsme.api.FunctionDefinition.get(f)
|
|
||||||
funcdef.resolve_types(wsme.types.registry)
|
|
||||||
|
|
||||||
@functools.wraps(f)
|
|
||||||
def callfunction(self, *args, **kwargs):
|
|
||||||
return_type = funcdef.return_type
|
|
||||||
|
|
||||||
try:
|
|
||||||
args, kwargs = wsme.rest.args.get_args(
|
|
||||||
funcdef, args, kwargs, pecan.request.params, None,
|
|
||||||
pecan.request.body, pecan.request.content_type
|
|
||||||
)
|
|
||||||
if funcdef.pass_request:
|
|
||||||
kwargs[funcdef.pass_request] = pecan.request
|
|
||||||
result = f(self, *args, **kwargs)
|
|
||||||
|
|
||||||
# NOTE: Support setting of status_code with default 201
|
|
||||||
pecan.response.status = funcdef.status_code
|
|
||||||
if isinstance(result, wsme.api.Response):
|
|
||||||
pecan.response.status = result.status_code
|
|
||||||
|
|
||||||
# NOTE(lucasagomes): If the return code is 204
|
|
||||||
# (No Response) we have to make sure that we are not
|
|
||||||
# returning anything in the body response and the
|
|
||||||
# content-length is 0
|
|
||||||
if result.status_code == 204:
|
|
||||||
return_type = None
|
|
||||||
elif not isinstance(result.return_type,
|
|
||||||
wsme.types.UnsetType):
|
|
||||||
return_type = result.return_type
|
|
||||||
|
|
||||||
result = result.obj
|
|
||||||
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
exception_info = sys.exc_info()
|
|
||||||
orig_exception = exception_info[1]
|
|
||||||
orig_code = getattr(orig_exception, 'code', None)
|
|
||||||
data = wsme.api.format_exception(
|
|
||||||
exception_info,
|
|
||||||
pecan.conf.get('wsme', {}).get('debug', False)
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
del exception_info
|
|
||||||
|
|
||||||
if orig_code and is_valid_code(orig_code):
|
|
||||||
pecan.response.status = orig_code
|
|
||||||
else:
|
|
||||||
pecan.response.status = 500
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
if return_type is None:
|
|
||||||
pecan.request.pecan['content_type'] = None
|
|
||||||
pecan.response.content_type = None
|
|
||||||
return ''
|
|
||||||
|
|
||||||
return dict(
|
|
||||||
datatype=return_type,
|
|
||||||
result=result
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'xml' in funcdef.rest_content_types:
|
|
||||||
pecan_xml_decorate(callfunction)
|
|
||||||
pecan_text_xml_decorate(callfunction)
|
|
||||||
if 'json' in funcdef.rest_content_types:
|
|
||||||
pecan_json_decorate(callfunction)
|
|
||||||
pecan.util._cfg(callfunction)['argspec'] = inspect.getargspec(f)
|
|
||||||
callfunction._wsme_definition = funcdef
|
|
||||||
return callfunction
|
|
||||||
|
|
||||||
return decorate
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from wsmeext.soap.protocol import SoapProtocol
|
|
||||||
|
|
||||||
__all__ = ['SoapProtocol']
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
"""
|
|
||||||
A SOAP implementation for wsme.
|
|
||||||
Parts of the code were taken from the tgwebservices soap implmentation.
|
|
||||||
"""
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
import base64
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from wsmeext.soap.simplegeneric import generic
|
|
||||||
from wsmeext.soap.wsdl import WSDLGenerator
|
|
||||||
|
|
||||||
try:
|
|
||||||
from lxml import etree as ET
|
|
||||||
use_lxml = True
|
|
||||||
except ImportError:
|
|
||||||
from xml.etree import cElementTree as ET # noqa
|
|
||||||
use_lxml = False
|
|
||||||
|
|
||||||
from wsme.protocol import CallContext, Protocol, expose
|
|
||||||
|
|
||||||
import wsme.types
|
|
||||||
import wsme.runtime
|
|
||||||
|
|
||||||
from wsme import exc
|
|
||||||
from wsme.utils import parse_isodate, parse_isotime, parse_isodatetime
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
xsd_ns = 'http://www.w3.org/2001/XMLSchema'
|
|
||||||
xsi_ns = 'http://www.w3.org/2001/XMLSchema-instance'
|
|
||||||
soapenv_ns = 'http://schemas.xmlsoap.org/soap/envelope/'
|
|
||||||
|
|
||||||
if not use_lxml:
|
|
||||||
ET.register_namespace('soap', soapenv_ns)
|
|
||||||
|
|
||||||
type_qn = '{%s}type' % xsi_ns
|
|
||||||
nil_qn = '{%s}nil' % xsi_ns
|
|
||||||
|
|
||||||
Envelope_qn = '{%s}Envelope' % soapenv_ns
|
|
||||||
Body_qn = '{%s}Body' % soapenv_ns
|
|
||||||
Fault_qn = '{%s}Fault' % soapenv_ns
|
|
||||||
faultcode_qn = '{%s}faultcode' % soapenv_ns
|
|
||||||
faultstring_qn = '{%s}faultstring' % soapenv_ns
|
|
||||||
detail_qn = '{%s}detail' % soapenv_ns
|
|
||||||
|
|
||||||
|
|
||||||
type_registry = {
|
|
||||||
wsme.types.bytes: 'xs:string',
|
|
||||||
wsme.types.text: 'xs:string',
|
|
||||||
int: 'xs:int',
|
|
||||||
float: "xs:float",
|
|
||||||
bool: "xs:boolean",
|
|
||||||
datetime.datetime: "xs:dateTime",
|
|
||||||
datetime.date: "xs:date",
|
|
||||||
datetime.time: "xs:time",
|
|
||||||
decimal.Decimal: "xs:decimal",
|
|
||||||
wsme.types.binary: "xs:base64Binary",
|
|
||||||
}
|
|
||||||
|
|
||||||
if not six.PY3:
|
|
||||||
type_registry[long] = "xs:long"
|
|
||||||
|
|
||||||
array_registry = {
|
|
||||||
wsme.types.text: "String_Array",
|
|
||||||
wsme.types.bytes: "String_Array",
|
|
||||||
int: "Int_Array",
|
|
||||||
float: "Float_Array",
|
|
||||||
bool: "Boolean_Array",
|
|
||||||
}
|
|
||||||
|
|
||||||
if not six.PY3:
|
|
||||||
array_registry[long] = "Long_Array"
|
|
||||||
|
|
||||||
|
|
||||||
def soap_array(datatype, ns):
|
|
||||||
if datatype.item_type in array_registry:
|
|
||||||
name = array_registry[datatype.item_type]
|
|
||||||
else:
|
|
||||||
name = soap_type(datatype.item_type, False) + '_Array'
|
|
||||||
if ns:
|
|
||||||
name = 'types:' + name
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def soap_type(datatype, ns):
|
|
||||||
name = None
|
|
||||||
if wsme.types.isarray(datatype):
|
|
||||||
return soap_array(datatype, ns)
|
|
||||||
if wsme.types.isdict(datatype):
|
|
||||||
return None
|
|
||||||
if datatype in type_registry:
|
|
||||||
stype = type_registry[datatype]
|
|
||||||
if not ns:
|
|
||||||
stype = stype[3:]
|
|
||||||
return stype
|
|
||||||
if wsme.types.iscomplex(datatype):
|
|
||||||
name = datatype.__name__
|
|
||||||
if name and ns:
|
|
||||||
name = 'types:' + name
|
|
||||||
return name
|
|
||||||
if wsme.types.isusertype(datatype):
|
|
||||||
return soap_type(datatype.basetype, ns)
|
|
||||||
|
|
||||||
|
|
||||||
def soap_fname(path, funcdef):
|
|
||||||
return "".join([path[0]] + [i.capitalize() for i in path[1:]])
|
|
||||||
|
|
||||||
|
|
||||||
class SoapEncoder(object):
|
|
||||||
def __init__(self, types_ns):
|
|
||||||
self.types_ns = types_ns
|
|
||||||
|
|
||||||
def make_soap_element(self, datatype, tag, value, xsitype=None):
|
|
||||||
el = ET.Element(tag)
|
|
||||||
if value is None:
|
|
||||||
el.set(nil_qn, 'true')
|
|
||||||
elif xsitype is not None:
|
|
||||||
el.set(type_qn, xsitype)
|
|
||||||
el.text = value
|
|
||||||
elif wsme.types.isusertype(datatype):
|
|
||||||
return self.tosoap(datatype.basetype, tag,
|
|
||||||
datatype.tobasetype(value))
|
|
||||||
elif wsme.types.iscomplex(datatype):
|
|
||||||
el.set(type_qn, 'types:%s' % (datatype.__name__))
|
|
||||||
for attrdef in wsme.types.list_attributes(datatype):
|
|
||||||
attrvalue = getattr(value, attrdef.key)
|
|
||||||
if attrvalue is not wsme.types.Unset:
|
|
||||||
el.append(self.tosoap(
|
|
||||||
attrdef.datatype,
|
|
||||||
'{%s}%s' % (self.types_ns, attrdef.name),
|
|
||||||
attrvalue
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
el.set(type_qn, type_registry.get(datatype))
|
|
||||||
if not isinstance(value, wsme.types.text):
|
|
||||||
value = wsme.types.text(value)
|
|
||||||
el.text = value
|
|
||||||
return el
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def tosoap(self, datatype, tag, value):
|
|
||||||
"""Converts a value into xml Element objects for inclusion in the SOAP
|
|
||||||
response output (after adding the type to the type_registry).
|
|
||||||
|
|
||||||
If a non-complex user specific type is to be used in the api,
|
|
||||||
a specific toxml should be added::
|
|
||||||
|
|
||||||
from wsme.protocol.soap import tosoap, make_soap_element, \
|
|
||||||
type_registry
|
|
||||||
|
|
||||||
class MySpecialType(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
type_registry[MySpecialType] = 'xs:MySpecialType'
|
|
||||||
|
|
||||||
@tosoap.when_object(MySpecialType)
|
|
||||||
def myspecialtype_tosoap(datatype, tag, value):
|
|
||||||
return make_soap_element(datatype, tag, str(value))
|
|
||||||
"""
|
|
||||||
return self.make_soap_element(datatype, tag, value)
|
|
||||||
|
|
||||||
@tosoap.when_type(wsme.types.ArrayType)
|
|
||||||
def array_tosoap(self, datatype, tag, value):
|
|
||||||
el = ET.Element(tag)
|
|
||||||
el.set(type_qn, soap_array(datatype, self.types_ns))
|
|
||||||
if value is None:
|
|
||||||
el.set(nil_qn, 'true')
|
|
||||||
elif len(value) == 0:
|
|
||||||
el.append(ET.Element('item'))
|
|
||||||
else:
|
|
||||||
for item in value:
|
|
||||||
el.append(self.tosoap(datatype.item_type, 'item', item))
|
|
||||||
return el
|
|
||||||
|
|
||||||
@tosoap.when_object(bool)
|
|
||||||
def bool_tosoap(self, datatype, tag, value):
|
|
||||||
return self.make_soap_element(
|
|
||||||
datatype,
|
|
||||||
tag,
|
|
||||||
'true' if value is True else 'false' if value is False else None
|
|
||||||
)
|
|
||||||
|
|
||||||
@tosoap.when_object(wsme.types.bytes)
|
|
||||||
def bytes_tosoap(self, datatype, tag, value):
|
|
||||||
log.debug('(bytes_tosoap, %s, %s, %s, %s)', datatype,
|
|
||||||
tag, value, type(value))
|
|
||||||
if isinstance(value, wsme.types.bytes):
|
|
||||||
value = value.decode('ascii')
|
|
||||||
return self.make_soap_element(datatype, tag, value)
|
|
||||||
|
|
||||||
@tosoap.when_object(datetime.datetime)
|
|
||||||
def datetime_tosoap(self, datatype, tag, value):
|
|
||||||
return self.make_soap_element(
|
|
||||||
datatype,
|
|
||||||
tag,
|
|
||||||
value is not None and value.isoformat() or None
|
|
||||||
)
|
|
||||||
|
|
||||||
@tosoap.when_object(wsme.types.binary)
|
|
||||||
def binary_tosoap(self, datatype, tag, value):
|
|
||||||
log.debug("(%s, %s, %s)", datatype, tag, value)
|
|
||||||
value = base64.encodestring(value) if value is not None else None
|
|
||||||
if six.PY3:
|
|
||||||
value = value.decode('ascii')
|
|
||||||
return self.make_soap_element(
|
|
||||||
datatype.basetype, tag, value, 'xs:base64Binary'
|
|
||||||
)
|
|
||||||
|
|
||||||
@tosoap.when_object(None)
|
|
||||||
def None_tosoap(self, datatype, tag, value):
|
|
||||||
return self.make_soap_element(datatype, tag, None)
|
|
||||||
|
|
||||||
|
|
||||||
@generic
|
|
||||||
def fromsoap(datatype, el, ns):
|
|
||||||
"""
|
|
||||||
A generic converter from soap elements to python datatype.
|
|
||||||
|
|
||||||
If a non-complex user specific type is to be used in the api,
|
|
||||||
a specific fromsoap should be added.
|
|
||||||
"""
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if datatype in type_registry:
|
|
||||||
value = datatype(el.text)
|
|
||||||
elif wsme.types.isusertype(datatype):
|
|
||||||
value = datatype.frombasetype(
|
|
||||||
fromsoap(datatype.basetype, el, ns))
|
|
||||||
else:
|
|
||||||
value = datatype()
|
|
||||||
for attr in wsme.types.list_attributes(datatype):
|
|
||||||
child = el.find('{%s}%s' % (ns['type'], attr.name))
|
|
||||||
if child is not None:
|
|
||||||
setattr(value, attr.key, fromsoap(attr.datatype, child, ns))
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_type(wsme.types.ArrayType)
|
|
||||||
def array_fromsoap(datatype, el, ns):
|
|
||||||
if len(el) == 1:
|
|
||||||
if datatype.item_type \
|
|
||||||
not in wsme.types.pod_types + wsme.types.dt_types \
|
|
||||||
and len(el[0]) == 0:
|
|
||||||
return []
|
|
||||||
return [fromsoap(datatype.item_type, child, ns) for child in el]
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_object(wsme.types.bytes)
|
|
||||||
def bytes_fromsoap(datatype, el, ns):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if el.get(type_qn) not in (None, 'xs:string'):
|
|
||||||
raise exc.InvalidInput(el.tag, ET.tostring(el))
|
|
||||||
return el.text.encode('ascii') if el.text else six.b('')
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_object(wsme.types.text)
|
|
||||||
def text_fromsoap(datatype, el, ns):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if el.get(type_qn) not in (None, 'xs:string'):
|
|
||||||
raise exc.InvalidInput(el.tag, ET.tostring(el))
|
|
||||||
return datatype(el.text if el.text else '')
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_object(bool)
|
|
||||||
def bool_fromsoap(datatype, el, ns):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if el.get(type_qn) not in (None, 'xs:boolean'):
|
|
||||||
raise exc.InvalidInput(el.tag, ET.tostring(el))
|
|
||||||
return el.text.lower() != 'false'
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_object(datetime.date)
|
|
||||||
def date_fromsoap(datatype, el, ns):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if el.get(type_qn) not in (None, 'xs:date'):
|
|
||||||
raise exc.InvalidInput(el.tag, ET.tostring(el))
|
|
||||||
return parse_isodate(el.text)
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_object(datetime.time)
|
|
||||||
def time_fromsoap(datatype, el, ns):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if el.get(type_qn) not in (None, 'xs:time'):
|
|
||||||
raise exc.InvalidInput(el.tag, ET.tostring(el))
|
|
||||||
return parse_isotime(el.text)
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_object(datetime.datetime)
|
|
||||||
def datetime_fromsoap(datatype, el, ns):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if el.get(type_qn) not in (None, 'xs:dateTime'):
|
|
||||||
raise exc.InvalidInput(el.tag, ET.tostring(el))
|
|
||||||
return parse_isodatetime(el.text)
|
|
||||||
|
|
||||||
|
|
||||||
@fromsoap.when_object(wsme.types.binary)
|
|
||||||
def binary_fromsoap(datatype, el, ns):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
if el.get(type_qn) not in (None, 'xs:base64Binary'):
|
|
||||||
raise exc.InvalidInput(el.tag, ET.tostring(el))
|
|
||||||
return base64.decodestring(el.text.encode('ascii'))
|
|
||||||
|
|
||||||
|
|
||||||
class SoapProtocol(Protocol):
|
|
||||||
"""
|
|
||||||
SOAP protocol.
|
|
||||||
|
|
||||||
.. autoattribute:: name
|
|
||||||
.. autoattribute:: content_types
|
|
||||||
"""
|
|
||||||
name = 'soap'
|
|
||||||
displayname = 'SOAP'
|
|
||||||
content_types = ['application/soap+xml']
|
|
||||||
|
|
||||||
ns = {
|
|
||||||
"soap": "http://www.w3.org/2001/12/soap-envelope",
|
|
||||||
"soapenv": "http://schemas.xmlsoap.org/soap/envelope/",
|
|
||||||
"soapenc": "http://schemas.xmlsoap.org/soap/encoding/",
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, tns=None, typenamespace=None, baseURL=None,
|
|
||||||
servicename='MyApp'):
|
|
||||||
self.tns = tns
|
|
||||||
self.typenamespace = typenamespace
|
|
||||||
self.servicename = servicename
|
|
||||||
self.baseURL = baseURL
|
|
||||||
self._name_mapping = {}
|
|
||||||
|
|
||||||
self.encoder = SoapEncoder(typenamespace)
|
|
||||||
|
|
||||||
def get_name_mapping(self, service=None):
|
|
||||||
if service not in self._name_mapping:
|
|
||||||
self._name_mapping[service] = dict(
|
|
||||||
(soap_fname(path, f), path)
|
|
||||||
for path, f in self.root.getapi()
|
|
||||||
if service is None or (path and path[0] == service)
|
|
||||||
)
|
|
||||||
return self._name_mapping[service]
|
|
||||||
|
|
||||||
def accept(self, req):
|
|
||||||
for ct in self.content_types:
|
|
||||||
if req.headers['Content-Type'].startswith(ct):
|
|
||||||
return True
|
|
||||||
if req.headers.get("Soapaction"):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def iter_calls(self, request):
|
|
||||||
yield CallContext(request)
|
|
||||||
|
|
||||||
def extract_path(self, context):
|
|
||||||
request = context.request
|
|
||||||
el = ET.fromstring(request.body)
|
|
||||||
body = el.find('{%(soapenv)s}Body' % self.ns)
|
|
||||||
# Extract the service name from the tns
|
|
||||||
message = list(body)[0]
|
|
||||||
fname = message.tag
|
|
||||||
if fname.startswith('{%s}' % self.typenamespace):
|
|
||||||
fname = fname[len(self.typenamespace) + 2:]
|
|
||||||
mapping = self.get_name_mapping()
|
|
||||||
if fname not in mapping:
|
|
||||||
raise exc.UnknownFunction(fname)
|
|
||||||
path = mapping[fname]
|
|
||||||
context.soap_message = message
|
|
||||||
return path
|
|
||||||
return None
|
|
||||||
|
|
||||||
def read_arguments(self, context):
|
|
||||||
kw = {}
|
|
||||||
if not hasattr(context, 'soap_message'):
|
|
||||||
return kw
|
|
||||||
msg = context.soap_message
|
|
||||||
for param in msg:
|
|
||||||
# FIX for python2.6 (only for lxml)
|
|
||||||
if use_lxml and isinstance(param, ET._Comment):
|
|
||||||
continue
|
|
||||||
name = param.tag[len(self.typenamespace) + 2:]
|
|
||||||
arg = context.funcdef.get_arg(name)
|
|
||||||
value = fromsoap(arg.datatype, param, {
|
|
||||||
'type': self.typenamespace,
|
|
||||||
})
|
|
||||||
kw[name] = value
|
|
||||||
wsme.runtime.check_arguments(context.funcdef, (), kw)
|
|
||||||
return kw
|
|
||||||
|
|
||||||
def soap_response(self, path, funcdef, result):
|
|
||||||
r = ET.Element('{%s}%sResponse' % (
|
|
||||||
self.typenamespace, soap_fname(path, funcdef)
|
|
||||||
))
|
|
||||||
log.debug('(soap_response, %s, %s)', funcdef.return_type, result)
|
|
||||||
r.append(self.encoder.tosoap(
|
|
||||||
funcdef.return_type, '{%s}result' % self.typenamespace, result
|
|
||||||
))
|
|
||||||
return r
|
|
||||||
|
|
||||||
def encode_result(self, context, result):
|
|
||||||
log.debug('(encode_result, %s)', result)
|
|
||||||
if use_lxml:
|
|
||||||
envelope = ET.Element(
|
|
||||||
Envelope_qn,
|
|
||||||
nsmap={'xs': xsd_ns, 'types': self.typenamespace}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
envelope = ET.Element(Envelope_qn, {
|
|
||||||
'xmlns:xs': xsd_ns,
|
|
||||||
'xmlns:types': self.typenamespace
|
|
||||||
})
|
|
||||||
body = ET.SubElement(envelope, Body_qn)
|
|
||||||
body.append(self.soap_response(context.path, context.funcdef, result))
|
|
||||||
s = ET.tostring(envelope)
|
|
||||||
return s
|
|
||||||
|
|
||||||
def get_template(self, name):
|
|
||||||
return pkg_resources.resource_string(
|
|
||||||
__name__, '%s.html' % name)
|
|
||||||
|
|
||||||
def encode_error(self, context, infos):
|
|
||||||
envelope = ET.Element(Envelope_qn)
|
|
||||||
body = ET.SubElement(envelope, Body_qn)
|
|
||||||
fault = ET.SubElement(body, Fault_qn)
|
|
||||||
ET.SubElement(fault, faultcode_qn).text = infos['faultcode']
|
|
||||||
ET.SubElement(fault, faultstring_qn).text = infos['faultstring']
|
|
||||||
if 'debuginfo' in infos:
|
|
||||||
ET.SubElement(fault, detail_qn).text = infos['debuginfo']
|
|
||||||
s = ET.tostring(envelope)
|
|
||||||
return s
|
|
||||||
|
|
||||||
@expose('/api.wsdl', 'text/xml')
|
|
||||||
def api_wsdl(self, service=None):
|
|
||||||
if service is None:
|
|
||||||
servicename = self.servicename
|
|
||||||
else:
|
|
||||||
servicename = self.servicename + service.capitalize()
|
|
||||||
return WSDLGenerator(
|
|
||||||
tns=self.tns,
|
|
||||||
types_ns=self.typenamespace,
|
|
||||||
soapenc=self.ns['soapenc'],
|
|
||||||
service_name=servicename,
|
|
||||||
complex_types=self.root.__registry__.complex_types,
|
|
||||||
funclist=self.root.getapi(),
|
|
||||||
arrays=self.root.__registry__.array_types,
|
|
||||||
baseURL=self.baseURL,
|
|
||||||
soap_array=soap_array,
|
|
||||||
soap_type=soap_type,
|
|
||||||
soap_fname=soap_fname,
|
|
||||||
).generate(True)
|
|
||||||
|
|
||||||
def encode_sample_value(self, datatype, value, format=False):
|
|
||||||
r = self.encoder.make_soap_element(datatype, 'value', value)
|
|
||||||
if format:
|
|
||||||
xml_indent(r)
|
|
||||||
return ('xml', unicode(r))
|
|
||||||
|
|
||||||
|
|
||||||
def xml_indent(elem, level=0):
|
|
||||||
i = "\n" + level * " "
|
|
||||||
if len(elem):
|
|
||||||
if not elem.text or not elem.text.strip():
|
|
||||||
elem.text = i + " "
|
|
||||||
for e in elem:
|
|
||||||
xml_indent(e, level + 1)
|
|
||||||
if not e.tail or not e.tail.strip():
|
|
||||||
e.tail = i
|
|
||||||
if level and (not elem.tail or not elem.tail.strip()):
|
|
||||||
elem.tail = i
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import inspect
|
|
||||||
|
|
||||||
__all__ = ["generic"]
|
|
||||||
try:
|
|
||||||
from types import ClassType, InstanceType
|
|
||||||
classtypes = type, ClassType
|
|
||||||
except ImportError:
|
|
||||||
classtypes = type
|
|
||||||
InstanceType = None
|
|
||||||
|
|
||||||
|
|
||||||
def generic(func, argpos=None):
|
|
||||||
"""Create a simple generic function"""
|
|
||||||
|
|
||||||
if argpos is None:
|
|
||||||
if hasattr(func, 'argpos'):
|
|
||||||
argpos = func.argpos
|
|
||||||
else:
|
|
||||||
argnames = inspect.getargspec(func)[0]
|
|
||||||
if argnames and argnames[0] == 'self':
|
|
||||||
argpos = 1
|
|
||||||
else:
|
|
||||||
argpos = 0
|
|
||||||
|
|
||||||
_sentinel = object()
|
|
||||||
|
|
||||||
def _by_class(*args, **kw):
|
|
||||||
cls = args[argpos].__class__
|
|
||||||
for t in type(cls.__name__, (cls, object), {}).__mro__:
|
|
||||||
f = _gbt(t, _sentinel)
|
|
||||||
if f is not _sentinel:
|
|
||||||
return f(*args, **kw)
|
|
||||||
else:
|
|
||||||
return func(*args, **kw)
|
|
||||||
|
|
||||||
_by_type = {object: func, InstanceType: _by_class}
|
|
||||||
_gbt = _by_type.get
|
|
||||||
|
|
||||||
def when_type(*types):
|
|
||||||
"""Decorator to add a method that will be called for the given types"""
|
|
||||||
for t in types:
|
|
||||||
if not isinstance(t, classtypes):
|
|
||||||
raise TypeError(
|
|
||||||
"%r is not a type or class" % (t,)
|
|
||||||
)
|
|
||||||
|
|
||||||
def decorate(f):
|
|
||||||
for t in types:
|
|
||||||
if _by_type.setdefault(t, f) is not f:
|
|
||||||
raise TypeError(
|
|
||||||
"%r already has method for type %r" % (func, t)
|
|
||||||
)
|
|
||||||
return f
|
|
||||||
return decorate
|
|
||||||
|
|
||||||
_by_object = {}
|
|
||||||
_gbo = _by_object.get
|
|
||||||
|
|
||||||
def when_object(*obs):
|
|
||||||
"""Decorator to add a method to be called for the given object(s)"""
|
|
||||||
def decorate(f):
|
|
||||||
for o in obs:
|
|
||||||
if _by_object.setdefault(id(o), (o, f))[1] is not f:
|
|
||||||
raise TypeError(
|
|
||||||
"%r already has method for object %r" % (func, o)
|
|
||||||
)
|
|
||||||
return f
|
|
||||||
return decorate
|
|
||||||
|
|
||||||
def dispatch(*args, **kw):
|
|
||||||
f = _gbo(id(args[argpos]), _sentinel)
|
|
||||||
if f is _sentinel:
|
|
||||||
for t in type(args[argpos]).__mro__:
|
|
||||||
f = _gbt(t, _sentinel)
|
|
||||||
if f is not _sentinel:
|
|
||||||
return f(*args, **kw)
|
|
||||||
else:
|
|
||||||
return func(*args, **kw)
|
|
||||||
else:
|
|
||||||
return f[1](*args, **kw)
|
|
||||||
|
|
||||||
dispatch.__name__ = func.__name__
|
|
||||||
dispatch.__dict__ = func.__dict__.copy()
|
|
||||||
dispatch.__doc__ = func.__doc__
|
|
||||||
dispatch.__module__ = func.__module__
|
|
||||||
|
|
||||||
dispatch.when_type = when_type
|
|
||||||
dispatch.when_object = when_object
|
|
||||||
dispatch.default = func
|
|
||||||
dispatch.has_object = lambda o: id(o) in _by_object
|
|
||||||
dispatch.has_type = lambda t: t in _by_type
|
|
||||||
dispatch.argpos = argpos
|
|
||||||
return dispatch
|
|
||||||
|
|
||||||
|
|
||||||
def test_suite():
|
|
||||||
import doctest
|
|
||||||
return doctest.DocFileSuite(
|
|
||||||
'README.txt',
|
|
||||||
optionflags=doctest.ELLIPSIS | doctest.REPORT_ONLY_FIRST_FAILURE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import unittest
|
|
||||||
r = unittest.TextTestRunner()
|
|
||||||
r.run(test_suite())
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
import six
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
try:
|
|
||||||
from lxml import etree as ET
|
|
||||||
use_lxml = True
|
|
||||||
except ImportError:
|
|
||||||
from xml.etree import cElementTree as ET # noqa
|
|
||||||
use_lxml = False
|
|
||||||
|
|
||||||
|
|
||||||
def xml_tostring(el, pretty_print=False):
|
|
||||||
if use_lxml:
|
|
||||||
return ET.tostring(el, pretty_print=pretty_print)
|
|
||||||
return ET.tostring(el)
|
|
||||||
|
|
||||||
|
|
||||||
class NS(object):
|
|
||||||
def __init__(self, url):
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
def __call__(self, name):
|
|
||||||
return self.qn(name)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.url
|
|
||||||
|
|
||||||
def qn(self, name):
|
|
||||||
return '{%s}%s' % (self.url, name)
|
|
||||||
|
|
||||||
|
|
||||||
wsdl_ns = NS("http://schemas.xmlsoap.org/wsdl/")
|
|
||||||
soap_ns = NS("http://schemas.xmlsoap.org/wsdl/soap/")
|
|
||||||
xs_ns = NS("http://www.w3.org/2001/XMLSchema")
|
|
||||||
soapenc_ns = NS("http://schemas.xmlsoap.org/soap/encoding/")
|
|
||||||
|
|
||||||
|
|
||||||
class WSDLGenerator(object):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
tns,
|
|
||||||
types_ns,
|
|
||||||
soapenc,
|
|
||||||
service_name,
|
|
||||||
complex_types,
|
|
||||||
funclist,
|
|
||||||
arrays,
|
|
||||||
baseURL,
|
|
||||||
soap_array,
|
|
||||||
soap_type,
|
|
||||||
soap_fname):
|
|
||||||
|
|
||||||
self.tns = NS(tns)
|
|
||||||
self.types_ns = NS(types_ns)
|
|
||||||
self.soapenc = soapenc
|
|
||||||
self.service_name = service_name
|
|
||||||
self.complex_types = complex_types
|
|
||||||
self.funclist = funclist
|
|
||||||
self.arrays = arrays
|
|
||||||
self.baseURL = baseURL or ''
|
|
||||||
self.soap_array = soap_array
|
|
||||||
self.soap_fname = soap_fname
|
|
||||||
self.soap_type = soap_type
|
|
||||||
|
|
||||||
def gen_complex_type(self, cls):
|
|
||||||
complexType = ET.Element(xs_ns('complexType'))
|
|
||||||
complexType.set('name', cls.__name__)
|
|
||||||
sequence = ET.SubElement(complexType, xs_ns('sequence'))
|
|
||||||
for attrdef in wsme.types.list_attributes(cls):
|
|
||||||
soap_type = self.soap_type(attrdef.datatype, str(self.types_ns))
|
|
||||||
if soap_type is None:
|
|
||||||
continue
|
|
||||||
element = ET.SubElement(sequence, xs_ns('element'))
|
|
||||||
element.set('name', attrdef.name)
|
|
||||||
element.set('type', soap_type)
|
|
||||||
element.set('minOccurs', '1' if attrdef.mandatory else '0')
|
|
||||||
element.set('maxOccurs', '1')
|
|
||||||
return complexType
|
|
||||||
|
|
||||||
def gen_array(self, array):
|
|
||||||
complexType = ET.Element(xs_ns('complexType'))
|
|
||||||
complexType.set('name', self.soap_array(array, False))
|
|
||||||
ET.SubElement(
|
|
||||||
ET.SubElement(complexType, xs_ns('sequence')),
|
|
||||||
xs_ns('element'),
|
|
||||||
name='item',
|
|
||||||
maxOccurs='unbounded',
|
|
||||||
nillable='true',
|
|
||||||
type=self.soap_type(array.item_type, self.types_ns)
|
|
||||||
)
|
|
||||||
return complexType
|
|
||||||
|
|
||||||
def gen_function_types(self, path, funcdef):
|
|
||||||
args_el = ET.Element(
|
|
||||||
xs_ns('element'),
|
|
||||||
name=self.soap_fname(path, funcdef)
|
|
||||||
)
|
|
||||||
|
|
||||||
sequence = ET.SubElement(
|
|
||||||
ET.SubElement(args_el, xs_ns('complexType')),
|
|
||||||
xs_ns('sequence')
|
|
||||||
)
|
|
||||||
|
|
||||||
for farg in funcdef.arguments:
|
|
||||||
t = self.soap_type(farg.datatype, True)
|
|
||||||
if t is None:
|
|
||||||
continue
|
|
||||||
element = ET.SubElement(
|
|
||||||
sequence, xs_ns('element'),
|
|
||||||
name=farg.name,
|
|
||||||
type=self.soap_type(farg.datatype, True)
|
|
||||||
)
|
|
||||||
if not farg.mandatory:
|
|
||||||
element.set('minOccurs', '0')
|
|
||||||
|
|
||||||
response_el = ET.Element(
|
|
||||||
xs_ns('element'),
|
|
||||||
name=self.soap_fname(path, funcdef) + 'Response'
|
|
||||||
)
|
|
||||||
element = ET.SubElement(
|
|
||||||
ET.SubElement(
|
|
||||||
ET.SubElement(
|
|
||||||
response_el,
|
|
||||||
xs_ns('complexType')
|
|
||||||
),
|
|
||||||
xs_ns('sequence')
|
|
||||||
),
|
|
||||||
xs_ns('element'),
|
|
||||||
name='result'
|
|
||||||
)
|
|
||||||
return_soap_type = self.soap_type(funcdef.return_type, True)
|
|
||||||
if return_soap_type is not None:
|
|
||||||
element.set('type', return_soap_type)
|
|
||||||
|
|
||||||
return args_el, response_el
|
|
||||||
|
|
||||||
def gen_types(self):
|
|
||||||
types = ET.Element(wsdl_ns('types'))
|
|
||||||
schema = ET.SubElement(types, xs_ns('schema'))
|
|
||||||
schema.set('elementFormDefault', 'qualified')
|
|
||||||
schema.set('targetNamespace', str(self.types_ns))
|
|
||||||
for cls in self.complex_types:
|
|
||||||
schema.append(self.gen_complex_type(cls))
|
|
||||||
for array in self.arrays:
|
|
||||||
schema.append(self.gen_array(array))
|
|
||||||
for path, funcdef in self.funclist:
|
|
||||||
schema.extend(self.gen_function_types(path, funcdef))
|
|
||||||
return types
|
|
||||||
|
|
||||||
def gen_functions(self):
|
|
||||||
messages = []
|
|
||||||
|
|
||||||
binding = ET.Element(
|
|
||||||
wsdl_ns('binding'),
|
|
||||||
name='%s_Binding' % self.service_name,
|
|
||||||
type='tns:%s_PortType' % self.service_name
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
binding,
|
|
||||||
soap_ns('binding'),
|
|
||||||
style='document',
|
|
||||||
transport='http://schemas.xmlsoap.org/soap/http'
|
|
||||||
)
|
|
||||||
|
|
||||||
portType = ET.Element(
|
|
||||||
wsdl_ns('portType'),
|
|
||||||
name='%s_PortType' % self.service_name
|
|
||||||
)
|
|
||||||
|
|
||||||
for path, funcdef in self.funclist:
|
|
||||||
soap_fname = self.soap_fname(path, funcdef)
|
|
||||||
|
|
||||||
# message
|
|
||||||
req_message = ET.Element(
|
|
||||||
wsdl_ns('message'),
|
|
||||||
name=soap_fname + 'Request',
|
|
||||||
xmlns=str(self.types_ns)
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
req_message,
|
|
||||||
wsdl_ns('part'),
|
|
||||||
name='parameters',
|
|
||||||
element='types:%s' % soap_fname
|
|
||||||
)
|
|
||||||
messages.append(req_message)
|
|
||||||
|
|
||||||
res_message = ET.Element(
|
|
||||||
wsdl_ns('message'),
|
|
||||||
name=soap_fname + 'Response',
|
|
||||||
xmlns=str(self.types_ns)
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
res_message,
|
|
||||||
wsdl_ns('part'),
|
|
||||||
name='parameters',
|
|
||||||
element='types:%sResponse' % soap_fname
|
|
||||||
)
|
|
||||||
messages.append(res_message)
|
|
||||||
|
|
||||||
# portType/operation
|
|
||||||
operation = ET.SubElement(
|
|
||||||
portType,
|
|
||||||
wsdl_ns('operation'),
|
|
||||||
name=soap_fname
|
|
||||||
)
|
|
||||||
if funcdef.doc:
|
|
||||||
ET.SubElement(
|
|
||||||
operation,
|
|
||||||
wsdl_ns('documentation')
|
|
||||||
).text = funcdef.doc
|
|
||||||
ET.SubElement(
|
|
||||||
operation, wsdl_ns('input'),
|
|
||||||
message='tns:%sRequest' % soap_fname
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
operation, wsdl_ns('output'),
|
|
||||||
message='tns:%sResponse' % soap_fname
|
|
||||||
)
|
|
||||||
|
|
||||||
# binding/operation
|
|
||||||
operation = ET.SubElement(
|
|
||||||
binding,
|
|
||||||
wsdl_ns('operation'),
|
|
||||||
name=soap_fname
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
operation,
|
|
||||||
soap_ns('operation'),
|
|
||||||
soapAction=soap_fname
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
ET.SubElement(
|
|
||||||
operation,
|
|
||||||
wsdl_ns('input')
|
|
||||||
),
|
|
||||||
soap_ns('body'),
|
|
||||||
use='literal'
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
ET.SubElement(
|
|
||||||
operation,
|
|
||||||
wsdl_ns('output')
|
|
||||||
),
|
|
||||||
soap_ns('body'),
|
|
||||||
use='literal'
|
|
||||||
)
|
|
||||||
|
|
||||||
return messages + [portType, binding]
|
|
||||||
|
|
||||||
def gen_service(self):
|
|
||||||
service = ET.Element(wsdl_ns('service'), name=self.service_name)
|
|
||||||
ET.SubElement(
|
|
||||||
service,
|
|
||||||
wsdl_ns('documentation')
|
|
||||||
).text = six.u('WSDL File for %s') % self.service_name
|
|
||||||
ET.SubElement(
|
|
||||||
ET.SubElement(
|
|
||||||
service,
|
|
||||||
wsdl_ns('port'),
|
|
||||||
binding='tns:%s_Binding' % self.service_name,
|
|
||||||
name='%s_PortType' % self.service_name
|
|
||||||
),
|
|
||||||
soap_ns('address'),
|
|
||||||
location=self.baseURL
|
|
||||||
)
|
|
||||||
|
|
||||||
return service
|
|
||||||
|
|
||||||
def gen_definitions(self):
|
|
||||||
attrib = {
|
|
||||||
'name': self.service_name,
|
|
||||||
'targetNamespace': str(self.tns)
|
|
||||||
}
|
|
||||||
if use_lxml:
|
|
||||||
definitions = ET.Element(
|
|
||||||
wsdl_ns('definitions'),
|
|
||||||
attrib=attrib,
|
|
||||||
nsmap={
|
|
||||||
'xs': str(xs_ns),
|
|
||||||
'soap': str(soap_ns),
|
|
||||||
'types': str(self.types_ns),
|
|
||||||
'tns': str(self.tns)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
definitions = ET.Element(wsdl_ns('definitions'), **attrib)
|
|
||||||
definitions.set('xmlns:types', str(self.types_ns))
|
|
||||||
definitions.set('xmlns:tns', str(self.tns))
|
|
||||||
|
|
||||||
definitions.set('name', self.service_name)
|
|
||||||
definitions.append(self.gen_types())
|
|
||||||
definitions.extend(self.gen_functions())
|
|
||||||
definitions.append(self.gen_service())
|
|
||||||
return definitions
|
|
||||||
|
|
||||||
def generate(self, format=False):
|
|
||||||
return xml_tostring(self.gen_definitions(), pretty_print=format)
|
|
||||||
@@ -1,591 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from sphinx import addnodes
|
|
||||||
from sphinx.ext import autodoc
|
|
||||||
from sphinx.domains.python import PyClasslike, PyClassmember
|
|
||||||
from sphinx.domains import Domain, ObjType
|
|
||||||
from sphinx.directives import ObjectDescription
|
|
||||||
from sphinx.util.docfields import Field
|
|
||||||
from sphinx.util.nodes import make_refnode
|
|
||||||
|
|
||||||
from sphinx.roles import XRefRole
|
|
||||||
from sphinx.locale import l_, _
|
|
||||||
|
|
||||||
from docutils.parsers.rst import Directive
|
|
||||||
from docutils.parsers.rst import directives
|
|
||||||
|
|
||||||
import wsme
|
|
||||||
import wsme.types
|
|
||||||
import wsme.rest.json
|
|
||||||
import wsme.rest.xml
|
|
||||||
|
|
||||||
field_re = re.compile(r':(?P<field>\w+)(\s+(?P<name>\w+))?:')
|
|
||||||
|
|
||||||
|
|
||||||
def datatypename(datatype):
|
|
||||||
if isinstance(datatype, wsme.types.UserType):
|
|
||||||
return datatype.name
|
|
||||||
if isinstance(datatype, wsme.types.DictType):
|
|
||||||
return 'dict(%s: %s)' % (datatypename(datatype.key_type),
|
|
||||||
datatypename(datatype.value_type))
|
|
||||||
if isinstance(datatype, wsme.types.ArrayType):
|
|
||||||
return 'list(%s)' % datatypename(datatype.item_type)
|
|
||||||
return datatype.__name__
|
|
||||||
|
|
||||||
|
|
||||||
def make_sample_object(datatype):
|
|
||||||
if datatype is wsme.types.bytes:
|
|
||||||
return six.b('samplestring')
|
|
||||||
if datatype is wsme.types.text:
|
|
||||||
return u'sample unicode'
|
|
||||||
if datatype is int:
|
|
||||||
return 5
|
|
||||||
sample_obj = getattr(datatype, 'sample', datatype)()
|
|
||||||
return sample_obj
|
|
||||||
|
|
||||||
|
|
||||||
def get_protocols(names):
|
|
||||||
names = list(names)
|
|
||||||
protocols = []
|
|
||||||
if 'rest' in names:
|
|
||||||
names.remove('rest')
|
|
||||||
protocols.extend('restjson', 'restxml')
|
|
||||||
if 'restjson' in names:
|
|
||||||
names.remove('restjson')
|
|
||||||
protocols.append(('Json', wsme.rest.json))
|
|
||||||
if 'restxml' in names:
|
|
||||||
names.remove('restxml')
|
|
||||||
protocols.append(('XML', wsme.rest.xml))
|
|
||||||
for name in names:
|
|
||||||
p = wsme.protocol.getprotocol(name)
|
|
||||||
protocols.append((p.displayname or p.name, p))
|
|
||||||
return protocols
|
|
||||||
|
|
||||||
|
|
||||||
class SampleType(object):
|
|
||||||
"""A Sample Type"""
|
|
||||||
|
|
||||||
#: A Int
|
|
||||||
aint = int
|
|
||||||
|
|
||||||
def __init__(self, aint=None):
|
|
||||||
if aint:
|
|
||||||
self.aint = aint
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sample(cls):
|
|
||||||
return cls(10)
|
|
||||||
|
|
||||||
|
|
||||||
class SampleService(wsme.WSRoot):
|
|
||||||
@wsme.expose(SampleType)
|
|
||||||
@wsme.validate(SampleType, int, str)
|
|
||||||
def change_aint(data, aint, dummy='useless'):
|
|
||||||
"""
|
|
||||||
:param aint: The new value
|
|
||||||
|
|
||||||
:return: The data object with its aint field value changed.
|
|
||||||
"""
|
|
||||||
data.aint = aint
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def getroot(env, force=False):
|
|
||||||
root = env.temp_data.get('wsme:root')
|
|
||||||
if not force and root:
|
|
||||||
return root
|
|
||||||
rootpath = env.temp_data.get('wsme:rootpath', env.app.config.wsme_root)
|
|
||||||
|
|
||||||
if rootpath is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
modname, classname = rootpath.rsplit('.', 1)
|
|
||||||
__import__(modname)
|
|
||||||
module = sys.modules[modname]
|
|
||||||
root = getattr(module, classname)
|
|
||||||
env.temp_data['wsme:root'] = root
|
|
||||||
return root
|
|
||||||
|
|
||||||
|
|
||||||
def scan_services(service, path=[]):
|
|
||||||
has_functions = False
|
|
||||||
for name in dir(service):
|
|
||||||
if name.startswith('_'):
|
|
||||||
continue
|
|
||||||
a = getattr(service, name)
|
|
||||||
if inspect.ismethod(a):
|
|
||||||
if hasattr(a, '_wsme_definition'):
|
|
||||||
has_functions = True
|
|
||||||
if inspect.isclass(a):
|
|
||||||
continue
|
|
||||||
if len(path) > wsme.rest.APIPATH_MAXLEN:
|
|
||||||
raise ValueError("Path is too long: " + str(path))
|
|
||||||
for value in scan_services(a, path + [name]):
|
|
||||||
yield value
|
|
||||||
if has_functions:
|
|
||||||
yield service, path
|
|
||||||
|
|
||||||
|
|
||||||
def find_service_path(env, service):
|
|
||||||
root = getroot(env)
|
|
||||||
if service == root:
|
|
||||||
return []
|
|
||||||
for s, path in scan_services(root):
|
|
||||||
if s == service:
|
|
||||||
return path
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class TypeDirective(PyClasslike):
|
|
||||||
def get_index_text(self, modname, name_cls):
|
|
||||||
return _('%s (webservice type)') % name_cls[0]
|
|
||||||
|
|
||||||
def add_target_and_index(self, name_cls, sig, signode):
|
|
||||||
ret = super(TypeDirective, self).add_target_and_index(
|
|
||||||
name_cls, sig, signode
|
|
||||||
)
|
|
||||||
name = name_cls[0]
|
|
||||||
types = self.env.domaindata['wsme']['types']
|
|
||||||
if name in types:
|
|
||||||
self.state_machine.reporter.warning(
|
|
||||||
'duplicate type description of %s ' % name)
|
|
||||||
types[name] = self.env.docname
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class AttributeDirective(PyClassmember):
|
|
||||||
doc_field_types = [
|
|
||||||
Field('datatype', label=l_('Type'), has_arg=False,
|
|
||||||
names=('type', 'datatype'))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def check_samples_slot(value):
|
|
||||||
"""Validate the samples_slot option to the TypeDocumenter.
|
|
||||||
|
|
||||||
Valid positions are 'before-docstring' and
|
|
||||||
'after-docstring'. Using the explicit 'none' disables sample
|
|
||||||
output. The default is after-docstring.
|
|
||||||
"""
|
|
||||||
if not value:
|
|
||||||
return 'after-docstring'
|
|
||||||
val = directives.choice(
|
|
||||||
value,
|
|
||||||
('none', # do not include
|
|
||||||
'before-docstring', # show samples then docstring
|
|
||||||
'after-docstring', # show docstring then samples
|
|
||||||
))
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
class TypeDocumenter(autodoc.ClassDocumenter):
|
|
||||||
objtype = 'type'
|
|
||||||
directivetype = 'type'
|
|
||||||
domain = 'wsme'
|
|
||||||
|
|
||||||
required_arguments = 1
|
|
||||||
default_samples_slot = 'after-docstring'
|
|
||||||
|
|
||||||
option_spec = dict(
|
|
||||||
autodoc.ClassDocumenter.option_spec,
|
|
||||||
**{'protocols': lambda l: [v.strip() for v in l.split(',')],
|
|
||||||
'samples-slot': check_samples_slot,
|
|
||||||
})
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def can_document_member(member, membername, isattr, parent):
|
|
||||||
# we don't want to be automaticaly used
|
|
||||||
# TODO check if the member is registered an an exposed type
|
|
||||||
return False
|
|
||||||
|
|
||||||
def format_name(self):
|
|
||||||
return self.object.__name__
|
|
||||||
|
|
||||||
def format_signature(self):
|
|
||||||
return u''
|
|
||||||
|
|
||||||
def add_directive_header(self, sig):
|
|
||||||
super(TypeDocumenter, self).add_directive_header(sig)
|
|
||||||
# remove the :module: option that was added by ClassDocumenter
|
|
||||||
if ':module:' in self.directive.result[-1]:
|
|
||||||
self.directive.result.pop()
|
|
||||||
|
|
||||||
def import_object(self):
|
|
||||||
if super(TypeDocumenter, self).import_object():
|
|
||||||
wsme.types.register_type(self.object)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_content(self, more_content, no_docstring=False):
|
|
||||||
# Check where to include the samples
|
|
||||||
samples_slot = self.options.samples_slot or self.default_samples_slot
|
|
||||||
|
|
||||||
def add_docstring():
|
|
||||||
super(TypeDocumenter, self).add_content(
|
|
||||||
more_content, no_docstring)
|
|
||||||
|
|
||||||
def add_samples():
|
|
||||||
protocols = get_protocols(
|
|
||||||
self.options.protocols or self.env.app.config.wsme_protocols
|
|
||||||
)
|
|
||||||
content = []
|
|
||||||
if protocols:
|
|
||||||
sample_obj = make_sample_object(self.object)
|
|
||||||
content.extend([
|
|
||||||
l_(u'Data samples:'),
|
|
||||||
u'',
|
|
||||||
u'.. cssclass:: toggle',
|
|
||||||
u''
|
|
||||||
])
|
|
||||||
for name, protocol in protocols:
|
|
||||||
language, sample = protocol.encode_sample_value(
|
|
||||||
self.object, sample_obj, format=True)
|
|
||||||
content.extend([
|
|
||||||
name,
|
|
||||||
u' .. code-block:: ' + language,
|
|
||||||
u'',
|
|
||||||
])
|
|
||||||
content.extend(
|
|
||||||
u' ' * 8 + line
|
|
||||||
for line in six.text_type(sample).split('\n'))
|
|
||||||
for line in content:
|
|
||||||
self.add_line(line, u'<wsmeext.sphinxext')
|
|
||||||
|
|
||||||
self.add_line(u'', '<wsmeext.sphinxext>')
|
|
||||||
|
|
||||||
if samples_slot == 'after-docstring':
|
|
||||||
add_docstring()
|
|
||||||
add_samples()
|
|
||||||
elif samples_slot == 'before-docstring':
|
|
||||||
add_samples()
|
|
||||||
add_docstring()
|
|
||||||
else:
|
|
||||||
add_docstring()
|
|
||||||
|
|
||||||
|
|
||||||
class AttributeDocumenter(autodoc.AttributeDocumenter):
|
|
||||||
datatype = None
|
|
||||||
domain = 'wsme'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def can_document_member(member, membername, isattr, parent):
|
|
||||||
return isinstance(parent, TypeDocumenter)
|
|
||||||
|
|
||||||
def import_object(self):
|
|
||||||
success = super(AttributeDocumenter, self).import_object()
|
|
||||||
if success:
|
|
||||||
self.datatype = self.object.datatype
|
|
||||||
return success
|
|
||||||
|
|
||||||
def add_content(self, more_content, no_docstring=False):
|
|
||||||
self.add_line(
|
|
||||||
u':type: %s' % datatypename(self.datatype),
|
|
||||||
'<wsmeext.sphinxext>'
|
|
||||||
)
|
|
||||||
self.add_line(u'', '<wsmeext.sphinxext>')
|
|
||||||
super(AttributeDocumenter, self).add_content(
|
|
||||||
more_content, no_docstring)
|
|
||||||
|
|
||||||
def add_directive_header(self, sig):
|
|
||||||
super(AttributeDocumenter, self).add_directive_header(sig)
|
|
||||||
|
|
||||||
|
|
||||||
class RootDirective(Directive):
|
|
||||||
"""
|
|
||||||
This directive is to tell what class is the Webservice root
|
|
||||||
"""
|
|
||||||
has_content = False
|
|
||||||
required_arguments = 1
|
|
||||||
optional_arguments = 0
|
|
||||||
final_argument_whitespace = False
|
|
||||||
option_spec = {
|
|
||||||
'webpath': directives.unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
env = self.state.document.settings.env
|
|
||||||
rootpath = self.arguments[0].strip()
|
|
||||||
env.temp_data['wsme:rootpath'] = rootpath
|
|
||||||
if 'wsme:root' in env.temp_data:
|
|
||||||
del env.temp_data['wsme:root']
|
|
||||||
if 'webpath' in self.options:
|
|
||||||
env.temp_data['wsme:webpath'] = self.options['webpath']
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceDirective(ObjectDescription):
|
|
||||||
name = 'service'
|
|
||||||
|
|
||||||
optional_arguments = 1
|
|
||||||
|
|
||||||
def handle_signature(self, sig, signode):
|
|
||||||
path = sig.split('/')
|
|
||||||
|
|
||||||
namespace = '/'.join(path[:-1])
|
|
||||||
if namespace and not namespace.endswith('/'):
|
|
||||||
namespace += '/'
|
|
||||||
|
|
||||||
servicename = path[-1]
|
|
||||||
|
|
||||||
if not namespace and not servicename:
|
|
||||||
servicename = '/'
|
|
||||||
|
|
||||||
signode += addnodes.desc_annotation('service ', 'service ')
|
|
||||||
|
|
||||||
if namespace:
|
|
||||||
signode += addnodes.desc_addname(namespace, namespace)
|
|
||||||
|
|
||||||
signode += addnodes.desc_name(servicename, servicename)
|
|
||||||
|
|
||||||
return sig
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceDocumenter(autodoc.ClassDocumenter):
|
|
||||||
domain = 'wsme'
|
|
||||||
objtype = 'service'
|
|
||||||
directivetype = 'service'
|
|
||||||
|
|
||||||
def add_directive_header(self, sig):
|
|
||||||
super(ServiceDocumenter, self).add_directive_header(sig)
|
|
||||||
# remove the :module: option that was added by ClassDocumenter
|
|
||||||
if ':module:' in self.directive.result[-1]:
|
|
||||||
self.directive.result.pop()
|
|
||||||
|
|
||||||
def format_signature(self):
|
|
||||||
return u''
|
|
||||||
|
|
||||||
def format_name(self):
|
|
||||||
path = find_service_path(self.env, self.object)
|
|
||||||
if path is None:
|
|
||||||
return
|
|
||||||
return '/' + '/'.join(path)
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionDirective(PyClassmember):
|
|
||||||
name = 'function'
|
|
||||||
objtype = 'function'
|
|
||||||
|
|
||||||
def get_signature_prefix(self, sig):
|
|
||||||
return 'function '
|
|
||||||
|
|
||||||
|
|
||||||
def document_function(funcdef, docstrings=None, protocols=['restjson']):
|
|
||||||
"""A helper function to complete a function documentation with return and
|
|
||||||
parameter types"""
|
|
||||||
# If the function doesn't have a docstring, add an empty list
|
|
||||||
# so the default behaviors below work correctly.
|
|
||||||
if not docstrings:
|
|
||||||
docstrings = [[]]
|
|
||||||
found_params = set()
|
|
||||||
|
|
||||||
for si, docstring in enumerate(docstrings):
|
|
||||||
for i, line in enumerate(docstring):
|
|
||||||
m = field_re.match(line)
|
|
||||||
if m and m.group('field') == 'param':
|
|
||||||
found_params.add(m.group('name'))
|
|
||||||
|
|
||||||
next_param_pos = (0, 0)
|
|
||||||
|
|
||||||
for arg in funcdef.arguments:
|
|
||||||
content = [
|
|
||||||
u':type %s: :wsme:type:`%s`' % (
|
|
||||||
arg.name, datatypename(arg.datatype))
|
|
||||||
]
|
|
||||||
if arg.name not in found_params:
|
|
||||||
content.insert(0, u':param %s: ' % (arg.name))
|
|
||||||
pos = next_param_pos
|
|
||||||
else:
|
|
||||||
for si, docstring in enumerate(docstrings):
|
|
||||||
for i, line in enumerate(docstring):
|
|
||||||
m = field_re.match(line)
|
|
||||||
if m and m.group('field') == 'param' \
|
|
||||||
and m.group('name') == arg.name:
|
|
||||||
pos = (si, i + 1)
|
|
||||||
break
|
|
||||||
docstring = docstrings[pos[0]]
|
|
||||||
docstring[pos[1]:pos[1]] = content
|
|
||||||
next_param_pos = (pos[0], pos[1] + len(content))
|
|
||||||
|
|
||||||
if funcdef.return_type:
|
|
||||||
content = [
|
|
||||||
u':rtype: %s' % datatypename(funcdef.return_type)
|
|
||||||
]
|
|
||||||
pos = None
|
|
||||||
for si, docstring in enumerate(docstrings):
|
|
||||||
for i, line in enumerate(docstring):
|
|
||||||
m = field_re.match(line)
|
|
||||||
if m and m.group('field') == 'return':
|
|
||||||
pos = (si, i + 1)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
pos = next_param_pos
|
|
||||||
docstring = docstrings[pos[0]]
|
|
||||||
docstring[pos[1]:pos[1]] = content
|
|
||||||
|
|
||||||
codesamples = []
|
|
||||||
|
|
||||||
if protocols:
|
|
||||||
params = []
|
|
||||||
for arg in funcdef.arguments:
|
|
||||||
params.append((
|
|
||||||
arg.name,
|
|
||||||
arg.datatype,
|
|
||||||
make_sample_object(arg.datatype)
|
|
||||||
))
|
|
||||||
codesamples.extend([
|
|
||||||
u':%s:' % l_(u'Parameters samples'),
|
|
||||||
u' .. cssclass:: toggle',
|
|
||||||
u''
|
|
||||||
])
|
|
||||||
for name, protocol in protocols:
|
|
||||||
language, sample = protocol.encode_sample_params(
|
|
||||||
params, format=True)
|
|
||||||
codesamples.extend([
|
|
||||||
u' ' * 4 + name,
|
|
||||||
u' .. code-block:: ' + language,
|
|
||||||
u'',
|
|
||||||
])
|
|
||||||
codesamples.extend((
|
|
||||||
u' ' * 12 + line
|
|
||||||
for line in six.text_type(sample).split('\n')
|
|
||||||
))
|
|
||||||
|
|
||||||
if funcdef.return_type:
|
|
||||||
codesamples.extend([
|
|
||||||
u':%s:' % l_(u'Return samples'),
|
|
||||||
u' .. cssclass:: toggle',
|
|
||||||
u''
|
|
||||||
])
|
|
||||||
sample_obj = make_sample_object(funcdef.return_type)
|
|
||||||
for name, protocol in protocols:
|
|
||||||
language, sample = protocol.encode_sample_result(
|
|
||||||
funcdef.return_type, sample_obj, format=True)
|
|
||||||
codesamples.extend([
|
|
||||||
u' ' * 4 + name,
|
|
||||||
u' .. code-block:: ' + language,
|
|
||||||
u'',
|
|
||||||
])
|
|
||||||
codesamples.extend((
|
|
||||||
u' ' * 12 + line
|
|
||||||
for line in six.text_type(sample).split('\n')
|
|
||||||
))
|
|
||||||
|
|
||||||
docstrings[0:0] = [codesamples]
|
|
||||||
return docstrings
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionDocumenter(autodoc.MethodDocumenter):
|
|
||||||
domain = 'wsme'
|
|
||||||
directivetype = 'function'
|
|
||||||
objtype = 'function'
|
|
||||||
priority = 1
|
|
||||||
|
|
||||||
option_spec = {
|
|
||||||
'path': directives.unchanged,
|
|
||||||
'method': directives.unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def can_document_member(member, membername, isattr, parent):
|
|
||||||
return (isinstance(parent, ServiceDocumenter) and
|
|
||||||
wsme.api.iswsmefunction(member))
|
|
||||||
|
|
||||||
def import_object(self):
|
|
||||||
ret = super(FunctionDocumenter, self).import_object()
|
|
||||||
self.directivetype = 'function'
|
|
||||||
self.wsme_fd = wsme.api.FunctionDefinition.get(self.object)
|
|
||||||
self.retann = datatypename(self.wsme_fd.return_type)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def format_args(self):
|
|
||||||
args = [arg.name for arg in self.wsme_fd.arguments]
|
|
||||||
defaults = [
|
|
||||||
arg.default
|
|
||||||
for arg in self.wsme_fd.arguments if not arg.mandatory
|
|
||||||
]
|
|
||||||
return inspect.formatargspec(args, defaults=defaults)
|
|
||||||
|
|
||||||
def get_doc(self, encoding=None):
|
|
||||||
"""Inject the type and param fields into the docstrings so that the
|
|
||||||
user can add its own param fields to document the parameters"""
|
|
||||||
docstrings = super(FunctionDocumenter, self).get_doc(encoding)
|
|
||||||
|
|
||||||
protocols = get_protocols(
|
|
||||||
self.options.protocols or self.env.app.config.wsme_protocols
|
|
||||||
)
|
|
||||||
|
|
||||||
return document_function(
|
|
||||||
self.wsme_fd, docstrings, protocols
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_content(self, more_content, no_docstring=False):
|
|
||||||
super(FunctionDocumenter, self).add_content(more_content, no_docstring)
|
|
||||||
|
|
||||||
def format_name(self):
|
|
||||||
return self.wsme_fd.name
|
|
||||||
|
|
||||||
def add_directive_header(self, sig):
|
|
||||||
super(FunctionDocumenter, self).add_directive_header(sig)
|
|
||||||
# remove the :module: option that was added by ClassDocumenter
|
|
||||||
if ':module:' in self.directive.result[-1]:
|
|
||||||
self.directive.result.pop()
|
|
||||||
|
|
||||||
|
|
||||||
class WSMEDomain(Domain):
|
|
||||||
name = 'wsme'
|
|
||||||
label = 'WSME'
|
|
||||||
|
|
||||||
object_types = {
|
|
||||||
'type': ObjType(l_('type'), 'type', 'obj'),
|
|
||||||
'service': ObjType(l_('service'), 'service', 'obj')
|
|
||||||
}
|
|
||||||
|
|
||||||
directives = {
|
|
||||||
'type': TypeDirective,
|
|
||||||
'attribute': AttributeDirective,
|
|
||||||
'service': ServiceDirective,
|
|
||||||
'root': RootDirective,
|
|
||||||
'function': FunctionDirective,
|
|
||||||
}
|
|
||||||
|
|
||||||
roles = {
|
|
||||||
'type': XRefRole()
|
|
||||||
}
|
|
||||||
|
|
||||||
initial_data = {
|
|
||||||
'types': {}, # fullname -> docname
|
|
||||||
}
|
|
||||||
|
|
||||||
def clear_doc(self, docname):
|
|
||||||
keys = list(self.data['types'].keys())
|
|
||||||
for key in keys:
|
|
||||||
value = self.data['types'][key]
|
|
||||||
if value == docname:
|
|
||||||
del self.data['types'][key]
|
|
||||||
|
|
||||||
def resolve_xref(self, env, fromdocname, builder,
|
|
||||||
type, target, node, contnode):
|
|
||||||
if target not in self.data['types']:
|
|
||||||
return None
|
|
||||||
todocname = self.data['types'][target]
|
|
||||||
return make_refnode(
|
|
||||||
builder, fromdocname, todocname, target, contnode, target)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
|
||||||
app.add_domain(WSMEDomain)
|
|
||||||
app.add_autodocumenter(TypeDocumenter)
|
|
||||||
app.add_autodocumenter(AttributeDocumenter)
|
|
||||||
app.add_autodocumenter(ServiceDocumenter)
|
|
||||||
app.add_autodocumenter(FunctionDocumenter)
|
|
||||||
|
|
||||||
app.add_config_value('wsme_root', None, 'env')
|
|
||||||
app.add_config_value('wsme_webpath', '/', 'env')
|
|
||||||
app.add_config_value('wsme_protocols', ['restjson', 'restxml'], 'env')
|
|
||||||
app.add_javascript('toggle.js')
|
|
||||||
app.add_stylesheet('toggle.css')
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
from wsme.rest import expose, validate
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
from wsmeext.sqlalchemy.types import SQLAlchemyRegistry
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDControllerMeta(type):
|
|
||||||
def __init__(cls, name, bases, dct):
|
|
||||||
if cls.__saclass__ is not None:
|
|
||||||
if cls.__registry__ is None:
|
|
||||||
cls.__registry__ = wsme.types.registry
|
|
||||||
if cls.__wstype__ is None:
|
|
||||||
cls.__wstype__ = cls.__registry__.resolve_type(
|
|
||||||
SQLAlchemyRegistry.get(
|
|
||||||
cls.__registry__).getdatatype(cls.__saclass__))
|
|
||||||
|
|
||||||
cls.create = expose(
|
|
||||||
cls.__wstype__,
|
|
||||||
method='PUT',
|
|
||||||
wrap=True
|
|
||||||
)(cls.create)
|
|
||||||
cls.create = validate(cls.__wstype__)(cls.create)
|
|
||||||
|
|
||||||
cls.read = expose(
|
|
||||||
cls.__wstype__,
|
|
||||||
method='GET',
|
|
||||||
wrap=True
|
|
||||||
)(cls.read)
|
|
||||||
cls.read = validate(cls.__wstype__)(cls.read)
|
|
||||||
|
|
||||||
cls.update = expose(
|
|
||||||
cls.__wstype__,
|
|
||||||
method='POST',
|
|
||||||
wrap=True
|
|
||||||
)(cls.update)
|
|
||||||
cls.update = validate(cls.__wstype__)(cls.update)
|
|
||||||
|
|
||||||
cls.delete = expose(
|
|
||||||
method='DELETE',
|
|
||||||
wrap=True
|
|
||||||
)(cls.delete)
|
|
||||||
cls.delete = validate(cls.__wstype__)(cls.delete)
|
|
||||||
|
|
||||||
super(CRUDControllerMeta, cls).__init__(name, bases, dct)
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDControllerBase(object):
|
|
||||||
__registry__ = None
|
|
||||||
__saclass__ = None
|
|
||||||
__wstype__ = None
|
|
||||||
__dbsession__ = None
|
|
||||||
|
|
||||||
def _create_one(self, data):
|
|
||||||
obj = self.__saclass__()
|
|
||||||
data.to_instance(obj)
|
|
||||||
self.__dbsession__.add(obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def _get_one(self, ref):
|
|
||||||
q = self.__dbsession__.query(self.__saclass__)
|
|
||||||
q = q.filter(ref.get_ref_criterion())
|
|
||||||
return q.one()
|
|
||||||
|
|
||||||
def _update_one(self, data):
|
|
||||||
obj = self._get_one(data)
|
|
||||||
if obj is None:
|
|
||||||
raise ValueError("No match for data=%s" % data)
|
|
||||||
data.to_instance(obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def _delete(self, ref):
|
|
||||||
obj = self._get_one(ref)
|
|
||||||
self.__dbsession__.delete(obj)
|
|
||||||
|
|
||||||
def create(self, data):
|
|
||||||
obj = self._create_one(data)
|
|
||||||
self.__dbsession__.flush()
|
|
||||||
return self.__wstype__(obj)
|
|
||||||
|
|
||||||
def read(self, ref):
|
|
||||||
obj = self._get_one(ref)
|
|
||||||
return self.__wstype__(obj)
|
|
||||||
|
|
||||||
def update(self, data):
|
|
||||||
obj = self._update_one(data)
|
|
||||||
self.__dbsession__.flush()
|
|
||||||
return self.__wstype__(obj)
|
|
||||||
|
|
||||||
def delete(self, ref):
|
|
||||||
self._delete(ref)
|
|
||||||
self.__dbsession__.flush()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
CRUDController = CRUDControllerMeta(
|
|
||||||
'CRUDController', (CRUDControllerBase,), {}
|
|
||||||
)
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from sqlalchemy.orm import class_mapper
|
|
||||||
from sqlalchemy.orm.properties import ColumnProperty, RelationProperty
|
|
||||||
|
|
||||||
import sqlalchemy.types
|
|
||||||
|
|
||||||
import wsme.types
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyRegistry(object):
|
|
||||||
@classmethod
|
|
||||||
def get(cls, registry):
|
|
||||||
if not hasattr(registry, 'sqlalchemy'):
|
|
||||||
registry.sqlalchemy = cls()
|
|
||||||
return registry.sqlalchemy
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.types = {}
|
|
||||||
self.satypeclasses = {
|
|
||||||
sqlalchemy.types.Integer: int,
|
|
||||||
sqlalchemy.types.Boolean: bool,
|
|
||||||
sqlalchemy.types.Float: float,
|
|
||||||
sqlalchemy.types.Numeric: decimal.Decimal,
|
|
||||||
sqlalchemy.types.Date: datetime.date,
|
|
||||||
sqlalchemy.types.Time: datetime.time,
|
|
||||||
sqlalchemy.types.DateTime: datetime.datetime,
|
|
||||||
sqlalchemy.types.String: wsme.types.text,
|
|
||||||
sqlalchemy.types.Unicode: wsme.types.text,
|
|
||||||
}
|
|
||||||
|
|
||||||
def getdatatype(self, sadatatype):
|
|
||||||
if sadatatype.__class__ in self.satypeclasses:
|
|
||||||
return self.satypeclasses[sadatatype.__class__]
|
|
||||||
elif sadatatype in self.types:
|
|
||||||
return self.types[sadatatype]
|
|
||||||
else:
|
|
||||||
return sadatatype.__name__
|
|
||||||
|
|
||||||
|
|
||||||
def register_saclass(registry, saclass, typename=None):
|
|
||||||
"""Associate a webservice type name to a SQLAlchemy mapped class.
|
|
||||||
The default typename if the saclass name itself.
|
|
||||||
"""
|
|
||||||
if typename is None:
|
|
||||||
typename = saclass.__name__
|
|
||||||
|
|
||||||
SQLAlchemyRegistry.get(registry).types[saclass] = typename
|
|
||||||
|
|
||||||
|
|
||||||
class wsattr(wsme.types.wsattr):
|
|
||||||
def __init__(self, datatype, saproperty=None, **kw):
|
|
||||||
super(wsattr, self).__init__(datatype, **kw)
|
|
||||||
self.saname = saproperty.key
|
|
||||||
self.saproperty = saproperty
|
|
||||||
self.isrelation = isinstance(saproperty, RelationProperty)
|
|
||||||
|
|
||||||
|
|
||||||
def make_wsattr(registry, saproperty):
|
|
||||||
datatype = None
|
|
||||||
if isinstance(saproperty, ColumnProperty):
|
|
||||||
if len(saproperty.columns) > 1:
|
|
||||||
log.warning("Cannot handle multi-column ColumnProperty")
|
|
||||||
return None
|
|
||||||
datatype = SQLAlchemyRegistry.get(registry).getdatatype(
|
|
||||||
saproperty.columns[0].type)
|
|
||||||
elif isinstance(saproperty, RelationProperty):
|
|
||||||
other_saclass = saproperty.mapper.class_
|
|
||||||
datatype = SQLAlchemyRegistry.get(registry).getdatatype(other_saclass)
|
|
||||||
if saproperty.uselist:
|
|
||||||
datatype = [datatype]
|
|
||||||
else:
|
|
||||||
log.warning("Don't know how to handle %s attributes" %
|
|
||||||
saproperty.__class__)
|
|
||||||
|
|
||||||
if datatype:
|
|
||||||
return wsattr(datatype, saproperty)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMeta(wsme.types.BaseMeta):
|
|
||||||
def __new__(cls, name, bases, dct):
|
|
||||||
if '__registry__' not in dct:
|
|
||||||
dct['__registry__'] = wsme.types.registry
|
|
||||||
return type.__new__(cls, name, bases, dct)
|
|
||||||
|
|
||||||
def __init__(cls, name, bases, dct):
|
|
||||||
saclass = getattr(cls, '__saclass__', None)
|
|
||||||
if saclass:
|
|
||||||
mapper = class_mapper(saclass)
|
|
||||||
cls._pkey_attrs = []
|
|
||||||
cls._ref_attrs = []
|
|
||||||
for prop in mapper.iterate_properties:
|
|
||||||
key = prop.key
|
|
||||||
if hasattr(cls, key):
|
|
||||||
continue
|
|
||||||
if key.startswith('_'):
|
|
||||||
continue
|
|
||||||
attr = make_wsattr(cls.__registry__, prop)
|
|
||||||
if attr is not None:
|
|
||||||
setattr(cls, key, attr)
|
|
||||||
|
|
||||||
if attr and isinstance(prop, ColumnProperty) and \
|
|
||||||
prop.columns[0] in mapper.primary_key:
|
|
||||||
cls._pkey_attrs.append(attr)
|
|
||||||
cls._ref_attrs.append(attr)
|
|
||||||
|
|
||||||
register_saclass(cls.__registry__, cls.__saclass__, cls.__name__)
|
|
||||||
super(BaseMeta, cls).__init__(name, bases, dct)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(six.with_metaclass(BaseMeta, wsme.types.Base)):
|
|
||||||
def __init__(self, instance=None, keyonly=False, attrs=None, eagerload=[]):
|
|
||||||
if instance:
|
|
||||||
self.from_instance(instance, keyonly, attrs, eagerload)
|
|
||||||
|
|
||||||
def from_instance(self, instance, keyonly=False, attrs=None, eagerload=[]):
|
|
||||||
if keyonly:
|
|
||||||
attrs = self._pkey_attrs + self._ref_attrs
|
|
||||||
for attr in self._wsme_attributes:
|
|
||||||
if not isinstance(attr, wsattr):
|
|
||||||
continue
|
|
||||||
if attrs and not attr.isrelation and attr.name not in attrs:
|
|
||||||
continue
|
|
||||||
if attr.isrelation and attr.name not in eagerload:
|
|
||||||
continue
|
|
||||||
value = getattr(instance, attr.saname)
|
|
||||||
if attr.isrelation:
|
|
||||||
attr_keyonly = attr.name not in eagerload
|
|
||||||
attr_attrs = None
|
|
||||||
attr_eagerload = []
|
|
||||||
if not attr_keyonly:
|
|
||||||
attr_attrs = [
|
|
||||||
aname[len(attr.name) + 1:]
|
|
||||||
for aname in attrs
|
|
||||||
if aname.startswith(attr.name + '.')
|
|
||||||
]
|
|
||||||
attr_eagerload = [
|
|
||||||
aname[len(attr.name) + 1:]
|
|
||||||
for aname in eagerload
|
|
||||||
if aname.startswith(attr.name + '.')
|
|
||||||
]
|
|
||||||
if attr.saproperty.uselist:
|
|
||||||
value = [
|
|
||||||
attr.datatype.item_type(
|
|
||||||
o,
|
|
||||||
keyonly=attr_keyonly,
|
|
||||||
attrs=attr_attrs,
|
|
||||||
eagerload=attr_eagerload
|
|
||||||
)
|
|
||||||
for o in value
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
value = attr.datatype(
|
|
||||||
value,
|
|
||||||
keyonly=attr_keyonly,
|
|
||||||
attrs=attr_attrs,
|
|
||||||
eagerload=attr_eagerload
|
|
||||||
)
|
|
||||||
attr.__set__(self, value)
|
|
||||||
|
|
||||||
def to_instance(self, instance):
|
|
||||||
for attr in self._wsme_attributes:
|
|
||||||
if isinstance(attr, wsattr):
|
|
||||||
value = attr.__get__(self, self.__class__)
|
|
||||||
if value is not wsme.types.Unset:
|
|
||||||
setattr(instance, attr.saname, value)
|
|
||||||
|
|
||||||
def get_ref_criterion(self):
|
|
||||||
"""Returns a criterion that match a database object
|
|
||||||
having the pkey/ref attribute values of this webservice object"""
|
|
||||||
criterions = []
|
|
||||||
for attr in self._pkey_attrs + self._ref_attrs:
|
|
||||||
value = attr.__get__(self, self.__class__)
|
|
||||||
if value is not wsme.types.Unset:
|
|
||||||
criterions.append(attr.saproperty == value)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_types(*classes, **kw):
|
|
||||||
registry = kw.pop('registry', wsme.types.registry)
|
|
||||||
prefix = kw.pop('prefix', '')
|
|
||||||
postfix = kw.pop('postfix', '')
|
|
||||||
makename = kw.pop('makename', lambda s: prefix + s + postfix)
|
|
||||||
|
|
||||||
newtypes = {}
|
|
||||||
for c in classes:
|
|
||||||
if isinstance(c, list):
|
|
||||||
newtypes.update(generate_types(c))
|
|
||||||
else:
|
|
||||||
name = makename(c.__name__)
|
|
||||||
newtypes[name] = BaseMeta(name, (Base, ), {
|
|
||||||
'__saclass__': c,
|
|
||||||
'__registry__': registry
|
|
||||||
})
|
|
||||||
return newtypes
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json # noqa
|
|
||||||
|
|
||||||
import wsme.tests.protocol
|
|
||||||
from wsme.utils import parse_isodatetime, parse_isodate, parse_isotime
|
|
||||||
from wsme.types import isarray, isdict, isusertype
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
if six.PY3:
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
else:
|
|
||||||
from urllib import urlencode # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def encode_arg(value):
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
value, datatype = value
|
|
||||||
else:
|
|
||||||
datatype = type(value)
|
|
||||||
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
value = [encode_arg((item, datatype[0])) for item in value]
|
|
||||||
elif isinstance(datatype, dict):
|
|
||||||
key_type, value_type = list(datatype.items())[0]
|
|
||||||
value = dict((
|
|
||||||
(encode_arg((key, key_type)),
|
|
||||||
encode_arg((value, value_type)))
|
|
||||||
for key, value in value.items()
|
|
||||||
))
|
|
||||||
elif datatype in (datetime.date, datetime.time, datetime.datetime):
|
|
||||||
value = value.isoformat()
|
|
||||||
elif datatype == wsme.types.binary:
|
|
||||||
value = base64.encodestring(value).decode('ascii')
|
|
||||||
elif datatype == wsme.types.bytes:
|
|
||||||
value = value.decode('ascii')
|
|
||||||
elif datatype == decimal.Decimal:
|
|
||||||
value = str(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def decode_result(value, datatype):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if datatype == wsme.types.binary:
|
|
||||||
value = base64.decodestring(value.encode('ascii'))
|
|
||||||
return value
|
|
||||||
if isusertype(datatype):
|
|
||||||
datatype = datatype.basetype
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
value = [decode_result(item, datatype[0]) for item in value]
|
|
||||||
elif isarray(datatype):
|
|
||||||
value = [decode_result(item, datatype.item_type) for item in value]
|
|
||||||
elif isinstance(datatype, dict):
|
|
||||||
key_type, value_type = list(datatype.items())[0]
|
|
||||||
value = dict((
|
|
||||||
(decode_result(key, key_type),
|
|
||||||
decode_result(value, value_type))
|
|
||||||
for key, value in value.items()
|
|
||||||
))
|
|
||||||
elif isdict(datatype):
|
|
||||||
key_type, value_type = datatype.key_type, datatype.value_type
|
|
||||||
value = dict((
|
|
||||||
(decode_result(key, key_type),
|
|
||||||
decode_result(value, value_type))
|
|
||||||
for key, value in value.items()
|
|
||||||
))
|
|
||||||
elif datatype == datetime.time:
|
|
||||||
value = parse_isotime(value)
|
|
||||||
elif datatype == datetime.date:
|
|
||||||
value = parse_isodate(value)
|
|
||||||
elif datatype == datetime.datetime:
|
|
||||||
value = parse_isodatetime(value)
|
|
||||||
elif hasattr(datatype, '_wsme_attributes'):
|
|
||||||
for attr in datatype._wsme_attributes:
|
|
||||||
if attr.key not in value:
|
|
||||||
continue
|
|
||||||
value[attr.key] = decode_result(value[attr.key], attr.datatype)
|
|
||||||
elif datatype == decimal.Decimal:
|
|
||||||
value = decimal.Decimal(value)
|
|
||||||
elif datatype == wsme.types.bytes:
|
|
||||||
value = value.encode('ascii')
|
|
||||||
elif datatype is not None and type(value) != datatype:
|
|
||||||
value = datatype(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class TestExtDirectProtocol(wsme.tests.protocol.ProtocolTestCase):
|
|
||||||
protocol = 'extdirect'
|
|
||||||
protocol_options = {
|
|
||||||
'namespace': 'MyNS.api',
|
|
||||||
'nsfolder': 'app'
|
|
||||||
}
|
|
||||||
|
|
||||||
def call(self, fname, _rt=None, _no_result_decode=False, _accept=None,
|
|
||||||
**kw):
|
|
||||||
path = fname.split('/')
|
|
||||||
try:
|
|
||||||
func, funcdef, args = self.root._lookup_function(path)
|
|
||||||
arguments = funcdef.arguments
|
|
||||||
except:
|
|
||||||
arguments = []
|
|
||||||
if len(path) == 1:
|
|
||||||
ns, action, fname = '', '', path[0]
|
|
||||||
elif len(path) == 2:
|
|
||||||
ns, action, fname = '', path[0], path[1]
|
|
||||||
else:
|
|
||||||
ns, action, fname = '.'.join(path[:-2]), path[-2], path[-1]
|
|
||||||
print(kw)
|
|
||||||
|
|
||||||
args = [
|
|
||||||
dict(
|
|
||||||
(arg.name, encode_arg(kw[arg.name]))
|
|
||||||
for arg in arguments if arg.name in kw
|
|
||||||
)
|
|
||||||
]
|
|
||||||
print("args =", args)
|
|
||||||
data = json.dumps({
|
|
||||||
'type': 'rpc',
|
|
||||||
'tid': 0,
|
|
||||||
'action': action,
|
|
||||||
'method': fname,
|
|
||||||
'data': args,
|
|
||||||
})
|
|
||||||
print(data)
|
|
||||||
headers = {'Content-Type': 'application/json'}
|
|
||||||
if _accept:
|
|
||||||
headers['Accept'] = _accept
|
|
||||||
res = self.app.post('/extdirect/router/%s' % ns, data, headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
|
|
||||||
print(res.body)
|
|
||||||
|
|
||||||
if _no_result_decode:
|
|
||||||
return res
|
|
||||||
|
|
||||||
data = json.loads(res.text)
|
|
||||||
if data['type'] == 'rpc':
|
|
||||||
r = data['result']
|
|
||||||
return decode_result(r, _rt)
|
|
||||||
elif data['type'] == 'exception':
|
|
||||||
faultcode, faultstring = data['message'].split(': ', 1)
|
|
||||||
debuginfo = data.get('where')
|
|
||||||
raise wsme.tests.protocol.CallException(
|
|
||||||
faultcode, faultstring, debuginfo)
|
|
||||||
|
|
||||||
def test_api_alias(self):
|
|
||||||
assert self.root._get_protocol('extdirect').api_alias == '/app/api.js'
|
|
||||||
|
|
||||||
def test_get_api(self):
|
|
||||||
res = self.app.get('/app/api.js')
|
|
||||||
print(res.body)
|
|
||||||
assert res.body
|
|
||||||
|
|
||||||
def test_positional(self):
|
|
||||||
self.root._get_protocol('extdirect').default_params_notation = \
|
|
||||||
'positional'
|
|
||||||
|
|
||||||
data = json.dumps({
|
|
||||||
'type': 'rpc',
|
|
||||||
'tid': 0,
|
|
||||||
'action': 'misc',
|
|
||||||
'method': 'multiply',
|
|
||||||
'data': [2, 5],
|
|
||||||
})
|
|
||||||
headers = {'Content-Type': 'application/json'}
|
|
||||||
res = self.app.post('/extdirect/router', data, headers=headers)
|
|
||||||
|
|
||||||
print(res.body)
|
|
||||||
|
|
||||||
data = json.loads(res.text)
|
|
||||||
assert data['type'] == 'rpc'
|
|
||||||
r = data['result']
|
|
||||||
assert r == 10
|
|
||||||
|
|
||||||
def test_batchcall(self):
|
|
||||||
data = json.dumps([{
|
|
||||||
'type': 'rpc',
|
|
||||||
'tid': 1,
|
|
||||||
'action': 'argtypes',
|
|
||||||
'method': 'setdate',
|
|
||||||
'data': [{'value': '2011-04-06'}],
|
|
||||||
}, {
|
|
||||||
'type': 'rpc',
|
|
||||||
'tid': 2,
|
|
||||||
'action': 'returntypes',
|
|
||||||
'method': 'getbytes',
|
|
||||||
'data': []
|
|
||||||
}])
|
|
||||||
print(data)
|
|
||||||
headers = {'Content-Type': 'application/json'}
|
|
||||||
res = self.app.post('/extdirect/router', data, headers=headers)
|
|
||||||
|
|
||||||
print(res.body)
|
|
||||||
|
|
||||||
rdata = json.loads(res.text)
|
|
||||||
|
|
||||||
assert len(rdata) == 2
|
|
||||||
|
|
||||||
assert rdata[0]['tid'] == 1
|
|
||||||
assert rdata[0]['result'] == '2011-04-06'
|
|
||||||
assert rdata[1]['tid'] == 2
|
|
||||||
assert rdata[1]['result'] == 'astring'
|
|
||||||
|
|
||||||
def test_form_call(self):
|
|
||||||
params = {
|
|
||||||
'value[0].inner.aint': 54,
|
|
||||||
'value[1].inner.aint': 55,
|
|
||||||
'extType': 'rpc',
|
|
||||||
'extTID': 1,
|
|
||||||
'extAction': 'argtypes',
|
|
||||||
'extMethod': 'setnestedarray',
|
|
||||||
}
|
|
||||||
|
|
||||||
body = urlencode(params)
|
|
||||||
r = self.app.post(
|
|
||||||
'/extdirect/router',
|
|
||||||
body,
|
|
||||||
headers={'Content-Type': 'application/x-www-form-urlencoded'}
|
|
||||||
)
|
|
||||||
print(r)
|
|
||||||
|
|
||||||
assert json.loads(r.text) == {
|
|
||||||
"tid": "1",
|
|
||||||
"action": "argtypes",
|
|
||||||
"type": "rpc",
|
|
||||||
"method": "setnestedarray",
|
|
||||||
"result": [{
|
|
||||||
"inner": {
|
|
||||||
"aint": 54
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
"inner": {
|
|
||||||
"aint": 55
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
@@ -1,423 +0,0 @@
|
|||||||
import decimal
|
|
||||||
import datetime
|
|
||||||
import base64
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import wsme.tests.protocol
|
|
||||||
|
|
||||||
try:
|
|
||||||
import xml.etree.ElementTree as et
|
|
||||||
except:
|
|
||||||
import cElementTree as et # noqa
|
|
||||||
|
|
||||||
import suds.cache
|
|
||||||
import suds.client
|
|
||||||
import suds.transport
|
|
||||||
|
|
||||||
import wsme.utils
|
|
||||||
|
|
||||||
|
|
||||||
class XDecimal(suds.xsd.sxbuiltin.XBuiltin):
|
|
||||||
def translate(self, value, topython=True):
|
|
||||||
if topython:
|
|
||||||
if isinstance(value, six.string_types) and len(value):
|
|
||||||
return decimal.Decimal(value)
|
|
||||||
else:
|
|
||||||
if isinstance(value, (decimal.Decimal, int, float)):
|
|
||||||
return str(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
suds.xsd.sxbuiltin.Factory.tags['decimal'] = XDecimal
|
|
||||||
|
|
||||||
|
|
||||||
class WebtestSudsTransport(suds.transport.Transport):
|
|
||||||
def __init__(self, app):
|
|
||||||
suds.transport.Transport.__init__(self)
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
def open(self, request):
|
|
||||||
res = self.app.get(request.url, headers=request.headers)
|
|
||||||
return six.BytesIO(res.body)
|
|
||||||
|
|
||||||
def send(self, request):
|
|
||||||
res = self.app.post(
|
|
||||||
request.url,
|
|
||||||
request.message,
|
|
||||||
headers=dict((
|
|
||||||
(key, str(value)) for key, value in request.headers.items()
|
|
||||||
)),
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
return suds.transport.Reply(
|
|
||||||
res.status_int,
|
|
||||||
dict(res.headers),
|
|
||||||
res.body
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SudsCache(suds.cache.Cache):
|
|
||||||
def __init__(self):
|
|
||||||
self.d = {}
|
|
||||||
|
|
||||||
def get(self, id):
|
|
||||||
return self.d.get(id)
|
|
||||||
|
|
||||||
def getf(self, id):
|
|
||||||
b = self.get(id)
|
|
||||||
if b is not None:
|
|
||||||
return six.StringIO(self.get(id))
|
|
||||||
|
|
||||||
def put(self, id, bfr):
|
|
||||||
self.d[id] = bfr
|
|
||||||
|
|
||||||
def putf(self, id, fp):
|
|
||||||
self.put(id, fp.read())
|
|
||||||
|
|
||||||
def purge(self, id):
|
|
||||||
try:
|
|
||||||
del self.d[id]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def clear(self, id):
|
|
||||||
self.d = {}
|
|
||||||
|
|
||||||
|
|
||||||
sudscache = SudsCache()
|
|
||||||
|
|
||||||
tns = "http://foo.bar.baz/soap/"
|
|
||||||
typenamespace = "http://foo.bar.baz/types/"
|
|
||||||
|
|
||||||
soapenv_ns = 'http://schemas.xmlsoap.org/soap/envelope/'
|
|
||||||
xsi_ns = 'http://www.w3.org/2001/XMLSchema-instance'
|
|
||||||
body_qn = '{%s}Body' % soapenv_ns
|
|
||||||
fault_qn = '{%s}Fault' % soapenv_ns
|
|
||||||
faultcode_qn = '{%s}faultcode' % soapenv_ns
|
|
||||||
faultstring_qn = '{%s}faultstring' % soapenv_ns
|
|
||||||
faultdetail_qn = '{%s}detail' % soapenv_ns
|
|
||||||
type_qn = '{%s}type' % xsi_ns
|
|
||||||
nil_qn = '{%s}nil' % xsi_ns
|
|
||||||
|
|
||||||
|
|
||||||
def build_soap_message(method, params=""):
|
|
||||||
message = """<?xml version="1.0"?>
|
|
||||||
<soap:Envelope
|
|
||||||
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
||||||
|
|
||||||
<soap:Body xmlns="%(typenamespace)s">
|
|
||||||
<%(method)s>
|
|
||||||
%(params)s
|
|
||||||
</%(method)s>
|
|
||||||
</soap:Body>
|
|
||||||
|
|
||||||
</soap:Envelope>
|
|
||||||
""" % dict(method=method, params=params, typenamespace=typenamespace)
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
python_types = {
|
|
||||||
int: ('xs:int', str),
|
|
||||||
float: ('xs:float', str),
|
|
||||||
bool: ('xs:boolean', str),
|
|
||||||
wsme.types.bytes: (
|
|
||||||
'xs:string',
|
|
||||||
lambda x: x.decode('ascii') if isinstance(x, wsme.types.bytes) else x
|
|
||||||
),
|
|
||||||
wsme.types.text: ('xs:string', wsme.types.text),
|
|
||||||
wsme.types.binary: (
|
|
||||||
'xs:base64Binary',
|
|
||||||
lambda x: base64.encodestring(x).decode('ascii')
|
|
||||||
),
|
|
||||||
decimal.Decimal: ('xs:decimal', str),
|
|
||||||
datetime.date: ('xs:date', datetime.date.isoformat),
|
|
||||||
datetime.time: ('xs:time', datetime.time.isoformat),
|
|
||||||
datetime.datetime: ('xs:dateTime', datetime.datetime.isoformat),
|
|
||||||
}
|
|
||||||
|
|
||||||
array_types = {
|
|
||||||
wsme.types.bytes: "String_Array",
|
|
||||||
wsme.types.text: "String_Array",
|
|
||||||
int: "Int_Array",
|
|
||||||
float: "Float_Array",
|
|
||||||
bool: "Boolean_Array",
|
|
||||||
datetime.datetime: "dateTime_Array"
|
|
||||||
}
|
|
||||||
|
|
||||||
if not six.PY3:
|
|
||||||
array_types[long] = "Long_Array"
|
|
||||||
|
|
||||||
|
|
||||||
def tosoap(tag, value):
|
|
||||||
el = et.Element(tag)
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
value, datatype = value
|
|
||||||
else:
|
|
||||||
datatype = type(value)
|
|
||||||
if value is None:
|
|
||||||
el.set('xsi:nil', 'true')
|
|
||||||
return el
|
|
||||||
if datatype in python_types:
|
|
||||||
stype, conv = python_types[datatype]
|
|
||||||
el.text = conv(value)
|
|
||||||
el.set('xsi:type', stype)
|
|
||||||
el.text = str(value)
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
def tosuds(client, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
value, datatype = value
|
|
||||||
else:
|
|
||||||
datatype = type(value)
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if isinstance(datatype, list):
|
|
||||||
if datatype[0] in array_types:
|
|
||||||
tname = array_types[datatype[0]]
|
|
||||||
else:
|
|
||||||
tname = datatype[0].__name__ + '_Array'
|
|
||||||
o = client.factory.create('types:' + tname)
|
|
||||||
o.item = [tosuds(client, (item, datatype[0])) for item in value]
|
|
||||||
return o
|
|
||||||
elif datatype in python_types:
|
|
||||||
return python_types[datatype][1](value)
|
|
||||||
else:
|
|
||||||
o = client.factory.create('types:' + datatype.__name__)
|
|
||||||
|
|
||||||
for attr in datatype._wsme_attributes:
|
|
||||||
if attr.name in value:
|
|
||||||
setattr(
|
|
||||||
o, attr.name,
|
|
||||||
tosuds(client, (value[attr.name], attr.datatype))
|
|
||||||
)
|
|
||||||
return o
|
|
||||||
|
|
||||||
|
|
||||||
def read_bool(value):
|
|
||||||
return value == 'true'
|
|
||||||
|
|
||||||
|
|
||||||
soap_types = {
|
|
||||||
'xs:string': wsme.types.text,
|
|
||||||
'xs:int': int,
|
|
||||||
'xs:long': int if six.PY3 else long,
|
|
||||||
'xs:float': float,
|
|
||||||
'xs:decimal': decimal.Decimal,
|
|
||||||
'xs:boolean': read_bool,
|
|
||||||
'xs:date': wsme.utils.parse_isodate,
|
|
||||||
'xs:time': wsme.utils.parse_isotime,
|
|
||||||
'xs:dateTime': wsme.utils.parse_isodatetime,
|
|
||||||
'xs:base64Binary': base64.decodestring,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def fromsoap(el):
|
|
||||||
if el.get(nil_qn) == 'true':
|
|
||||||
return None
|
|
||||||
t = el.get(type_qn)
|
|
||||||
if t == 'xs:string':
|
|
||||||
return wsme.types.text(el.text if el.text else '')
|
|
||||||
if t in soap_types:
|
|
||||||
return soap_types[t](el.text)
|
|
||||||
elif t and t.endswith('_Array'):
|
|
||||||
return [fromsoap(i) for i in el]
|
|
||||||
else:
|
|
||||||
d = {}
|
|
||||||
for child in el:
|
|
||||||
name = child.tag
|
|
||||||
assert name.startswith('{%s}' % typenamespace), name
|
|
||||||
name = name[len(typenamespace) + 2:]
|
|
||||||
d[name] = fromsoap(child)
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def tobytes(value):
|
|
||||||
if isinstance(value, wsme.types.text):
|
|
||||||
value = value.encode()
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def tobin(value):
|
|
||||||
value = base64.decodestring(value.encode())
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
fromsuds_types = {
|
|
||||||
wsme.types.binary: tobin,
|
|
||||||
wsme.types.bytes: tobytes,
|
|
||||||
decimal.Decimal: decimal.Decimal,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def fromsuds(dt, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if isinstance(dt, list):
|
|
||||||
return [fromsuds(dt[0], item) for item in value.item]
|
|
||||||
if wsme.types.isarray(dt):
|
|
||||||
return [fromsuds(dt.item_type, item) for item in value.item]
|
|
||||||
if wsme.types.isusertype(dt) and dt not in fromsuds_types:
|
|
||||||
dt = dt.basetype
|
|
||||||
if dt in fromsuds_types:
|
|
||||||
print(dt, value)
|
|
||||||
value = fromsuds_types[dt](value)
|
|
||||||
print(value)
|
|
||||||
return value
|
|
||||||
if wsme.types.iscomplex(dt):
|
|
||||||
d = {}
|
|
||||||
for attrdef in dt._wsme_attributes:
|
|
||||||
if not hasattr(value, attrdef.name):
|
|
||||||
continue
|
|
||||||
d[attrdef.name] = fromsuds(
|
|
||||||
attrdef.datatype, getattr(value, attrdef.name)
|
|
||||||
)
|
|
||||||
return d
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class TestSOAP(wsme.tests.protocol.ProtocolTestCase):
|
|
||||||
protocol = 'soap'
|
|
||||||
protocol_options = dict(tns=tns, typenamespace=typenamespace)
|
|
||||||
ws_path = '/'
|
|
||||||
_sudsclient = None
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
wsme.tests.protocol.ProtocolTestCase.setUp(self)
|
|
||||||
|
|
||||||
def test_simple_call(self):
|
|
||||||
message = build_soap_message('touch')
|
|
||||||
print(message)
|
|
||||||
res = self.app.post(
|
|
||||||
self.ws_path,
|
|
||||||
message,
|
|
||||||
headers={"Content-Type": "application/soap+xml; charset=utf-8"},
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
print(res.body)
|
|
||||||
assert res.status.startswith('200')
|
|
||||||
|
|
||||||
def call(self, fpath, _rt=None, _accept=None, _no_result_decode=False,
|
|
||||||
**kw):
|
|
||||||
|
|
||||||
if _no_result_decode or _accept or self._testMethodName in (
|
|
||||||
'test_missing_argument', 'test_invalid_path', 'test_settext_empty',
|
|
||||||
'test_settext_none'
|
|
||||||
):
|
|
||||||
return self.raw_call(fpath, _rt, _accept, _no_result_decode, **kw)
|
|
||||||
|
|
||||||
path = fpath.strip('/').split('/')
|
|
||||||
methodname = ''.join([path[0]] + [i.capitalize() for i in path[1:]])
|
|
||||||
|
|
||||||
m = getattr(self.sudsclient.service, methodname)
|
|
||||||
kw = dict((
|
|
||||||
(key, tosuds(self.sudsclient, value)) for key, value in kw.items()
|
|
||||||
))
|
|
||||||
print(kw)
|
|
||||||
try:
|
|
||||||
return fromsuds(_rt, m(**kw))
|
|
||||||
except suds.WebFault as exc:
|
|
||||||
raise wsme.tests.protocol.CallException(
|
|
||||||
exc.fault.faultcode,
|
|
||||||
exc.fault.faultstring,
|
|
||||||
getattr(exc.fault, 'detail', None) or None
|
|
||||||
)
|
|
||||||
|
|
||||||
def raw_call(self, fpath, _rt=None, _accept=None, _no_result_decode=False,
|
|
||||||
**kw):
|
|
||||||
path = fpath.strip('/').split('/')
|
|
||||||
methodname = ''.join([path[0]] + [i.capitalize() for i in path[1:]])
|
|
||||||
# get the actual definition so we can build the adequate request
|
|
||||||
if kw:
|
|
||||||
el = et.Element('parameters')
|
|
||||||
for key, value in kw.items():
|
|
||||||
el.append(tosoap(key, value))
|
|
||||||
|
|
||||||
params = six.b("\n").join(et.tostring(el) for el in el)
|
|
||||||
else:
|
|
||||||
params = ""
|
|
||||||
methodname = ''.join([path[0]] + [i.capitalize() for i in path[1:]])
|
|
||||||
message = build_soap_message(methodname, params)
|
|
||||||
print(message)
|
|
||||||
headers = {"Content-Type": "application/soap+xml; charset=utf-8"}
|
|
||||||
if _accept is not None:
|
|
||||||
headers['Accept'] = _accept
|
|
||||||
res = self.app.post(
|
|
||||||
self.ws_path,
|
|
||||||
message,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True
|
|
||||||
)
|
|
||||||
print("Status: ", res.status, "Received:", res.body)
|
|
||||||
|
|
||||||
if _no_result_decode:
|
|
||||||
return res
|
|
||||||
|
|
||||||
el = et.fromstring(res.body)
|
|
||||||
body = el.find(body_qn)
|
|
||||||
print(body)
|
|
||||||
|
|
||||||
if res.status_int == 200:
|
|
||||||
response_tag = '{%s}%sResponse' % (typenamespace, methodname)
|
|
||||||
r = body.find(response_tag)
|
|
||||||
result = r.find('{%s}result' % typenamespace)
|
|
||||||
print("Result element: ", result)
|
|
||||||
return fromsoap(result)
|
|
||||||
elif res.status_int == 400:
|
|
||||||
fault = body.find(fault_qn)
|
|
||||||
raise wsme.tests.protocol.CallException(
|
|
||||||
fault.find(faultcode_qn).text,
|
|
||||||
fault.find(faultstring_qn).text,
|
|
||||||
"")
|
|
||||||
|
|
||||||
elif res.status_int == 500:
|
|
||||||
fault = body.find(fault_qn)
|
|
||||||
raise wsme.tests.protocol.CallException(
|
|
||||||
fault.find(faultcode_qn).text,
|
|
||||||
fault.find(faultstring_qn).text,
|
|
||||||
fault.find(faultdetail_qn) is not None and
|
|
||||||
fault.find(faultdetail_qn).text or None)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sudsclient(self):
|
|
||||||
if self._sudsclient is None:
|
|
||||||
self._sudsclient = suds.client.Client(
|
|
||||||
self.ws_path + 'api.wsdl',
|
|
||||||
transport=WebtestSudsTransport(self.app),
|
|
||||||
cache=sudscache
|
|
||||||
)
|
|
||||||
return self._sudsclient
|
|
||||||
|
|
||||||
def test_wsdl(self):
|
|
||||||
c = self.sudsclient
|
|
||||||
assert c.wsdl.tns[1] == tns, c.wsdl.tns
|
|
||||||
|
|
||||||
sd = c.sd[0]
|
|
||||||
|
|
||||||
assert len(sd.ports) == 1
|
|
||||||
port, methods = sd.ports[0]
|
|
||||||
self.assertEqual(len(methods), 51)
|
|
||||||
|
|
||||||
methods = dict(methods)
|
|
||||||
|
|
||||||
assert 'returntypesGettext' in methods
|
|
||||||
print(methods)
|
|
||||||
|
|
||||||
assert methods['argtypesSettime'][0][0] == 'value'
|
|
||||||
|
|
||||||
def test_return_nesteddict(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_setnesteddict(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_return_objectdictattribute(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_setnested_nullobj(self):
|
|
||||||
pass # TODO write a soap adapted version of this test.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user