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