diff -Nru python-wsme-0.5b1/AUTHORS python-wsme-0.6/AUTHORS --- python-wsme-0.5b1/AUTHORS 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/AUTHORS 2014-02-06 14:49:32.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru python-wsme-0.5b1/ChangeLog python-wsme-0.6/ChangeLog --- python-wsme-0.5b1/ChangeLog 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/ChangeLog 2014-02-06 14:49:32.000000000 +0000 @@ -0,0 +1,699 @@ +CHANGES +======= + +0.6 +--- + +* 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 +----- + +* Add changes entry for 0.5b6 +* json: convert value to string before encoding +* Run Flask tests by default +* Validate body when using Pecan +* Remove MANIFEST.in +* Return a ClientSideError if unable to convert data +* Add custom error code to ClientSideError +* doc: remove useless validate import +* Enable and fix Sphinx tests +* Handle [] {} for body in rest protocols +* types: fix error return when None is in Enum +* Add a test environment against pecan's development (master) branch +* pecantest: remove useless config.py +* Include Pecan tests in default Python environment +* Add a test case for wsattr default +* Handle mandatory attributes +* Remove various usage of sys.exc_info() +* Minor code cleanups + +0.5b5 +----- + +* update the b5 release date +* Add improved support for HTTP response codes in cornice apps +* Remove version number from setup.cfg + +0.5b4 +----- + +* Update tox config to allow packaging jobs to work + +0.5b3 +----- + +* The cornice adapter will not make it in 0.5b3 +* Completed Changelog +* Add improved support for HTTP response codes in TG 1.x apps +* Add improved support for HTTP response codes in flask apps +* Require the ordereddict package for py26 +* pep8 fixes, including a declaration in tox.ini for running flake8 +* Remove py25 and add 26 support to tox +* Remove a deprecated flag from tox.ini +* Changelog for version 0.5b3 +* Fix BaseMeta with six >= 1.4.0 +* Fix for returned status code and .gitignore +* Add a py33 tox target +* Fix attributes sorting based on source code +* Sort set in type exception +* Switch to pbr +* Setup a .gitignore file +* Fix a little syntax error +* Update README and package metadata to reflect gerrit review process +* Add .gitreview file +* Merged in sileht/wsme/sileht/unicode-clientsideerror (pull request #16) +* Support unicode message in ClientSideError +* Fix issue #11 in the pecan adapter +* Add a unittest to reproduce issue #11 +* Require a python2.5 compatible version of Jinja2 +* The Response object can now carry error details. Not sure about this though, it needs refinements +* Fix inner null objects in the extdirect protocol +* Fix returning objects with object attributes set to 'None' +* More --nologcapture to run tests so that suds does not get in the way +* use --nologcapture to avoid a nasty failure when suds do some logging +* Use assertEquals so we see what is the wrong value if any +* Merged in asalkeld/wsme-2 (pull request #15) +* Test changing the default status_code in pecan +* Add a test to make sure we can use the Response from pecan +* Change version to 0.5b3 (may change to 0.5 directly) +* pecan: Make it possible to use the Response to return non-default status codes +* Added tag 0.5b2 for changeset d3e5eee0b150 + +0.5b2 +----- + +* Prepare next release +* Install mini-doc now use pip +* wsmeext.cornice now handle errors properly +* Tests cornice resource +* Include missing files +* wsmeext.cornice.signature can now decorate resource class methods +* Pecan adapter: Debug mode (which returns the exception tracebacks to the client) can be enabled by the pecan application configuration +* Explicitely ignore the routines when scanning a class +* Fix a problem when a complex type has a 'attr' attribute, due to the DataHolder __slots__ list construction, which leads to a DataHolder having a wsattr +* Please flake8 +* 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 '_' +* Allow a wsme.types.Base child class to override the default 'wsattr' class by having a __wsattrclass__ class attribute +* If dateutil is present, dateutil.parser is used to aparse the iso dates +* The rest encodings now use the parse_iso[date|time] functions of wsme.utils +* Fix ClientSideError constructor +* The cornice adapter now handle the url matched parameters +* Fix the ClientSideError constructor so that it is propertly displayed in the backtrace +* Small documentation improvements +* Use a cyrilic unicode sample in the demo instead of japanese so that the pdf build is easier +* Add a chapter on the use of HostRequest +* Improve the documentation +* Add the last change to the changelog +* Move the missing argument verification in a wsme.runtime module so that it can be used in all adapters +* Move the function documentation code to a separate function +* A new HostRequest type can be used to have the host framework request object passed to the exposed functions +* Create some (incomplete) tests for the cornice adapter and fix it +* Add tests and fix check ordering in flask.py +* Backed out changeset d6facd75c051 +* Fix typo +* Fix typo +* Re-iterate status_code code +* Merged cdevienne/wsme into default +* Document the Flask adapter +* Fix array parameters support in the Flask adapter +* Fixup support for content types in other places then Headers +* Support override of response format via request.dataformat +* Add support for passing status_code or getting it from request +* Fix the way the TG adapter calls wsme.rest.args.get_args +* Fix the way the pecan adapter calls wsme.rest.args.get_args +* Various fixes for the flask adapter +* Add more tests +* Downgrade webtest for tg11 and tg15 tests that are python2.5 based +* Readd webtest for python 3 +* More python2.5 workaround +* Workaround issues with python2.5 environment tests +* Fix the serverside error test +* flask is now part of the default test suite +* Test & fix Server-side errors +* Flask simple call now works +* Remove ipdb +* New flask adapter + test (run "tox -e flask" to test) +* update the Changelog for next version + update version number +* Fix a bug when the only parameter of a function is a 'body' parameter +* Add a test to reproduce the bug reported by Endre Karlson +* Add a test for the body= parameter of wsexpose +* Re add the dev and build tags on version +* Don't use the validate decorator in the first-page example +* Remove the WSME-Soap dependency +* Add missing modules to the packages list +* Made the summary shorter (see issue #6) +* Add a google analytics id +* Added tag 0.5b1 for changeset 359199eb4e09 + +0.5b1 +----- + +* Merging a dead branch (messed up with 'amend' +* Merging a dead branch (messed up with 'amend') +* Merging a dead branch (messed up with 'amend') +* Merging a dead branch (messed up with 'amend') +* Merging a dead branch (messed up with 'amend') +* Merging a dead branch (messed up with 'amend') +* Update the changelog and add a requirements file to give readthedoc a try +* amends 9c4e1f9a0c129cbb690bdd0459530c793aa3273b +* Document the tg1x and cornice adapters +* Document the tg1x and cornice adapters +* Start documenting the new integration approach +* Rewrite a bit the introduction text +* Now use the awesome 'Cloud' sphinx theme +* Do not test py25 with std json +* Do not test py32 with sqlalchemy 0.5 anymore +* Update the dependencies in the documentation +* Don't install wsme-sqlalchemy and wsme-extdirect to build the doc anymore +* Fix a python3 w/o lxml issue with binary serialization in the soap protocol +* Merged in wsme-sqlalchemy +* Add missing dependencies +* Now use toxgen to produce the tox.ini file +* Add missing __init__.py in wsmeext.tests +* add tests for file property of File +* fix binary type tests for python 3 +* add tests for file types and fix a python 3 issue with handling files coming from fieldstorage objects +* add tests for weakref conversions for type references +* test invalid float values +* add tests for binary encoding and decoding +* add tests for ArrayType validation +* amends a32bd89f8984b13f22a9fe5b66b881c91308e459 +* tox -e doc now produce the zipped html documentation too +* tox -e doc now produce the zipped html documentation too +* Tell about the __body__ parameter in the changelog +* Fix the wsmeext.sphinxext module name +* Fix the wsmeext.sphinxext module name +* Add a 'doc' testenv that builds the documentation +* Add a 'doc' testenv that builds the documentation +* Complete the changelog for version 0.5b1 +* Complete the changelog for version 0.5b1 +* Set version to 0.5b1 +* Set version to 0.5b1 +* Use nosetests --with-coverage instead of coverage run for testenv that tests wsmeext submodules (I have issues with the namespace module) +* Simplify __body__ extraction from params +* Fix unicode values read from json input on python 2.5 +* Don't read the body if content_length is 0-like +* Choose float values that have no rounding issue on python 2.5 for the float multiplication test +* Python 3 compatibility +* Rename the body argument to __body__ in tests + now tests for request body single argument +* Now rely on wsme.rest.args to parse the parameters, which avoids a lot of code duplication +* Now handle __body__ parameter, which needed to add a mimetype parameter to the different args_from_* functions +* amends 1ea5bc68101a7f4075553df49fe58ba0b250316b +* Add a test for multiply exposed functions +* Add a test for multiply exposed functions +* Don't use assertIn as it is supported from python 2.7 only +* Use assertEquals(a, b) instead of assert a == b +* Moved non-core features to the wsmeext namespace package +* Pecan adapter is now at 100% of code coverage +* Fix code coverage for pecan tests +* Improve code coverage for the TG 1.5 adapter +* Code cleaning + better code coverage of the TG1.1 adapter +* Show missing lines in coverage reports +* Now generate coverage reports for individual testenv +* Fix the tg 1.x tests +* Ignore all the tests reports +* Merged in cmcdaniel/wsme/empty_strings3 (pull request #11: restxml empty string fixes) +* Fix Python 3 compatibility +* restxml empty string fixes +* Merged in cmcdaniel/wsme/json_strictness (pull request #8: rest protocol detection: test Accept header first; use startswith for Content-Type match) +* rest protocol detection: test Accept header first; use startswith for Content-Type match +* Merged in cmcdaniel/wsme/bool_fromsoap (pull request #7: Handle bool from xml properly; add setbool and getbool unit tests.) +* Handle bool from xml properly; add setbool and getbool unit tests +* Add a new parameter 'ignore_extra_args' to the @signature decorator (and its frontends @wsexpose) +* Fix the TG 1.x adapters, the resquest.params are now needed by the get_args function +* Now supports non-indexed arrays of objects as GET parameters +* Fix array as input GET parameters in the pecan adapter +* Update the changelog +* Mention the additional protocols in the intro +* Better mentionning of the framework independance +* Merged in dhellmann/wsme-sphinx (pull request #5) +* Move the check for an empty body up +* Add samples_slot option to TypeDocumenter +* Fixes for sphinxext +* Add sample() method to ArrayType and DictType +* Roll back previous change to the root XML tag name for sample data +* Copy properties to decorator +* Fix error handling tests for pecan +* Add a test for client-side errors +* Allow adapters to use the format_exception function, and use it in the Pecan adapter. /\!\ the response status is not properly changed by the decorator yet +* Allow the method autodocumenter (.. autofunction) to work without a Service parent (ie without a WSRoot). Added the path & method parameters that NOT considered for now +* PEP8 +* Update datetypename() to work with DictType and ArrayType instances +* Merged in dhellmann/wsme-validate (pull request #2) +* show the docstring for a type before the formatted sample values +* use tag name 'result' for sample data to match data returned by services +* tighten up validate_value logic and allow string promotion to integers +* ignore all coverage output files +* restrict the types that can be promoted to float +* ignore emacs temporary files +* Fix samples for functions parameters and return types +* Fix xml & json samples by autotype +* resolve types in pecan.wsexpose. It is a temporary solution +* resolve types in pecan.wsexpose. It is a temporary solution +* Merged in dhellmann/wsme-validate (pull request #1) +* Fix the encode_result call +* allow type promotion to float +* Use the new encode_result functions of the rest encoding modules +* Add unittests for returning array and dict of objects as attributes +* Add unittests for returning array and dict of objects as attributes +* Fix the __eq__ operator for ArrayType +* Fix the __eq__ operator for ArrayType +* Remove useless imports +* Remove useless imports +* Adapter for turbogears 1.1 +* Adding a tg 2.1 test case (not enabled for now) +* Reorganise the tg1x adapters, and make the tg 1.5 adapter able to handle multiprotocol on 'native' rest +* The tg1 adapter can now expose rest functions outside the WSRoot _and_ enable other other protocols. Soap is tested +* Change the scan_api interface. It now yield the original function and static args (typically the 'self' attribute). Thanks to that the lookup_function method of WSRoot can access functions outside the WSRoot +* Fix the module names +* args_from_body now ignore empty bodies +* Python <2.7 compat +* Python 3 compat +* Remove the parse_arg tests (parse_arg does not exist anymore), fix the json nest_result option handling, and fix the encode_sample tests and implementations +* Rework the rest implementation. We now have a single protocol that can manupulate different dataformat thanks to the helpers provided by the xml, json and args modules (which will be used by the adapters too). Some corner cases still don't pass the unittest, and some code cleaning is required +* Add a test for the pecan adapter +* Move around the REST implementation : wsme.protocols.commons -> wsme.rest.args, wsme.protocols.rest -> wsme.rest.protocol, wsme.protocols.restxml/json -> wsme.rest.xml/json, wsme.protocols.__init__ -> wsme.protocol +* Start working on a better tg 1.1 integration. Need to rework the rest implementation to make it easier (especially the body parsing) +* Don't use wsme.release anymore +* Change version to avoid dependencies problems in the CI +* Make DummyProtocol inherit from Protocol because it now needs a 'iter_routes' function +* wsme.protocols.expose now accept templated paths, and can expose a single function several times +* Introduce a new decoratore wsme.protocol.expose, which replaces the clumsy former pexpose +* Reorganise the decorators. expose and validate are now in wsme.rest, and ws.api.signature becomes the raw decorator to declare a function signature. Got rid of the 'pexpose' decorator, which will be replaced later by a better way +* The pecan adapter is now tested +* Make json the default format +* Rename WSRoot.scan_api to WSRoot._scan_api to avoid infinite recursion by scan_api +* Start working on adding protocols when used as a cornice complement +* Cornice extension: Fix function args preparation, and choose the renderer based on the 'Accept' header +* Adapter for cornice +* Split get_args in several functions to make adapters implementation easier +* Add a paramter 'multiple_expose' to the expose decorator +* merge +* Added tag 0.4 for changeset f06e004ca8e4 + +0.4 +--- + +* Add a test for one item long text arrays +* Get WSME version from the package, not wsme.release +* Update the changelog +* Fix the sample include line numbers +* Remove the b1 tag, 0.4 is about to be released +* Move the imports to avoid cross-import problems (all this needs some rework) +* Add a default value to FunctionDefinition.body_type +* Rest protocols can now take parameters from url + a parameter from the body by adding a parameter body= to expose() +* Add an adapter for pecan +* Add a helper function for adapters that need to convert a function arguments +* Get rid of the function wrapper. The only case it was needed is for exposing a function several times, but this case can be handled differently. I may reintroduce it as an option +* Introduce a new decorator: 'sig', which combines expose and validate in a single decorator +* Now use six.with_metaclass to create the Base type in a python 2/3 compatible way +* spore.getdesc does not take a request anymore but only the host url (it makes the tests simplier) +* Add a little demo of a SPORE client to call a function of the demo program +* Fix the spore 'base_url' attribute +* Fix the request headers log +* Test SPORE crud function descriptions +* Python 3 compat +* Fix the spore test, as some functions were added by restjson +* Initial implementation of SPORE description of the api (fetch /api.spore to get it) +* Fix test_default_usertype +* Test text to bytes auto-conversion +* text and bytes attributes now convert values from/to bytes/text when needed (ascii only conversion) +* Remove the now useless test_release (the release module was removed) +* Still need to specify the requires int the setup.py, as d2to1 does not seem to handle python_version dependant metadata +* Now using d2to1, which simplifies a lot the setup.py +* Added tag 0.4b1 for changeset 5ad01afed877 + +0.4b1 +----- + +* Update the change log (preparing release 0.4b1 +* To avoid any array or dict duplication, use set() instead of list() for the registry array_types and dict_types attributes +* Fix ArrayType __eq__ operator so that array types are not duplicated in the registry +* Add more list corner cases to test the soap behavior with empty arrays +* Allow 'None' to be set on an Array attribute +* Mention the new wsme.types.Base type in the changelog +* Fix the demo so that we can use the soap client again, and changed the function names in the client (the soap function naming scheme just changed) +* Fix the array and dict registering. The register() function has to be rethought, as resolve_type is doing more, and the ArrayType and DictType are in the game +* Mention the wsme.wsgi replacement by wsme.WSRoot.wsgiapp in the changelog +* Python 3 compatibility +* Fix the Enum constructor, and document the change +* Documents the File type +* Rename FileType to File, and make it a complex type instead of a native one +* Rest protocols should now accept multipart/form-data posts (see issue #4) +* New type: FileType. Supports file inputs in forms + documents and demonstrate it in the demo. Should solve issue #4 +* Mention wsme.types.Base +* The Base type for complex types now has a constructor that takes attribute values as kwargs +* Now use WSRoot.wsgiapp() instead of wsme.wsgi.adapt() +* Change the wsgi example to match the new way of obtaining a wsgi app from a WSRoot. Add a bottle integration example +* completing the wsgiapp docstring +* funcproxy now copy the function name +* Remove the wsgi adapter, it is now a function on WSRoot +* Fix a problem with protocol specific paths +* Rest protocols now make use of the http method to select the function is needed +* Use request.content_lenght to check for a request.body existence before accessing it +* Little english mistake fix +* resolve_type now always returns regular datatypes, never weak refs. Generally speaking weakrefs should remain inside the registry +* Registry.lookup does not return weakrefs anymore. resolve_type does it instead +* Python 3 compatibility +* Adapt tests and fix remaining issues with the list/dict handling changes +* Fix various bugs revealed by the soap unit tests +* Improve dict and array types handling by introducing DictType and ArrayType. Nested structures should have a better behavior +* Fix the list of complex types handling (dicts are not ok yet, and lists still not complet imo) +* Test attributes which are lists of complex types +* Got lost in metaclass __new__/__init__ choice. Yet another attempt using both (needed by wsme-sqlalchemy) +* My last commit was a mistake : it is better to use __new__, so that the class registering is done the later +* Use the metaclass __init__ instead of __new__ so that the class inspection occurs later, making it possible to add attributes in a sub-metaclass __new__ of __init__ +* Use py2 & py3 compatible metaclass +* Attempting a on-demand resolution of datatypes references, so we don't need to call resolve_references anymore. It works with python 2, but not yet python 3 (some weakref issues) +* Add a type Registry. It mainly allow to have easy cross-referenced types by setting the attribute datatype to the class name instead of the class instance. It may allow to have separate registry for different apis in the future +* Update the demo to reproduce issue #3 +* Completing the Changelog +* Add a test for unset attributes +* Add tests for the sphinx extension, raising the total coverage over 95% +* Fix Unset attributes serialisation +* Reproduce a bug with unset attributes serialisation to xml +* Introduce an adapter for tg 1.5 + unittest. It needs more realistic tests though +* Add a b1 version tag +* Now test the tg1 adapter +* Improve code coverage +* Simplifie the tox steps (no need for an initial clean) +* Now cover wsme.protocols.__init__ +* Now tests wsme.release +* restjson is now 100% covered by tests +* Add --show-missing to the coverage report +* Add wsme.tests to the egg dist +* merge +* Set version to 0.4, and update the documentation +* tox now combine code coverage results +* Point to the jenkins general dashboard instead of the wsme job +* Make the webob requirements more precise depending on the python version Add Python 3.2 to the classifiers +* webob 1.2b4 is not out yet.. +* Require the latest WebOb for python 3 +* Completed the python 3 port +* Python 3 port in progress +* Remove debug print statement +* Fix the binary type decoding +* Clarified the bytes/text types handling. Now all the restjson tests pass on python 2 and 3 +* The syntax is now python 3.2 compliant. A lot of tests still fail, I need to rethink how unicode / non-unicode are handled +* Remove a debug print statement +* Porting restjson to Python 3.2 (in progress) +* Python 3.2 port +* Python 3.2 port +* Python 3.2 compat +* Python 3.2 compat +* Reduce the default envlist +* test_types unit tests now successfully pass under python 3.2 +* Added a tox configuration file to ease the python 3 port +* Fix the README.rst filename +* Correct a sentence +* Change a bit the short description to make it more explicit +* Add a link to the Changelog in the main description +* Update the TODO list +* Adding the precise Python versions and implementation classifiers +* Add a missing indent +* Make the examples more compact +* Remove the .. highlight:: directives to remains plain-rst compatible +* Attempt a rename of README to README.rst to force bb rendering +* Add a small code sample at the very beginning of the documentation +* Added tag 0.3 for changeset 603c8586b076 + +0.3 +--- + +* Preparing the 0.3 release +* Update the changelog +* The function documenter now add parameters and return value samples +* Reached 100% test coverage of wsme.utils, which makes an overall coverage of 98% for the wsme module +* wsme.types is now 100% covered by unit tests +* Slightly improve wsme.root coverage +* Fixed nil date/time decoding from xml +* restxml now has a decent code coverage +* simplejson and native json dumps formated output behave differently. Adapting the unittest that depends on it +* More tests and coverage +* Now check for unknown arguments in request params +* Enable code coverage by default +* Improve wsme.protocols.rest code coverage +* 100% test coverage for wsme.protocols.commons +* Improve detection of double-exposed functions + added a unittest +* Improve documentation on types +* Mention toggle.css and toggle.js installation in the documentation +* Change version +* Update the sentense about Sphinx integration features +* handle_signature should return a value +* Start implementing xref on types +* Remove debug print statements +* Implements webpath and namespace detection +* The service directive now handle a namespace +* Now auto document the function parameters +* Document a bit the sphinx extension +* The sphinx ext is now able to basicaly autodocument the functions (just retrivieve the docstrings for now +* load wsme/release.py in a python 3 friendly way +* Now add data samples for the wanted protocols on the data types +* Introduce a Protocol base class, and add a method to render a sample data (used by the documentation tool) +* Use the agogo theme options instead of overloading the css +* Re-put the agogo theme (sphinxdoc was just a test +* Start working on the sphinx extension +* Add the release dates +* Added tag 0.3b2 for changeset d5eab01bf491 + +0.3b2 +----- + +* Prepare the next release +* TG1 server.webpath mechanics makes it impossible to use a filter on the controller itself. So we put it on the root controller and carrefully change the WSRoot webpath so that everything works properly +* BugFix: if no Content-Type header is present, read_arguments would fail +* Completing the changelog +* Better handling of errors on protocol selection +* Don't stop if the body is application/x-www-form-urlencoded encoded and the request has params (for other encoding it would mean both params and a body were provided) +* Add a cherrypy filter in the tg1 adapter so that the body is not parsed by cherrypy. This makes Webob happier when reading itself the body +* Fix response status code transmission in the TG1 adapter +* :class:`wsattr` now takes a 'default' parameter +* Fix nested dict/list structures +* Change version number +* Update the documentation +* handle dict and UserType as input from forms +* Added tag 0.3b1 for changeset ebe2c6f228ad + +0.3b1 +----- + +* Avoid using a weakref.proxy for CallContext.request because we need to get a real ref to the request at some point in the tests +* Test the 'division by zero' message in a smarter way so it adapts itself to the python version +* Fix the test_enum test (the error message now contains the attribute name +* Rename WSRoot.transaction to WSRoot._transaction to avoid scan_api failure in some cases +* Add a per-call transaction management +* Remove an empty file +* Make the int and long types validation interchangeable +* Fix dictionnary values validation +* Fix: the dict attributes were not registered correctly +* Now supports dictionnaries in addition to arrays +* Mention the doc in the changes +* Document a bit +* Preparing next release +* The restjson return values are not nested in the result attribute of an object. The former behavior can be obtained with the nest_result protocol option +* Code cleaning (thanks to flake8) +* Add a setperson function to test complex function arguments +* Better handling of function arguments as params (POST or GET) +* Don't output Unset values +* Fix the named attributes dump in restxml tests +* Fix the test_setnamedattrsobj test +* Code cleaning +* Implement and test the named attributes in the rest protocols +* Add a 'name' attribute on wsattr and wsproperty +* Unset now evaluate as False when converted to bool +* Add the ShinningPanda jenkins URL +* Fix user types, str and None values encoding/decoding +* Fix registering of class inheriting from an already registerd class +* Reproduce a bug when a Child complex type is registered after its Parent (it's not actually) +* Fix Unset values validation +* Add 'Unset' to the wsme module +* Fix parse_[date][time] that were using the Invalid exception from formencode +* Fix array attributes validation +* updated the todo list +* Added tag 0.2.0 for changeset cfb5efc624f5 + +0.2.0 +----- + +* Time for a 0.2.0 +* Documented REST+XML and a bit SOAP +* Document REST+Json +* More documentation for the next release +* Enum now takes values as args instead of a list +* binary is now a UserType, which simplify most of the protocols implementation +* Now supports user types (non-complex types that are base on native types), the first on being Enum +* Documented the ExtDirect protocol +* Completed the change list +* Add type validation on complex type attributes assignment +* pep8 now likes wsme +* Split the controller module into api, protocols and root +* Improved the complex type handling by using python Descriptors for attributes. They also carry the attribute name, so that the _wsme_attributes is now a list of wsattr/wsproperty instead of a list of (name, wsattr/wsproperty) +* Bugfix: a complex type used only in validate and never in expose was not registered +* The decorators now wrap the exposed function so that children classes can expose the parent functions with different signatures +* Fix DummyProtocol +* Fix: Complex types normal properties were not ignored +* Completed the change list +* Completed the change list. version -> 0.2.0 +* Protocols can now implement batch-calls +* Add a function to retrieve an attribute definition of a complex type +* Fix self reference complex type registration +* Add a pyramid integration example +* Add a link to the documentation +* Move the links to the README file +* Move the titles to the README file +* Now read the long_description and the doc introduction from the README file +* Fix inspection of classes with inheritance +* The complex types attribute are now reset to 'Unset' when inspected +* Add the authentication question to the todo list +* Update to todo list +* The ziphtml target now zip the doc in _build instead of _build/html +* Added tag 0.1.1 for changeset c17de432c185 + +0.1.1 +----- + +* Prepare the 0.1.1 release +* Include the soap client example in the documentation +* Completed the soap client example now that wsme-soap seems to work +* Add a dependency on WSME-Soap +* Add misc test functions +* Test nested controllers +* The function path now contains the function name +* Remove the path attribute of FunctionDefinition, since a same FunctionDefinition could appears at different paths in an api tree +* Rename list_calls to iter_calls and makes it an iterator +* Prepare next release (should be soon) +* Fix the little demo +* Use addprotocol to that the protocol tests can pass options at instanciation +* We now have a CallContext that follows a function execution from the path extraction to the result encoding +* Fix the mandatory/default argument properties detection, and make the content-type detection more permissive +* array attributes were potentially not registered +* Rectify the integration examples +* More test coverage + code cleaning +* Fix a test +* add WSRoot members doc +* Update change list +* Added tag 0.1.0 for changeset b0019e486c80 + +0.1.0 +----- + +* Going 0.1.0 now that at least 1 application is runnging wsme. We are still in alpha state though +* Protocols are now stored in a list so that the order is considered when selecting the right protocol for a request +* expose now takes extra options that can be used by the protocols +* getprotocol now takes the options as named arguments, which make it easier to use from outside +* Remove the soap protocol (I am moving it to wsme-soap +* Fix doc +* WSRoot.addprotocol now takes keywords parameters and transmit them to getprotocol for instanciating the protocol +* Update the change list and clean the demo +* Added tag 0.1.0a4 for changeset b38c56a2b913 + +0.1.0a4 +------- + +* Update the doc and the version number +* Protocols are now found through entry points +* 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 +* Add the todo page to the index +* Completing the changes list for 0.1.0a3 +* Add a little 'Install' section +* Add a ziphtml target for preparing the documentation to be uploaded to pypi +* Added tag 0.1.0a3 for changeset 86466da44f44 + +0.1.0a3 +------- + +* Add links to the source code and issue tracker +* Make the feature-list clearer and add the mailing list address +* Update the contact mail and version +* Documented most of the api +* Improve code coverage of restjson +* Improve ClientSideError interface + raise wsme.exc coverage to 100% +* wswe.controller code coverage is back to 100% +* More code coverage, and fixed a bug with wsme.wsgi.WSRoot.clone +* More documentation +* Add a WSGI adapter +* Added an adapter for TG1 +* Added tag 0.1.0a2 for changeset 0eae00db9384 + +0.1.0a2 +------- + +* Prepare a new release +* Cleaning my test +* Add arrays support in soap +* Add array support to restxml +* Add array support to restjson +* Added tag 0.1.0a1 for changeset 2bd203a084dc + +0.1.0a1 +------- + +* Going 0.1.0a1 +* Fix README filename +* Add a reame file +* Completed the packaging, we should now be able to do an alpha release +* Documenting +* Add a setup.cfg +* Completing setup.py +* Initial documentation +* Move the protocols to a dedicated module, and their activation more explicit +* completed the setup informations +* Made the demo work with suds +* Add a complex type to the demo +* Add a small soap client for the demo app, so that we can see the wsdl generation is _not_ working properly... to be continued +* Adapt the rest xml tests +* Adapted the restjson tests to the changes I made for the soap tests +* The soap test case now fully pass. We are getting closer to a working implementation, but I think I messed up the namespaces +* Start implementing a 'fromsoap' set of functions +* A few more fixes on tests and return values in soap before diving into arguments feeding +* Missing arguments are now detected by the controller +* Fix a few tests +* A few utility functions +* Most of the return types now works with soap +* Making progress on the soap implementation. Will have to choose between Genshi and ElementTree though, it is getting anoying to juggle with both +* Adapt the rest protocol implementation to the changes I did for the soap protocol +* Working on the soap protocol +* Fix a few dummy errors +* Worked on the wsdl generation. It looks like it should although it lacks the basic types handling and arrays definitions +* Embedded the wsdl genshi template from tgws + start testing the soap protocol +* Start working on the soap protocol +* PEP8fying +* Move as much as possible the request handling code out of the protocol +* One can now test the rest api with a web browser +* Share more code between restjson and restxml +* Fix some issues with encoding +* Add a small demo, and fix a few problems +* PEP8 compliance +* Most of the types now works as argument and return type in rest+xml and rest+json +* PEP8 compliance +* Handle binary and decimal return types +* Use generic to prepare the json output so that non-structured custom types can be added +* rest+xml now handle the basic return types +* Add unittests for rest+xml +* Continue working on the rest-xml tests and implementation + changed the RestProtocol interface +* Start working on the rest-xml tests and implementation +* better testing (+ fixes) of sort_attributes +* test & fix the forced attribute order feature +* pep8 +* Structured types basically work with rest+json +* Rename AttrDef to wsattr, and introduce wsproperty +* Implementing the structured types inspection +* Start working on the non-trivial types handling +* rename WSRoot.debug to WSRoot._debug to avoid an infinite recursion when scanning the api (which will need a better implementation anyway) +* Continuing the rest+json implementation +* the rest+json protocol starts to work with basic return types +* Lowercased the project name +* Start implementing the rest json proto +* Completed the register test so we have 100% of test coverage +* Renamed EWS to WSME because ews is already taken on pypi +* A first working implemetation for the core controller code diff -Nru python-wsme-0.5b1/debian/changelog python-wsme-0.6/debian/changelog --- python-wsme-0.5b1/debian/changelog 2013-03-14 16:07:51.000000000 +0000 +++ python-wsme-0.6/debian/changelog 2014-03-17 04:05:08.000000000 +0000 @@ -1,12 +1,116 @@ -python-wsme (0.5b1-0ubuntu1~cloud0) precise-grizzly; urgency=low +python-wsme (0.6-0ubuntu1~cloud0) precise-icehouse; urgency=low - * New package for the Ubuntu Cloud Archive to support inclusion of - ceilometer. + * New upstream release for the Ubuntu Cloud Archive. - -- James Page Thu, 14 Mar 2013 16:07:51 +0000 + -- Openstack Ubuntu Testing Bot Mon, 17 Mar 2014 00:05:08 -0400 -python-wsme (0.5b1-0ubuntu1) raring; urgency=low +python-wsme (0.6-0ubuntu1) trusty; urgency=medium - * Initial release. + * New upstream release (LP: #1292579): + - d/control: Add BD on python{3}-ipaddr. + - d/p/*: Drop all patches, accepted upstream. - -- Yolanda Robla Fri, 01 Feb 2013 10:31:00 +0100 + -- James Page Fri, 14 Mar 2014 16:24:51 +0000 + +python-wsme (0.5b6-0ubuntu3) trusty; urgency=medium + + * Rebuild to drop files installed into /usr/share/pyshared. + + -- Matthias Klose Sun, 23 Feb 2014 13:53:34 +0000 + +python-wsme (0.5b6-0ubuntu2) trusty; urgency=low + + * debian/control: Add versioned dependency for python-six. + (LP: #1259203) + + -- Chuck Short Mon, 09 Dec 2013 10:18:39 -0500 + +python-wsme (0.5b6-0ubuntu1) trusty; urgency=low + + * New upstream release. + + -- Chuck Short Thu, 24 Oct 2013 11:20:57 -0700 + +python-wsme (0.5b5-1ubuntu2) saucy; urgency=low + + * d/p/fix_403_test_case.patch: Fixup test case for 403 error code; + older versions of python-werkzeug handle this differently which + creates issues in the Ubuntu Cloud Archive (LP: #1234291). + + -- James Page Wed, 02 Oct 2013 17:51:52 +0100 + +python-wsme (0.5b5-1ubuntu1) saucy; urgency=low + + * debian/control: Add python-flask as a dependency. + * debian/rules: Run the flasks tests specifically. + + -- Chuck Short Mon, 23 Sep 2013 11:18:40 -0400 + +python-wsme (0.5b5-1ubuntu0) saucy; urgency=low + + * New upstream version. + * debian/control: Add build-depends for python-pbr. + + -- Chuck Short Thu, 19 Sep 2013 08:54:00 -0400 + +python-wsme (0.5b2-2ubuntu5) saucy; urgency=low + + * Build depend on python{3}-webob, python{3}-six, and python{3}-simplegeneric + to fix FTBFS due to the build system attempting to download them from pypi + in order to run the test suite (LP: #1208203). + + -- Andrew Starr-Bochicchio Sun, 04 Aug 2013 12:19:58 -0400 + +python-wsme (0.5b2-2ubuntu4) saucy; urgency=low + + * debian/control: Add python-nose, python3-nose, + python3-cherrypy, and python-cherrypy as build + dependencies. + * debian/rules: Enable testsuite. + + -- Chuck Short Wed, 31 Jul 2013 13:47:35 -0400 + +python-wsme (0.5b2-2ubuntu3) saucy; urgency=low + + * debian/control: Add python-six and python-webob + + -- Chuck Short Wed, 19 Jun 2013 17:16:09 -0500 + +python-wsme (0.5b2-2ubuntu2) saucy; urgency=low + + * debian/control, debian/rules: Build for python2/python3. + + -- Chuck Short Wed, 19 Jun 2013 15:00:05 -0500 + +python-wsme (0.5b2-2ubuntu1) saucy; urgency=low + + * Merge from Debian unstable. Remaining changes: + - debian/control: Add python-d2to1 to Build-Depends to fix FTBFS without + Internet connection. + + -- Logan Rosen Mon, 20 May 2013 02:08:28 -0400 + +python-wsme (0.5b2-2) unstable; urgency=low + + * Uploading to unstable. + + -- Thomas Goirand Sun, 12 May 2013 12:15:15 +0000 + +python-wsme (0.5b2-1ubuntu1) saucy; urgency=low + + * debian/control: Add python-d2to1 to Build-Depends to fix FTBFS without + Internet connection. + + -- Logan Rosen Sat, 27 Apr 2013 12:07:21 -0400 + +python-wsme (0.5b2-1) experimental; urgency=low + + * New upstream release (Closes: #706238). + + -- Thomas Goirand Sat, 27 Apr 2013 13:06:20 +0800 + +python-wsme (0.5b1-1) experimental; urgency=low + + * Initial release (Closes: #701521). + + -- Thomas Goirand Tue, 09 Oct 2012 11:55:15 +0000 diff -Nru python-wsme-0.5b1/debian/control python-wsme-0.6/debian/control --- python-wsme-0.5b1/debian/control 2013-02-11 19:37:41.000000000 +0000 +++ python-wsme-0.6/debian/control 2014-03-14 16:02:55.000000000 +0000 @@ -2,15 +2,55 @@ Section: python Priority: optional Maintainer: Ubuntu Developers -Build-Depends: debhelper (>= 9), python-setuptools, python-all (>= 2.6.6-3~), - python-six, python-transaction, python-d2to1 +XSBC-Original-Maintainer: PKG OpenStack +Uploaders: Loic Dachary (OuoU) , + Julien Danjou , + Thomas Goirand , + Ghe Rivero , + Mehdi Abaakouk +Build-Depends: debhelper (>= 9), python-setuptools, python-all (>= 2.6.6-3~), python-d2to1, + python3-setuptools, python3-all, python3-d2to1, python3-nose, python-cherrypy3, python-webob, + python3-webob, python-six (>= 1.4.1), python3-six (>= 1.4.1), python-simplegeneric, python3-simplegeneric, python-flask, + python-pbr, python3-pbr, python-nose, python-ipaddr, python3-ipaddr Standards-Version: 3.9.4 -Homepage: http://pypi.python.org/pypi/WSME/0.5b1 +Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-wsme.git +Vcs-Git: git://anonscm.debian.org/openstack/python-wsme.git +Homepage: http://pythonhosted.org/WSME/ Package: python-wsme Architecture: all -Depends: ${python:Depends}, ${misc:Depends} -Description: Web Service Made Easy (WSME) - WSME simplifies the writing of REST web services by providing simple yet powerful - typing which removes the need to directly manipulate the request and the - response objects. +Pre-Depends: dpkg (>= 1.15.6~) +Depends: ${python:Depends}, ${misc:Depends}, python-six, python-webob +Description: Web Services Made Easy makes it easy to implement multi-protocol webservices + Web Service Made Easy (WSME) simplify the writing of REST web services by + providing simple yet powerful typing which removes 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 ones supporting batch-calls + * Easy documentation through a Sphinx extension + . + WSME is originally a rewrite of TGWebServices with focus on extensibility, + framework-independance and better type handling. + +Package: python3-wsme +Architecture: all +Pre-Depends: dpkg (>= 1.15.6~) +Depends: ${python3:Depends}, ${misc:Depends}, python3-six, python3-webob +Description: Web Services Made Easy makes it easy to implement multi-protocol webservices + Web Service Made Easy (WSME) simplify the writing of REST web services by + providing simple yet powerful typing which removes 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 ones supporting batch-calls + * Easy documentation through a Sphinx extension + . + WSME is originally a rewrite of TGWebServices with focus on extensibility, + framework-independance and better type handling. diff -Nru python-wsme-0.5b1/debian/copyright python-wsme-0.6/debian/copyright --- python-wsme-0.5b1/debian/copyright 2013-02-11 19:31:35.000000000 +0000 +++ python-wsme-0.6/debian/copyright 2014-03-14 15:55:57.000000000 +0000 @@ -1,9 +1,15 @@ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: wsme -Source: https://bitbucket.org/cdevienne/wsme +Upstream-Name: WSME +Source: https://pypi.python.org/pypi/WSME + +Files: debian/* +Copyright: (c) 2013, Thomas Goirand +License: MIT Files: * -Copyright: 2011 Christophe de Vienne +Copyright: Christophe de Vienne +License: 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 @@ -15,28 +21,10 @@ 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. - -Files: debian/* -Copyright: 2013 Canonical Ltd -License: GPL-2+ - This package is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - . - This package is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - . - You should have received a copy of the GNU General Public License - along with this program. If not, see - . - On Debian systems, the complete text of the GNU General - Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + 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. diff -Nru python-wsme-0.5b1/debian/gbp.conf python-wsme-0.6/debian/gbp.conf --- python-wsme-0.5b1/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/debian/gbp.conf 2014-03-14 15:55:57.000000000 +0000 @@ -0,0 +1,11 @@ +[DEFAULT] +upstream-branch = upstream-unstable +debian-branch = debian-unstable +pristine-tar = True + +[git-buildpackage] +export-dir = ../build-area/ +tarball-dir = ../tarballs/ + +[git-import-orig] +dch = False diff -Nru python-wsme-0.5b1/debian/rules python-wsme-0.6/debian/rules --- python-wsme-0.5b1/debian/rules 2013-02-11 19:36:19.000000000 +0000 +++ python-wsme-0.6/debian/rules 2014-03-14 15:55:57.000000000 +0000 @@ -2,8 +2,39 @@ #export DH_VERBOSE=1 +PYTHONS:=$(shell pyversions -vr) +PYTHON3S:=$(shell py3versions -vr) + %: - dh $@ --with python2 + dh $@ --with python2,python3 + +override_dh_auto_build: + set -e && for pyvers in $(PYTHONS); do \ + python$$pyvers setup.py build; \ + done + set -e && for pyvers in $(PYTHON3S); do \ + python$$pyvers setup.py build; \ + done + +override_dh_auto_install: + set -e && for pyvers in $(PYTHONS); do \ + python$$pyvers setup.py install --install-layout=deb \ + --root $(CURDIR)/debian/python-wsme; \ + done + set -e && for pyvers in $(PYTHON3S); do \ + python$$pyvers setup.py install --install-layout=deb \ + --root $(CURDIR)/debian/python3-wsme; \ + done + +override_dh_auto_test: + set -e && for pyvers in $(PYTHONS); do \ + nosetests tests/test_flask.py; \ + done + set -e && for pyvers in $(PYTHON3S); do \ + python$$pyvers setup.py test; \ + done + -get-orig-source: - uscan --verbose --force-download --rename --repack --download-current-version --destdir=../build-area +override_dh_clean: + dh_clean + rm -rf d2to1-*-py2.*.egg build diff -Nru python-wsme-0.5b1/debian/watch python-wsme-0.6/debian/watch --- python-wsme-0.5b1/debian/watch 2013-02-04 18:03:18.000000000 +0000 +++ python-wsme-0.6/debian/watch 2014-03-14 15:55:57.000000000 +0000 @@ -1,2 +1,2 @@ version=3 -http://pypi.python.org/packages/source/W/WSME/WSME-(.*).tar.gz +https://pypi.python.org/packages/source/W/WSME/WSME-(.*).tar.gz diff -Nru python-wsme-0.5b1/doc/api.rst python-wsme-0.6/doc/api.rst --- python-wsme-0.5b1/doc/api.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/api.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,45 @@ +API +=== + +Public API +---------- + +:mod:`wsme` -- Essentials +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. module:: wsme + +.. autoclass:: signature([return_type, [arg0_type, [arg1_type, ... ] ] ], body=None, status=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: + diff -Nru python-wsme-0.5b1/doc/changes.rst python-wsme-0.6/doc/changes.rst --- python-wsme-0.5b1/doc/changes.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/changes.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,361 @@ +Changes +======= + +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. diff -Nru python-wsme-0.5b1/doc/conf.py python-wsme-0.6/doc/conf.py --- python-wsme-0.5b1/doc/conf.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/conf.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,259 @@ +# -*- 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 +# " v 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 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') diff -Nru python-wsme-0.5b1/doc/document.rst python-wsme-0.6/doc/document.rst --- python-wsme-0.5b1/doc/document.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/document.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,186 @@ +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 you 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 + additionnal package (for example WSME-Soap or WSME-ExtDirect), it 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:: + + Allow to define the service root controller in one documentation source file. + To set it globally, see :confval:`wsme_root`. + + A ``webpath`` option allow to override :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 additionnal ``: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 ` + + +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: 42-67 + :language: python + +Documentation source +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: rst + + .. default-domain:: wsmeext + + .. 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/ diff -Nru python-wsme-0.5b1/doc/functions.rst python-wsme-0.6/doc/functions.rst --- python-wsme-0.5b1/doc/functions.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/functions.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,205 @@ +Functions +========= + +WSME is based on the idea that most of the time the input and output of web +services are actually stricly typed. It uses this fact to ease the +implementation of the actual functions by handling those input/output. +It also uses these informations to propose alternate protocols on top of a +proper REST api. + +This chapter explains in details how to 'sign' a function with WSME. + +The decorators +-------------- + +Depending on the framework you are using, you will have to use either a +@signature decorator, either a @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 @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 @wsexpose +decorator. + +It does what @signature does, *and* expose the function in the routing system +of the host framework. + +This decorator is generally used in object-dispatch routing context. + +.. note:: + + Since both decorators plays the same role function-wise, 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, and know 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 on which we POST datas. + +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 be like this: + +.. code-block:: javascript + + { + "data": + { + "id": 1, + "name": "Pierre-Joseph" + } + } + +If you think (and you should) that it has one extra nest level, 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 it does not prevent 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 code returned by WSME are 200, 400 (if the client send wrong +inputs) and 500 (for server-side errors). + +Since a proper Rest API should use different return codes (201, etc), one can +use the 'status=' option of @signature to do so. + +.. code-block:: python + + @signature(Author, body=Author, status=201) + def create_author(data): + # ... + return data + +Of course this code will only be used if no error occur. + +In case the function needs to change the status code on a per-request base, it +can return a :class:`wsme.Response` object, that allow to override the status +code: + +.. code-block:: python + + @signature(Author, body=Author, status=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 gives extra/unknown +arguments. In some (rare) cases, it can be unwanted. + +Adding 'ignore_extra_args=True' to @signature changes this behavior. + +.. note:: + + If using this option seems to solution to 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. diff -Nru python-wsme-0.5b1/doc/gettingstarted.rst python-wsme-0.6/doc/gettingstarted.rst --- python-wsme-0.5b1/doc/gettingstarted.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/gettingstarted.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,15 @@ +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 + diff -Nru python-wsme-0.5b1/doc/index.rst python-wsme-0.6/doc/index.rst --- python-wsme-0.5b1/doc/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/index.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,27 @@ + +.. 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` + diff -Nru python-wsme-0.5b1/doc/integrate.rst python-wsme-0.6/doc/integrate.rst --- python-wsme-0.5b1/doc/integrate.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/integrate.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,327 @@ +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 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 set 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 expects functions + taking a request object as a single parameter and returning a response + object. This is the case of :ref:`adapter-cornice` and + :ref:`adapter-flask`. + +Additionnaly, if you want to enable additionnal 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 assume 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). + +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, + ie 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 `_ + 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 `_ 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`. + + diff -Nru python-wsme-0.5b1/doc/make.bat python-wsme-0.6/doc/make.bat --- python-wsme-0.5b1/doc/make.bat 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/make.bat 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,155 @@ +@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 ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. 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 diff -Nru python-wsme-0.5b1/doc/Makefile python-wsme-0.6/doc/Makefile --- python-wsme-0.5b1/doc/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/Makefile 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,134 @@ +# 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 ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " 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." diff -Nru python-wsme-0.5b1/doc/protocols.rst python-wsme-0.6/doc/protocols.rst --- python-wsme-0.5b1/doc/protocols.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/protocols.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,367 @@ +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 also for the different adapters, not only 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. + +For example, the functions 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 (is 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 return 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). + +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 | +| | | +| | a string | ++---------------+----------------------------------------+ +| ``unicode`` | .. code-block:: xml | +| | | +| | a string | ++---------------+----------------------------------------+ +| ``int`` | .. code-block:: xml | +| | | +| | 5 | ++---------------+----------------------------------------+ +| ``float`` | .. code-block:: xml | +| | | +| | 3.14 | ++---------------+----------------------------------------+ +| ``bool`` | .. code-block:: xml | +| | | +| | true | ++---------------+----------------------------------------+ +| ``Decimal`` | .. code-block:: xml | +| | | +| | 5.46 | ++---------------+----------------------------------------+ +| ``date`` | .. code-block:: xml | +| | | +| | 2010-04-27 | ++---------------+----------------------------------------+ +| ``time`` | .. code-block:: xml | +| | | +| | 12:54:18 | ++---------------+----------------------------------------+ +| ``datetime`` | .. code-block:: xml | +| | | +| | 2010-04-27T12:54:18 | ++---------------+----------------------------------------+ +| Arrays | .. code-block:: xml | +| | | +| | | +| | Dinausaurs | +| | Rachel | +| | | ++---------------+----------------------------------------+ +| None | .. code-block:: xml | +| | | +| | | ++---------------+----------------------------------------+ +| Complex types | .. code-block:: xml | +| | | +| | | +| | 1 | +| | Ross | +| | | ++---------------+----------------------------------------+ + +Return +~~~~~~ + +A xml tree with a top 'result' element. + +.. code-block:: xml + + + 1 + Ross + Geller + + +Errors +~~~~~~ + +A xml tree with a top 'error' element, having 'faultcode', 'faultstring' +and 'debuginfo' subelements: + +.. code-block:: xml + + + Client + id is missing + + +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 match 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') + diff -Nru python-wsme-0.5b1/doc/requirements.txt python-wsme-0.6/doc/requirements.txt --- python-wsme-0.5b1/doc/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/requirements.txt 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,2 @@ +sphinx +cloud_sptheme diff -Nru python-wsme-0.5b1/doc/_static/toggle.css python-wsme-0.6/doc/_static/toggle.css --- python-wsme-0.5b1/doc/_static/toggle.css 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/_static/toggle.css 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,10 @@ +dl.toggle dt { + background-color: #eeffcc; + border: 1px solid #ac9; + display: inline; +} + +dl.toggle dd { + display: none; +} + diff -Nru python-wsme-0.5b1/doc/_static/toggle.js python-wsme-0.6/doc/_static/toggle.js --- python-wsme-0.5b1/doc/_static/toggle.js 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/_static/toggle.js 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,9 @@ +/*global $,document*/ +$(document).ready(function () { + "use strict"; + $("dl.toggle > dt").click( + function (event) { + $(this).next().toggle(250); + } + ); +}); diff -Nru python-wsme-0.5b1/doc/_static/wsme.css python-wsme-0.6/doc/_static/wsme.css --- python-wsme-0.5b1/doc/_static/wsme.css 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/_static/wsme.css 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,27 @@ +@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; +} + diff -Nru python-wsme-0.5b1/doc/todo.rst python-wsme-0.6/doc/todo.rst --- python-wsme-0.5b1/doc/todo.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/todo.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,31 @@ +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). diff -Nru python-wsme-0.5b1/doc/types.rst python-wsme-0.6/doc/types.rst --- python-wsme-0.5b1/doc/types.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/doc/types.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,246 @@ +Types +===== + +3 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 +the different protocols will map to theirs 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'] + + - Dictionnaries -- Statically typed mapping are allowed. When exposing + a dictionnary 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 to define new almost-native types. + +The idea is that you may have python data that should be transported as native +types by the different protocols, but needs conversion to/from this basetypes, +or needs to validate data integrity. + +To define a user type, you just have to inherit from +:class:`wsme.types.UserType` and instanciate 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 ` -- for transporting binary data as + base64 strings. +- :class:`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 ` +and :class:`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 + transfert 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 proposed, :class:`wsme.types.Base`, +but is not mandatory. The only thing it add 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 ` -- Allow to add more information about + the attribute, for example if it is mandatory. + - A :class:`wsproperty ` -- Special typed property. Works + like standard properties with additional properties like + :class:`wsattr `. + +Attributes having a leading '_' in there name will be ignored, as well as the +ones that are none of the above list. It means the type can have functions, +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 instanciable 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 reasons 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 instances attributes is + :data:`Unset `. + + :: + + class Person(object): + name = wsattr(unicode) + + p = Person() + assert p.name is Unset + + This allow 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 not, + sent by the caller. + + - When 2 complex types refers 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: + diff -Nru python-wsme-0.5b1/examples/demo/demo.py python-wsme-0.6/examples/demo/demo.py --- python-wsme-0.5b1/examples/demo/demo.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/examples/demo/demo.py 2014-02-06 14:49:22.000000000 +0000 @@ -49,7 +49,7 @@ @expose(unicode) def helloworld(self): - return u"こんにちは世界 (<- Hello World in Japanese !)" + return u"Здраво, свете (<- Hello World in Serbian !)" @expose(Person) def getperson(self): diff -Nru python-wsme-0.5b1/examples/demo/setup.py python-wsme-0.6/examples/demo/setup.py --- python-wsme-0.5b1/examples/demo/setup.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/examples/demo/setup.py 2014-02-06 14:49:22.000000000 +0000 @@ -3,7 +3,6 @@ setup(name='demo', install_requires=[ 'WSME', - 'WSME-Soap', 'Bottle', 'Pygments', ], diff -Nru python-wsme-0.5b1/examples/WSMECorniceDemo/setup.py python-wsme-0.6/examples/WSMECorniceDemo/setup.py --- python-wsme-0.5b1/examples/WSMECorniceDemo/setup.py 2012-10-22 10:29:33.000000000 +0000 +++ python-wsme-0.6/examples/WSMECorniceDemo/setup.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -""" Setup file. -""" -import os -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, 'README.rst')) as f: - README = f.read() - - -setup(name='WSMECorniceDemo', - version=0.1, - description='WSMECorniceDemo', - long_description=README, - classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application" - ], - keywords="web services", - author='', - author_email='', - url='', - packages=find_packages(), - include_package_data=True, - zip_safe=False, - install_requires=['cornice', 'PasteScript'], - entry_points = """\ - [paste.app_factory] - main = wsmecornicedemo:main - """, - paster_plugins=['pyramid'], -) diff -Nru python-wsme-0.5b1/examples/WSMECorniceDemo/wsmecornicedemo/__init__.py python-wsme-0.6/examples/WSMECorniceDemo/wsmecornicedemo/__init__.py --- python-wsme-0.5b1/examples/WSMECorniceDemo/wsmecornicedemo/__init__.py 2012-10-22 10:29:33.000000000 +0000 +++ python-wsme-0.6/examples/WSMECorniceDemo/wsmecornicedemo/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -"""Main entry point -""" -from pyramid.config import Configurator - - -def main(global_config, **settings): - config = Configurator(settings=settings) - config.include("cornice") - config.scan("wsmecornicedemo.views") - return config.make_wsgi_app() diff -Nru python-wsme-0.5b1/examples/WSMECorniceDemo/wsmecornicedemo/views.py python-wsme-0.6/examples/WSMECorniceDemo/wsmecornicedemo/views.py --- python-wsme-0.5b1/examples/WSMECorniceDemo/wsmecornicedemo/views.py 2012-10-22 10:30:13.000000000 +0000 +++ python-wsme-0.6/examples/WSMECorniceDemo/wsmecornicedemo/views.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -""" Cornice services. -""" -from cornice import Service - - -hello = Service(name='hello', path='/', description="Simplest app") - - -@hello.get() -def get_info(request): - """Returns Hello in JSON.""" - return {'Hello': 'World'} - - -@hello.post() -def set_info(request): - return {} diff -Nru python-wsme-0.5b1/.hgtags python-wsme-0.6/.hgtags --- python-wsme-0.5b1/.hgtags 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/.hgtags 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,14 @@ +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 diff -Nru python-wsme-0.5b1/MANIFEST.in python-wsme-0.6/MANIFEST.in --- python-wsme-0.5b1/MANIFEST.in 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/MANIFEST.in 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -include README.rst -include LICENSE -recursive-include examples *.py *.cfg diff -Nru python-wsme-0.5b1/PKG-INFO python-wsme-0.6/PKG-INFO --- python-wsme-0.5b1/PKG-INFO 2013-01-30 17:21:45.000000000 +0000 +++ python-wsme-0.6/PKG-INFO 2014-02-06 14:49:32.000000000 +0000 @@ -1,8 +1,7 @@ Metadata-Version: 1.1 Name: WSME -Version: 0.5b1 -Summary: Web Services Made Easy makes it easy to -implement multi-protocol webservices. +Version: 0.6 +Summary: Simplify the writing of REST APIs, and extend them with additional protocols. Home-page: UNKNOWN Author: "Christophe de Vienne" Author-email: "python-wsme@googlegroups.com" @@ -18,7 +17,7 @@ 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 prefered way of routing your REST + (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 ones supporting batch-calls @@ -32,11 +31,11 @@ Here is a standalone wsgi example:: - from wsme import WSRoot, expose, validate + from wsme import WSRoot, expose class MyService(WSRoot): - @expose(unicode) - @validate(unicode) + @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) @@ -69,9 +68,9 @@ - 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 independance : adapters are provided to easily integrate + - Framework independence : adapters are provided to easily integrate your API in any web framework, for example a wsgi container, - Pecan_, TurboGears_, cornice_... + 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 @@ -79,6 +78,7 @@ .. _Pecan: http://pecanpy.org/ .. _TurboGears: http://www.turbogears.org/ + .. _Flask: http://flask.pocoo.org/ .. _cornice: http://pypi.python.org/pypi/cornice Install @@ -86,6 +86,12 @@ :: + pip install WSME + + or, if you do not have pip on your system or virtualenv + + :: + easy_install WSME Changes @@ -103,13 +109,13 @@ ~~~~~~~~~~ :Report issues: `WSME issue tracker`_ - :Source code: hg clone https://bitbucket.org/cdevienne/wsme/ - :Jenkins: https://jenkins.shiningpanda.com/wsme/ + :Source code: git clone https://github.com/stackforge/wsme/ + :Gerrit: https://review.openstack.org/#/q/project:stackforge/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://bitbucket.org/cdevienne/wsme/issues?status=new&status=open + .. _WSME issue tracker: https://bugs.launchpad.net/wsme/+bugs .. _Sphinx: http://sphinx.pocoo.org/ @@ -117,10 +123,10 @@ Classifier: Development Status :: 3 - Alpha Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: MIT License diff -Nru python-wsme-0.5b1/README.rst python-wsme-0.6/README.rst --- python-wsme-0.5b1/README.rst 2013-01-30 16:49:34.000000000 +0000 +++ python-wsme-0.6/README.rst 2014-02-06 14:49:22.000000000 +0000 @@ -9,7 +9,7 @@ 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 prefered way of routing your REST +(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 ones supporting batch-calls @@ -23,11 +23,11 @@ Here is a standalone wsgi example:: - from wsme import WSRoot, expose, validate + from wsme import WSRoot, expose class MyService(WSRoot): - @expose(unicode) - @validate(unicode) + @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) @@ -60,9 +60,9 @@ - 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 independance : adapters are provided to easily integrate +- Framework independence : adapters are provided to easily integrate your API in any web framework, for example a wsgi container, - Pecan_, TurboGears_, cornice_... + 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 @@ -70,6 +70,7 @@ .. _Pecan: http://pecanpy.org/ .. _TurboGears: http://www.turbogears.org/ +.. _Flask: http://flask.pocoo.org/ .. _cornice: http://pypi.python.org/pypi/cornice Install @@ -77,6 +78,12 @@ :: + pip install WSME + +or, if you do not have pip on your system or virtualenv + +:: + easy_install WSME Changes @@ -94,11 +101,11 @@ ~~~~~~~~~~ :Report issues: `WSME issue tracker`_ -:Source code: hg clone https://bitbucket.org/cdevienne/wsme/ -:Jenkins: https://jenkins.shiningpanda.com/wsme/ +:Source code: git clone https://github.com/stackforge/wsme/ +:Gerrit: https://review.openstack.org/#/q/project:stackforge/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://bitbucket.org/cdevienne/wsme/issues?status=new&status=open +.. _WSME issue tracker: https://bugs.launchpad.net/wsme/+bugs .. _Sphinx: http://sphinx.pocoo.org/ diff -Nru python-wsme-0.5b1/setup.cfg python-wsme-0.6/setup.cfg --- python-wsme-0.5b1/setup.cfg 2013-01-30 17:21:45.000000000 +0000 +++ python-wsme-0.6/setup.cfg 2014-02-06 14:49:32.000000000 +0000 @@ -1,21 +1,19 @@ [metadata] name = WSME -version = 0.5b1 author = "Christophe de Vienne" author-email = "python-wsme@googlegroups.com" -summary = Web Services Made Easy makes it easy to - implement multi-protocol webservices. +summary = Simplify the writing of REST APIs, and extend them with additional protocols. description-file = README.rst -url = http://bitbucket.org/cdevienne/wsme +url = https://github.com/stackforge/wsme license = MIT classifier = Development Status :: 3 - Alpha Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2.5 Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.2 + Programming Language :: Python :: 3.3 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy License :: OSI Approved :: MIT License @@ -33,8 +31,6 @@ [files] packages = wsme - wsme.rest - wsme.tests wsmeext namespace_packages = wsmeext @@ -42,13 +38,11 @@ setup.py README.rst +[wheel] +universal = 1 + [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 -[aliases] -release = egg_info -RDb '' - -[nosetests] - diff -Nru python-wsme-0.5b1/setup.py python-wsme-0.6/setup.py --- python-wsme-0.5b1/setup.py 2013-01-17 15:12:02.000000000 +0000 +++ python-wsme-0.6/setup.py 2014-02-06 14:49:22.000000000 +0000 @@ -8,12 +8,20 @@ else: webob_version = '' +install_requires = [ + 'six', + 'simplegeneric', + 'WebOb' + webob_version +] + +if sys.version_info[:2] <= (2, 6): + install_requires += ('ordereddict',) + +if sys.version_info[:2] < (3, 3): + install_requires += ('ipaddr',) + setup( - setup_requires=['d2to1'], - install_requires=[ - 'six', - 'simplegeneric', - 'WebOb' + webob_version - ], - d2to1=True + setup_requires=['pbr>=0.5.21'], + install_requires=install_requires, + pbr=True ) diff -Nru python-wsme-0.5b1/tests/pecantest/setup.cfg python-wsme-0.6/tests/pecantest/setup.cfg --- python-wsme-0.5b1/tests/pecantest/setup.cfg 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/setup.cfg 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,6 @@ +[nosetests] +match=^test +where=test +nocapture=1 +cover-package=test +cover-erase=1 diff -Nru python-wsme-0.5b1/tests/pecantest/setup.py python-wsme-0.6/tests/pecantest/setup.py --- python-wsme-0.5b1/tests/pecantest/setup.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/setup.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,22 @@ +# -*- 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']) +) diff -Nru python-wsme-0.5b1/tests/pecantest/test/app.py python-wsme-0.6/tests/pecantest/test/app.py --- python-wsme-0.5b1/tests/pecantest/test/app.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/test/app.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,15 @@ +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) + ) diff -Nru python-wsme-0.5b1/tests/pecantest/test/controllers/root.py python-wsme-0.6/tests/pecantest/test/controllers/root.py --- python-wsme-0.5b1/tests/pecantest/test/controllers/root.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/test/controllers/root.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,21 @@ +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 diff -Nru python-wsme-0.5b1/tests/pecantest/test/controllers/ws.py python-wsme-0.6/tests/pecantest/test/controllers/ws.py --- python-wsme-0.5b1/tests/pecantest/test/controllers/ws.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/test/controllers/ws.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,131 @@ +# 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): + + 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) + 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 diff -Nru python-wsme-0.5b1/tests/pecantest/test/model/__init__.py python-wsme-0.6/tests/pecantest/test/model/__init__.py --- python-wsme-0.5b1/tests/pecantest/test/model/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/test/model/__init__.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,2 @@ +def init_model(): + pass diff -Nru python-wsme-0.5b1/tests/pecantest/test/tests/config.py python-wsme-0.6/tests/pecantest/test/tests/config.py --- python-wsme-0.5b1/tests/pecantest/test/tests/config.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/test/tests/config.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,24 @@ +# 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 diff -Nru python-wsme-0.5b1/tests/pecantest/test/tests/__init__.py python-wsme-0.6/tests/pecantest/test/tests/__init__.py --- python-wsme-0.5b1/tests/pecantest/test/tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/test/tests/__init__.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,22 @@ +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) diff -Nru python-wsme-0.5b1/tests/pecantest/test/tests/test_ws.py python-wsme-0.6/tests/pecantest/test/tests/test_ws.py --- python-wsme-0.5b1/tests/pecantest/test/tests/test_ws.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/pecantest/test/tests/test_ws.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,212 @@ +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 '1' in body + assert 'aname' 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") + + 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 'Client' 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 'Client' 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 'Server' 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 'Client' 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_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_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'] diff -Nru python-wsme-0.5b1/tests/sphinxexample/conf.py python-wsme-0.6/tests/sphinxexample/conf.py --- python-wsme-0.5b1/tests/sphinxexample/conf.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/sphinxexample/conf.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,232 @@ +# -*- 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 +# " v 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 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' +] diff -Nru python-wsme-0.5b1/tests/sphinxexample/document.rst python-wsme-0.6/tests/sphinxexample/document.rst --- python-wsme-0.5b1/tests/sphinxexample/document.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/sphinxexample/document.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,43 @@ +API Documentation test +====================== + +Example +~~~~~~~ + +.. wsme:root:: wsme.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 ` + + +.. default-domain:: wsme + +.. type:: int + + An integer + +.. autotype:: wsme.sphinxext.SampleType + :members: + +.. autoservice:: wsme.sphinxext.SampleService + :members: + + +.. autotype:: test_sphinxext.ASampleType + :members: + +.. autotype:: wsme.types.bytes + +.. autotype:: wsme.types.text + +.. _Sphinx: http://sphinx.pocoo.org/ diff -Nru python-wsme-0.5b1/tests/sphinxexample/index.rst python-wsme-0.6/tests/sphinxexample/index.rst --- python-wsme-0.5b1/tests/sphinxexample/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/sphinxexample/index.rst 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,3 @@ +.. toctree:: + + document diff -Nru python-wsme-0.5b1/tests/test_cornice.py python-wsme-0.6/tests/test_cornice.py --- python-wsme-0.5b1/tests/test_cornice.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/test_cornice.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,185 @@ +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.assertEquals( + resp.body, + '[{"id": 1, "name": "first"}]' + ) + + def test_get_xml_list(self): + resp = self.app.get('/users', headers={"Accept": "text/xml"}) + self.assertEquals( + resp.body, + '1first' + ) + + def test_post_json_data(self): + data = json.dumps({"name": "new"}) + resp = self.app.post( + '/users', data, + headers={"Content-Type": "application/json"} + ) + self.assertEquals( + resp.body, + '{"id": 2, "name": "new"}' + ) + + def test_post_xml_data(self): + data = 'new' + resp = self.app.post( + '/users', data, + headers={"Content-Type": "text/xml"} + ) + self.assertEquals( + resp.body, + '2new' + ) + + 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.assertEquals(resp.json['faultcode'], 'Server') + self.assertEquals(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 + ) + print resp.body + self.assertEquals(resp.json['faultcode'], 'Client') + self.assertEquals(resp.status_code, 400) + + def test_runtime_error(self): + resp = self.app.get( + '/secret', + headers={'Accept': 'application/json'}, + expect_errors=True + ) + print resp.body + self.assertEquals(resp.json['faultcode'], 'Client') + self.assertEquals(resp.status_code, 401) diff -Nru python-wsme-0.5b1/tests/test_flask.py python-wsme-0.6/tests/test_flask.py --- python-wsme-0.5b1/tests/test_flask.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/test_flask.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,172 @@ +import unittest +from flask import Flask, json, abort +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__) + + +@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/') +@signature(Model, text) +def get_model(name): + return Model(name=name) + + +@test_app.route('/models//secret') +@signature(Model, text) +def model_secret(name): + abort(403) + + +@test_app.route('/models//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 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 == '10' + + 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 + print resp.data + self.assertEquals( + resp.data, '[{"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 == ('Client' + '403: Forbidden' + '') + + 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 == ('Client' + 'FOO!' + '') + + def test_serversideerror(self): + r = self.app.get('/divide_by_zero') + assert r.status_code == 500 + self.assertEquals( + r.data, + '{"debuginfo": null, "faultcode": "Server", "faultstring": ' + '"integer division or modulo by zero"}' + ) + +if __name__ == '__main__': + test_app.run() diff -Nru python-wsme-0.5b1/tests/test_sphinxext.py python-wsme-0.6/tests/test_sphinxext.py --- python-wsme-0.5b1/tests/test_sphinxext.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/test_sphinxext.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,45 @@ +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/') + assert sphinx.main(['', + '-b', 'html', + '-d', '.test_sphinxext/doctree', + docpath, + '.test_sphinxext/html']) == 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)') diff -Nru python-wsme-0.5b1/tests/test_tg15.py python-wsme-0.6/tests/test_tg15.py --- python-wsme-0.5b1/tests/test_tg15.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/test_tg15.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,177 @@ +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 == "50" + + 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 == ("Client" + "(400, 'Cannot divide by zero!')" + "") + + 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.assertEquals(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.assertEquals( + "application/json", + wsmeext.tg1.AutoJSONTemplate().get_content_type('dummy') + ) + self.assertEquals( + "text/xml", + wsmeext.tg1.AutoXMLTemplate().get_content_type('dummy') + ) diff -Nru python-wsme-0.5b1/tests/test_tg1.py python-wsme-0.6/tests/test_tg1.py --- python-wsme-0.5b1/tests/test_tg1.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tests/test_tg1.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,196 @@ +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 == "50" + + 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 == ("Server" + "(400, 'Cannot divide by zero!')" + "") + + 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.assertEquals(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.assertEquals( + "application/json", + wsmeext.tg1.AutoJSONTemplate().get_content_type('dummy') + ) + self.assertEquals( + "text/xml", + wsmeext.tg1.AutoXMLTemplate().get_content_type('dummy') + ) diff -Nru python-wsme-0.5b1/toxgen.py python-wsme-0.6/toxgen.py --- python-wsme-0.5b1/toxgen.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/toxgen.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,144 @@ +""" +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() diff -Nru python-wsme-0.5b1/tox.ini python-wsme-0.6/tox.ini --- python-wsme-0.5b1/tox.ini 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tox.ini 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,1382 @@ +[tox] +envlist = py26,py26-nolxml,py27,py27-nolxml,py32,py32-nolxml,pypy,tg11,tg15,cornice,coverage,py33,py33-nolxml,pecan-dev26,pecan-dev27,pecan-dev32,pecan-dev33,pep8 + +[common] +testtools = + nose + coverage + pbr + webtest +basedeps = + transaction + pecan + Sphinx + Flask + +[testenv] +setenv = + COVERAGE_FILE=.coverage.{envname} + +[testenv:tg11] +basepython = python2.6 +deps = + pbr + nose + webtest < 1.4.99 + coverage + simplejson + suds + lxml +commands = + {envbindir}/easy_install -i http://www.turbogears.org/1.1/downloads/current/index/ 'TurboGears<1.1.99' + {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml tests/test_tg1.py --verbose --with-coverage --cover-package wsme,wsmeext {posargs} + {envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py + +[testenv:tg15] +basepython = python2.6 +deps = + pbr + nose + webtest < 1.4.99 + coverage + simplejson + suds + lxml +commands = + {envbindir}/easy_install -i http://www.turbogears.org/1.5/downloads/current/index/ 'TurboGears<1.5.99' + {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml tests/test_tg15.py --verbose --with-coverage --cover-package wsme,wsmeext {posargs} + {envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py + +[testenv:tg21] +basepython = python2.6 +deps = + pbr + nose + coverage + simplejson +commands = + {envbindir}/easy_install -i http://www.turbogears.org/2.1/downloads/current/index/ 'TurboGears2<2.1.99' webtest + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml tests/test_tg20.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 + +[testenv:cornice] +basepython = python2.7 +usedevelop = True +deps = + pbr + nose + webtest + coverage + 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:pecan-dev-base] +deps = + {[common]testtools} + webtest + transaction + suds + https://github.com/stackforge/pecan/zipball/master + +[testenv:pecan-dev26] +basepython = python2.6 +deps = {[testenv:pecan-dev-base]deps} +commands = + {envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs} + +[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-dev32] +basepython = python3.2 +deps = {[testenv:pecan-dev-base]deps} +commands = + {envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs} + +[testenv:pecan-dev33] +basepython = python3.3 +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 +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 +changedir = + doc +commands = + make clean ziphtml + +[testenv:pep8] +deps = flake8 +commands = flake8 wsme wsmeext setup.py + +[testenv:venv] +commands = {posargs} +usedevelop = True +deps = + pbr + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.5.99 + lxml + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.5.99 + lxml + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.5.99 + lxml + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.5.99 + lxml + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.5.99 + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.5.99 + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.5.99 + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.6.99 + lxml + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.6.99 + lxml + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.6.99 + lxml + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.6.99 + lxml + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.6.99 + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.6.99 + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.6.99 + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + lxml + +[testenv:py26] +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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + lxml + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + lxml + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + lxml + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + simplejson + +[testenv:py26-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 +basepython = python2.6 +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds + SQLAlchemy<=0.7.99 + simplejson + +[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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + SQLAlchemy<=0.7.99 + simplejson +basepython = python2.7 + +[testenv:py32-sa6-lxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 + lxml +basepython = python3.2 + +[testenv:py32-sa6] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 + lxml +basepython = python3.2 + +[testenv:py32-sa6-nolxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 +basepython = python3.2 + +[testenv:py32-sa6-nolxml] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 +basepython = python3.2 + +[testenv:py32-sa7-lxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + lxml +basepython = python3.2 + +[testenv:py32] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + lxml +basepython = python3.2 + +[testenv:py32-sa7-nolxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 +basepython = python3.2 + +[testenv:py32-nolxml] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 +basepython = python3.2 + +[testenv:py33-sa5-lxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.5.99 + lxml +basepython = python3.3 + +[testenv:py33-sa5] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.5.99 + lxml +basepython = python3.3 + +[testenv:py33-sa5-lxml-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.5.99 + lxml + simplejson +basepython = python3.3 + +[testenv:py33-sa5-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.5.99 + lxml + simplejson +basepython = python3.3 + +[testenv:py33-sa5-nolxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.5.99 +basepython = python3.3 + +[testenv:py33-sa5-nolxml] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.5.99 +basepython = python3.3 + +[testenv:py33-sa5-nolxml-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.5.99 + simplejson +basepython = python3.3 + +[testenv:py33-sa6-lxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 + lxml +basepython = python3.3 + +[testenv:py33-sa6] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 + lxml +basepython = python3.3 + +[testenv:py33-sa6-lxml-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 + lxml + simplejson +basepython = python3.3 + +[testenv:py33-sa6-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 + lxml + simplejson +basepython = python3.3 + +[testenv:py33-sa6-nolxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 +basepython = python3.3 + +[testenv:py33-sa6-nolxml] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 +basepython = python3.3 + +[testenv:py33-sa6-nolxml-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.6.99 + simplejson +basepython = python3.3 + +[testenv:py33-sa7-lxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + lxml +basepython = python3.3 + +[testenv:py33] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + lxml +basepython = python3.3 + +[testenv:py33-sa7-lxml-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + lxml + simplejson +basepython = python3.3 + +[testenv:py33-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + lxml + simplejson +basepython = python3.3 + +[testenv:py33-sa7-nolxml-json] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 +basepython = python3.3 + +[testenv:py33-nolxml] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 +basepython = python3.3 + +[testenv:py33-sa7-nolxml-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + simplejson +basepython = python3.3 + +[testenv:py33-nolxml-simplejson] +commands = + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest tests/test_sphinxext.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} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + SQLAlchemy<=0.7.99 + simplejson +basepython = python3.3 + +[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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + SQLAlchemy<=0.7.99 + simplejson + diff -Nru python-wsme-0.5b1/tox-tmpl.ini python-wsme-0.6/tox-tmpl.ini --- python-wsme-0.5b1/tox-tmpl.ini 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/tox-tmpl.ini 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,223 @@ +# content of: tox.ini , put in same dir as setup.py +[tox] +envlist = py26,py26-nolxml,py27,py27-nolxml,py32,py32-nolxml,pypy,tg11,tg15,cornice,coverage,py33,py33-nolxml,pecan-dev26,pecan-dev27,pecan-dev32,pecan-dev33,pep8 + +[common] +testtools= + nose + coverage + pbr + webtest +basedeps= + transaction + pecan + Sphinx + Flask + +[axes] +python=py26,py27,py32,py33,pypy +sqlalchemy=sa5,sa6,sa7* +lxml=lxml*,nolxml +json=json*,simplejson + +[axis:python] +deps = + {[common]testtools} + {[common]basedeps} + suds + +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:py26] +deps = + {[common]testtools} + unittest2 + {[common]basedeps} + suds +basepython=python2.6 + +[axis:python:py27] +basepython=python2.7 + +[axis:python:py32] +basepython=python3.2 + +deps = + {[common]testtools} + {[common]basedeps} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + +commands= + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml wsme/tests wsmeext/tests tests/pecantest 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:py33] +basepython=python3.3 + +deps = + {[common]testtools} + {[common]basedeps} + https://bitbucket.org/bernh/suds-python-3-patches/downloads/suds_patched.zip + +commands= + {envbindir}/coverage run {envbindir}/nosetests --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:sqlalchemy:sa5] +deps= + SQLAlchemy<=0.5.99 + +constraints= + !python:py32 + + +[axis:sqlalchemy:sa6] +deps= + SQLAlchemy<=0.6.99 + +[axis:sqlalchemy:sa7] +deps= + SQLAlchemy<=0.7.99 + +[axis:json:simplejson] +deps= + simplejson + +constraints= + !python:py32 + +[axis:lxml:lxml] +deps= + lxml + +[testenv] +setenv= + COVERAGE_FILE=.coverage.{envname} + +[testenv:tg11] +basepython=python2.6 +deps= + pbr + nose + webtest < 1.4.99 + coverage + simplejson + suds + lxml +commands= + {envbindir}/easy_install -i http://www.turbogears.org/1.1/downloads/current/index/ 'TurboGears<1.1.99' + {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml tests/test_tg1.py --verbose --with-coverage --cover-package wsme,wsmeext {posargs} + {envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py + +[testenv:tg15] +basepython=python2.6 +deps= + pbr + nose + webtest < 1.4.99 + coverage + simplejson + suds + lxml +commands= + {envbindir}/easy_install -i http://www.turbogears.org/1.5/downloads/current/index/ 'TurboGears<1.5.99' + {envbindir}/nosetests --nologcapture --with-xunit --xunit-file nosetests-{envname}.xml tests/test_tg15.py --verbose --with-coverage --cover-package wsme,wsmeext {posargs} + {envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsme/rest/*.py wsmeext/*.py + +[testenv:tg21] +basepython=python2.6 +deps= + pbr + nose + coverage + simplejson +commands= + {envbindir}/easy_install -i http://www.turbogears.org/2.1/downloads/current/index/ 'TurboGears2<2.1.99' webtest + {envbindir}/coverage run {envbindir}/nosetests --with-xunit --xunit-file nosetests-{envname}.xml tests/test_tg20.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 + + +[testenv:cornice] +basepython=python2.7 +usedevelop=True +deps= + pbr + nose + webtest + coverage + 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:pecan-dev-base] +deps= + {[common]testtools} + webtest + transaction + suds + https://github.com/stackforge/pecan/zipball/master + +[testenv:pecan-dev26] +basepython=python2.6 +deps={[testenv:pecan-dev-base]deps} +commands= + {envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs} + +[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-dev32] +basepython=python3.2 +deps={[testenv:pecan-dev-base]deps} +commands= + {envbindir}/nosetests tests/pecantest --with-xunit --xunit-file nosetests-{envname}.xml --verbose {posargs} + +[testenv:pecan-dev33] +basepython=python3.3 +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 +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 + +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 diff -Nru python-wsme-0.5b1/wsme/api.py python-wsme-0.6/wsme/api.py --- python-wsme-0.5b1/wsme/api.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/api.py 2014-02-06 14:49:22.000000000 +0000 @@ -4,6 +4,9 @@ import logging import wsme.exc +import wsme.types + +from wsme import utils log = logging.getLogger(__name__) @@ -66,16 +69,24 @@ #: 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 - @classmethod - def get(cls, func): + @staticmethod + def get(func): """ Returns the :class:`FunctionDefinition` of a method. """ @@ -96,11 +107,14 @@ 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, **extra_options): + def set_options(self, body=None, ignore_extra_args=False, status_code=200, + **extra_options): self.body_type = body + self.status_code = status_code self.ignore_extra_args = ignore_extra_args self.extra_options = extra_options @@ -117,14 +131,38 @@ default = None if not mandatory: default = defaults[i - (len(args) - len(defaults))] - self.arguments.append(FunctionArgument(argname, datatype, - mandatory, default)) + 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: 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 = types[1:] if len(types) > 1 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 @@ -144,12 +182,32 @@ sig = signature +class Response(object): + """ + Object to hold the "response" from a view function + """ + def __init__(self, obj, status_code=None, error=None): + #: 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 + + def format_exception(excinfo, debug=False): """Extract informations that can be sent to the client.""" error = excinfo[1] - if isinstance(error, wsme.exc.ClientSideError): + 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 str(error) r = dict(faultcode="Client", - faultstring=error.faultstring) + faultstring=faultstring) log.warning("Client-side error: %s" % r['faultstring']) r['debuginfo'] = None return r diff -Nru python-wsme-0.5b1/wsme/exc.py python-wsme-0.6/wsme/exc.py --- python-wsme-0.5b1/wsme/exc.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/exc.py 2014-02-06 14:49:22.000000000 +0000 @@ -1,33 +1,41 @@ import six -from six.moves import builtins -if '_' not in builtins.__dict__: - builtins._ = lambda s: s +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): - return str(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 - self.msg = msg + 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) + "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 - self.msg = msg + super(MissingArgument, self).__init__(msg) @property def faultstring(self): @@ -38,7 +46,7 @@ class UnknownArgument(ClientSideError): def __init__(self, argname, msg=''): self.argname = argname - self.msg = msg + super(UnknownArgument, self).__init__(msg) @property def faultstring(self): @@ -49,6 +57,7 @@ class UnknownFunction(ClientSideError): def __init__(self, name): self.name = name + super(UnknownFunction, self).__init__() @property def faultstring(self): diff -Nru python-wsme-0.5b1/wsme/rest/args.py python-wsme-0.6/wsme/rest/args.py --- python-wsme-0.5b1/wsme/rest/args.py 2013-01-18 10:38:56.000000000 +0000 +++ python-wsme-0.6/wsme/rest/args.py 2014-02-06 14:49:22.000000000 +0000 @@ -4,11 +4,14 @@ from simplegeneric import generic -from wsme.exc import ClientSideError, UnknownArgument +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 @@ -67,8 +70,10 @@ if objfound: r = datatype() for attrdef in list_attributes(datatype): - value = from_params(attrdef.datatype, - params, '%s.%s' % (path, attrdef.key), hit_paths) + 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 @@ -81,11 +86,19 @@ @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 params.getall(path)] + for value in getall(params, path)] if iscomplex(datatype.item_type): attributes = set() @@ -99,11 +112,11 @@ for attrdef in list_attributes(datatype.item_type): attrpath = '%s.%s' % (path, attrdef.key) hit_paths.add(attrpath) - attrvalues = params.getall(attrpath) + attrvalues = getall(params, attrpath) if len(value) < len(attrvalues): value[-1:] = [ datatype.item_type() - for i in xrange(len(attrvalues) - len(value)) + for i in moves.range(len(attrvalues) - len(value)) ] for i, attrvalue in enumerate(attrvalues): setattr( @@ -155,10 +168,18 @@ def args_from_args(funcdef, args, kwargs): newargs = [] for argdef, arg in zip(funcdef.arguments[:len(args)], args): - newargs.append(from_param(argdef.datatype, arg)) + try: + newargs.append(from_param(argdef.datatype, arg)) + except Exception: + raise InvalidInput( + argdef.name, + arg, + "unable to convert to %s" % argdef.datatype.__name__) newkwargs = {} for argname, value in kwargs.items(): - newkwargs[argname] = from_param(funcdef.get_arg(argname), value) + newkwargs[argname] = from_param( + funcdef.get_arg(argname).datatype, value + ) return newargs, newkwargs @@ -229,14 +250,14 @@ return newargs, newkwargs -def get_args(funcdef, args, kwargs, params, body, mimetype): +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 arguements from params of body + by arguments from params of body """ # get the body from params if not given directly if not body and '__body__' in params: @@ -248,17 +269,25 @@ # 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_body) + (from_params, from_form_params, from_body) ) - return combine_args( + args, kwargs = combine_args( funcdef, (from_args, from_params_and_body), allow_override=True ) + wsme.runtime.check_arguments(funcdef, args, kwargs) + return args, kwargs diff -Nru python-wsme-0.5b1/wsme/rest/__init__.py python-wsme-0.6/wsme/rest/__init__.py --- python-wsme-0.5b1/wsme/rest/__init__.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/rest/__init__.py 2014-02-06 14:49:22.000000000 +0000 @@ -12,25 +12,25 @@ return self.signature(func) @classmethod - def with_method(self, method, *args, **kwargs): + def with_method(cls, method, *args, **kwargs): kwargs['method'] = method - return expose(*args, **kwargs) + return cls(*args, **kwargs) @classmethod def get(cls, *args, **kwargs): - return expose.with_method('GET', *args, **kwargs) + return cls.with_method('GET', *args, **kwargs) @classmethod def post(cls, *args, **kwargs): - return expose.with_method('POST', *args, **kwargs) + return cls.with_method('POST', *args, **kwargs) @classmethod def put(cls, *args, **kwargs): - return expose.with_method('PUT', *args, **kwargs) + return cls.with_method('PUT', *args, **kwargs) @classmethod def delete(cls, *args, **kwargs): - return expose.with_method('DELETE', *args, **kwargs) + return cls.with_method('DELETE', *args, **kwargs) class validate(object): diff -Nru python-wsme-0.5b1/wsme/rest/json.py python-wsme-0.6/wsme/rest/json.py --- python-wsme-0.5b1/wsme/rest/json.py 2013-01-18 10:38:56.000000000 +0000 +++ python-wsme-0.6/wsme/rest/json.py 2014-02-06 14:49:22.000000000 +0000 @@ -11,7 +11,9 @@ from wsme.types import Unset import wsme.types -from wsme.exc import UnknownArgument +import wsme.utils +from wsme.exc import UnknownArgument, InvalidInput + try: import simplejson as json @@ -43,6 +45,8 @@ 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): @@ -131,9 +135,15 @@ obj = datatype() for attrdef in wsme.types.list_attributes(datatype): if attrdef.name in value: - setattr(obj, attrdef.key, - fromjson(attrdef.datatype, value[attrdef.name])) - return obj + val_fromjson = fromjson(attrdef.datatype, value[attrdef.name]) + if getattr(attrdef, 'readonly', False): + raise InvalidInput(attrdef.name, val_fromjson, + "Cannot set read only field.") + setattr(obj, attrdef.key, val_fromjson) + elif attrdef.mandatory: + raise 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)) @@ -159,9 +169,10 @@ @fromjson.when_object(six.binary_type) def str_fromjson(datatype, value): - if value is None: - return None - return value.encode('utf8') + 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) @@ -182,21 +193,21 @@ def date_fromjson(datatype, value): if value is None: return None - return datetime.datetime.strptime(value, '%Y-%m-%d').date() + return wsme.utils.parse_isodate(value) @fromjson.when_object(datetime.time) def time_fromjson(datatype, value): if value is None: return None - return datetime.datetime.strptime(value, '%H:%M:%S').time() + return wsme.utils.parse_isotime(value) @fromjson.when_object(datetime.datetime) def datetime_fromjson(datatype, value): if value is None: return None - return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S') + return wsme.utils.parse_isodatetime(value) def parse(s, datatypes, bodyarg, encoding='utf8'): @@ -235,9 +246,8 @@ 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) + content = json.dumps(r, ensure_ascii=False, indent=4 if format else 0, + sort_keys=format) return ('javascript', content) @@ -245,9 +255,8 @@ 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) + content = json.dumps(kw, ensure_ascii=False, indent=4 if format else 0, + sort_keys=format) return ('javascript', content) @@ -255,7 +264,6 @@ r = tojson(datatype, value) #if self.nest_result: #r = {'result': r} - content = json.dumps(r, ensure_ascii=False, - indent=4 if format else 0, - sort_keys=format) + content = json.dumps(r, ensure_ascii=False, indent=4 if format else 0, + sort_keys=format) return ('javascript', content) diff -Nru python-wsme-0.5b1/wsme/rest/protocol.py python-wsme-0.6/wsme/rest/protocol.py --- python-wsme-0.5b1/wsme/rest/protocol.py 2013-01-18 10:38:56.000000000 +0000 +++ python-wsme-0.6/wsme/rest/protocol.py 2014-02-06 14:49:22.000000000 +0000 @@ -6,6 +6,7 @@ import wsme.rest import wsme.rest.args +import wsme.runtime log = logging.getLogger(__name__) @@ -120,7 +121,7 @@ (wsme.rest.args.args_from_params(funcdef, request.params), wsme.rest.args.args_from_body(funcdef, body, context.inmime)) ) - assert len(args) == 0 + wsme.runtime.check_arguments(funcdef, args, kwargs) return kwargs def encode_result(self, context, result): diff -Nru python-wsme-0.5b1/wsme/rest/xml.py python-wsme-0.6/wsme/rest/xml.py --- python-wsme-0.5b1/wsme/rest/xml.py 2013-01-30 16:59:49.000000000 +0000 +++ python-wsme-0.6/wsme/rest/xml.py 2014-02-06 14:49:22.000000000 +0000 @@ -9,7 +9,7 @@ from simplegeneric import generic import wsme.types -from wsme.exc import UnknownArgument +from wsme.exc import UnknownArgument, InvalidInput import re @@ -61,7 +61,7 @@ else: if wsme.types.isusertype(datatype): return toxml(datatype.basetype, - key, datatype.tobasetype(value)) + key, datatype.tobasetype(value)) elif wsme.types.iscomplex(datatype): for attrdef in datatype._wsme_attributes: attrvalue = getattr(value, attrdef.key) @@ -95,15 +95,21 @@ if element.get('nil', False): return None if wsme.types.isusertype(datatype): - return datatype.frombasetype( - fromxml(datatype.basetype, element)) + return datatype.frombasetype(fromxml(datatype.basetype, element)) if wsme.types.iscomplex(datatype): obj = datatype() - for attrdef in datatype._wsme_attributes: + for attrdef in wsme.types.list_attributes(datatype): sub = element.find(attrdef.name) if sub is not None: - setattr(obj, attrdef.key, fromxml(attrdef.datatype, sub)) - return obj + 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) @@ -214,26 +220,21 @@ def date_fromxml(datatype, element): if element.get('nil') == 'true': return None - return datetime.datetime.strptime(element.text, '%Y-%m-%d').date() + return wsme.utils.parse_isodate(element.text) @fromxml.when_object(datetime.time) def time_fromxml(datatype, element): if element.get('nil') == 'true': return None - m = time_re.match(element.text) - if m: - return datetime.time( - int(m.group('h')), - int(m.group('m')), - int(m.group('s'))) + 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 datetime.datetime.strptime(element.text, '%Y-%m-%dT%H:%M:%S') + return wsme.utils.parse_isodatetime(element.text) def parse(s, datatypes, bodyarg): @@ -243,7 +244,7 @@ tree = et.fromstring(s) if bodyarg: name = list(datatypes.keys())[0] - return fromxml(datatypes[name], tree) + return {name: fromxml(datatypes[name], tree)} else: kw = {} extra_args = [] diff -Nru python-wsme-0.5b1/wsme/root.py python-wsme-0.6/wsme/root.py --- python-wsme-0.5b1/wsme/root.py 2013-01-22 10:53:49.000000000 +0000 +++ python-wsme-0.6/wsme/root.py 2014-02-06 14:49:22.000000000 +0000 @@ -7,7 +7,7 @@ import webob -from wsme.exc import ClientSideError, MissingArgument, UnknownFunction +from wsme.exc import ClientSideError, UnknownFunction from wsme.protocol import getprotocol from wsme.rest import scan_api from wsme import spore @@ -79,7 +79,7 @@ __registry__ = wsme.types.registry def __init__(self, protocols=[], webpath='', transaction=None, - scan_api=scan_api): + scan_api=scan_api): self._debug = True self._webpath = webpath self.protocols = [] @@ -180,10 +180,6 @@ kw = protocol.read_arguments(context) args = list(args) - for arg in context.funcdef.arguments: - if arg.mandatory and arg.name not in kw: - raise MissingArgument(arg.name) - txn = self.begin() try: result = context.func(*args, **kw) @@ -196,8 +192,7 @@ # TODO make sure result type == a._wsme_definition.return_type return protocol.encode_result(context, result) - except Exception: - e = sys.exc_info()[1] + except Exception as e: infos = wsme.api.format_exception(sys.exc_info(), self._debug) if isinstance(e, ClientSideError): request.client_errorcount += 1 @@ -237,8 +232,7 @@ try: msg = None protocol = self._select_protocol(request) - except Exception: - e = sys.exc_info()[1] + except Exception as e: msg = ("Error while selecting protocol: %s" % str(e)) log.exception(msg) protocol = None @@ -246,8 +240,8 @@ if protocol is None: if msg is None: msg = ("None of the following protocols can handle this " - "request : %s" % ','.join( - (p.name for p in self.protocols))) + "request : %s" % ','.join(( + p.name for p in self.protocols))) res.status = 500 res.content_type = 'text/plain' res.text = u(msg) @@ -343,8 +337,7 @@ return html_body % dict( css=formatter.get_style_defs(), content=highlight(content, lexer, formatter).encode('utf8')) - except Exception: - e = sys.exc_info()[1] + except Exception as e: log.warning( "Could not pygment the content because of the following " "error :\n%s" % e) diff -Nru python-wsme-0.5b1/wsme/runtime.py python-wsme-0.6/wsme/runtime.py --- python-wsme-0.5b1/wsme/runtime.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsme/runtime.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,9 @@ +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) diff -Nru python-wsme-0.5b1/wsme/tests/protocol.py python-wsme-0.6/wsme/tests/protocol.py --- python-wsme-0.5b1/wsme/tests/protocol.py 2013-01-30 16:59:55.000000000 +0000 +++ python-wsme-0.6/wsme/tests/protocol.py 2014-02-06 14:49:22.000000000 +0000 @@ -4,7 +4,6 @@ import warnings import datetime import decimal -import sys import six from six import u, b @@ -22,8 +21,7 @@ try: 1 / 0 -except ZeroDivisionError: - e = sys.exc_info()[1] +except ZeroDivisionError as e: zerodivisionerrormsg = str(e) @@ -35,7 +33,8 @@ def __str__(self): return 'faultcode=%s, faultstring=%s, debuginfo=%s' % ( - self.faultcode, self.faultstring, self.debuginfo) + self.faultcode, self.faultstring, self.debuginfo + ) myenumtype = wsme.types.Enum(wsme.types.bytes, 'v1', 'v2') @@ -309,6 +308,30 @@ return value +class BodyTypes(object): + def assertEquals(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.assertEquals(type(body), dict) + self.assertEquals(type(body['test']), int) + self.assertEquals(body['test'], 10) + return body['test'] + + @expose(int, body=[int]) + @validate(int) + def setlist(self, body): + print(body) + self.assertEquals(type(body), list) + self.assertEquals(type(body[0]), int) + self.assertEquals(body[0], 10) + return body[0] + + class WithErrors(object): @expose() def divide_by_zero(self): @@ -325,6 +348,7 @@ class WSTestRoot(WSRoot): argtypes = ArgTypes() returntypes = ReturnTypes() + bodytypes = BodyTypes() witherrors = WithErrors() nested = NestedOuterApi() misc = MiscFunctions() @@ -378,20 +402,17 @@ res = self.call('invalid_function') print(res) assert "No error raised" - except CallException: - e = sys.exc_info()[1] + except CallException as e: self.assertEquals(e.faultcode, 'Client') self.assertEquals(e.faultstring.lower(), - u('unknown function name: invalid_function')) + 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: - e = sys.exc_info()[1] - print(e) + except CallException as e: self.assertEquals(e.faultcode, 'Server') self.assertEquals(e.faultstring, zerodivisionerrormsg) assert e.debuginfo is not None @@ -402,9 +423,7 @@ res = self.call('witherrors/divide_by_zero') print(res) assert "No error raised" - except CallException: - e = sys.exc_info()[1] - print(e) + except CallException as e: self.assertEquals(e.faultcode, 'Server') self.assertEquals(e.faultstring, zerodivisionerrormsg) assert e.debuginfo is None @@ -471,23 +490,21 @@ def test_return_nesteddict(self): r = self.call('returntypes/getnesteddict', - _rt={wsme.types.bytes: NestedOuter}) + _rt={wsme.types.bytes: NestedOuter}) self.assertEquals(r, { b('a'): {'inner': {'aint': 0}}, b('b'): {'inner': {'aint': 0}} }) def test_return_objectarrayattribute(self): - r = self.call('returntypes/getobjectarrayattribute', - _rt=NestedOuter) + r = self.call('returntypes/getobjectarrayattribute', _rt=NestedOuter) self.assertEquals(r, { 'inner': {'aint': 0}, 'inner_array': [{'aint': 12}, {'aint': 13}] }) def test_return_objectdictattribute(self): - r = self.call('returntypes/getobjectdictattribute', - _rt=NestedOuter) + r = self.call('returntypes/getobjectdictattribute', _rt=NestedOuter) self.assertEquals(r, { 'inner': {'aint': 0}, 'inner_dict': { @@ -506,15 +523,15 @@ def test_setbytes(self): assert self.call('argtypes/setbytes', value=b('astring'), - _rt=wsme.types.bytes) == 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') + _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('') + _rt=wsme.types.text) == u('') def test_settext_none(self): self.assertEquals( @@ -564,28 +581,37 @@ def test_setbinary(self): value = binarysample r = self.call('argtypes/setbinary', value=(value, wsme.types.binary), - _rt=wsme.types.binary) == value + _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) + value=(value, NestedOuter), + _rt=NestedOuter) + self.assertEquals(r, value) + + def test_setnested_nullobj(self): + value = {'inner': None} + r = self.call( + 'argtypes/setnested', + value=(value, NestedOuter), + _rt=NestedOuter + ) self.assertEquals(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]) + value=(value, [wsme.types.bytes]), + _rt=[wsme.types.bytes]) self.assertEquals(r, value) def test_settextarray(self): value = [u("1")] r = self.call('argtypes/settextarray', - value=(value, [wsme.types.text]), - _rt=[wsme.types.text]) + value=(value, [wsme.types.text]), + _rt=[wsme.types.text]) self.assertEquals(r, value) def test_setdatetimearray(self): @@ -594,8 +620,8 @@ datetime.datetime(2008, 4, 6, 2, 12, 15), ] r = self.call('argtypes/setdatetimearray', - value=(value, [datetime.datetime]), - _rt=[datetime.datetime]) + value=(value, [datetime.datetime]), + _rt=[datetime.datetime]) self.assertEquals(r, value) def test_setnestedarray(self): @@ -604,8 +630,8 @@ {'inner': {'aint': 55}}, ] r = self.call('argtypes/setnestedarray', - value=(value, [NestedOuter]), - _rt=[NestedOuter]) + value=(value, [NestedOuter]), + _rt=[NestedOuter]) self.assertEquals(r, value) def test_setnesteddict(self): @@ -614,8 +640,8 @@ b('o2'): {'inner': {'aint': 55}}, } r = self.call('argtypes/setnesteddict', - value=(value, {six.binary_type: NestedOuter}), - _rt={six.binary_type: NestedOuter}) + value=(value, {six.binary_type: NestedOuter}), + _rt={six.binary_type: NestedOuter}) print(r) self.assertEquals(r, value) @@ -628,8 +654,8 @@ def test_setnamedattrsobj(self): value = {'attr.1': 10, 'attr.2': 20} r = self.call('argtypes/setnamedattrsobj', - value=(value, NamedAttrsObject), - _rt=NamedAttrsObject) + value=(value, NamedAttrsObject), + _rt=NamedAttrsObject) self.assertEquals(r, value) def test_nested_api(self): @@ -641,9 +667,7 @@ r = self.call('argtypes/setdatetime') print(r) assert "No error raised" - except CallException: - e = sys.exc_info()[1] - print(e) + except CallException as e: self.assertEquals(e.faultcode, 'Client') self.assertEquals(e.faultstring, u('Missing argument: "value"')) @@ -652,5 +676,17 @@ def test_html_format(self): res = self.call('argtypes/setdatetime', _accept="text/html", - _no_result_decode=True) + _no_result_decode=True) self.assertEquals(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) diff -Nru python-wsme-0.5b1/wsme/tests/test_api.py python-wsme-0.6/wsme/tests/test_api.py --- python-wsme-0.5b1/wsme/tests/test_api.py 2013-01-30 16:58:26.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_api.py 2014-02-06 14:49:22.000000000 +0000 @@ -1,15 +1,17 @@ # encoding=utf8 from six import b -import sys -import unittest +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.api import FunctionArgument, FunctionDefinition -from wsme.types import iscomplex +from wsme import types import wsme.types from wsme.tests.test_protocols import DummyProtocol @@ -58,7 +60,55 @@ assert not args[2].mandatory assert args[2].default == 0 - assert iscomplex(ComplexType) + 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): @@ -107,8 +157,7 @@ try: list(scan_api(r)) assert False, "ValueError not raised" - except ValueError: - e = sys.exc_info()[1] + except ValueError as e: assert str(e).startswith("Path is too long") def test_handle_request(self): @@ -222,6 +271,95 @@ self.assertEquals(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): diff -Nru python-wsme-0.5b1/wsme/tests/test_exc.py python-wsme-0.6/wsme/tests/test_exc.py --- python-wsme-0.5b1/wsme/tests/test_exc.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_exc.py 2014-02-06 14:49:22.000000000 +0000 @@ -1,21 +1,29 @@ # encoding=utf8 -from wsme.exc import * +from wsme.exc import (ClientSideError, InvalidInput, MissingArgument, + UnknownArgument) from six import u def test_clientside_error(): e = ClientSideError("Test") - assert e.faultstring == "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 + assert e.faultstring == u( + "Invalid input for field/attribute field. Value: 'badvalue'. " + "error message" + ), e.faultstring def test_missingargument(): diff -Nru python-wsme-0.5b1/wsme/tests/test_protocols_commons.py python-wsme-0.6/wsme/tests/test_protocols_commons.py --- python-wsme-0.5b1/wsme/tests/test_protocols_commons.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_protocols_commons.py 2014-02-06 14:49:22.000000000 +0000 @@ -42,8 +42,12 @@ 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()) + 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): diff -Nru python-wsme-0.5b1/wsme/tests/test_protocols.py python-wsme-0.6/wsme/tests/test_protocols.py --- python-wsme-0.5b1/wsme/tests/test_protocols.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_protocols.py 2014-02-06 14:49:22.000000000 +0000 @@ -68,4 +68,3 @@ 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') - diff -Nru python-wsme-0.5b1/wsme/tests/test_restjson.py python-wsme-0.6/wsme/tests/test_restjson.py --- python-wsme-0.5b1/wsme/tests/test_restjson.py 2013-01-30 17:00:06.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_restjson.py 2014-02-06 14:49:22.000000000 +0000 @@ -47,6 +47,8 @@ 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): @@ -138,19 +140,27 @@ wsme.tests.protocol.WSTestRoot.crud = MiniCrud() -class TestRestJson(wsme.tests.protocol.ProtocolTestCase): +class TestRestJson(wsme.tests.protocol.RestOnlyProtocolTestCase): protocol = 'restjson' - def call(self, fpath, _rt=None, _accept=None, - _no_result_decode=False, **kw): - for key in kw: - if isinstance(kw[key], tuple): - value, datatype = kw[key] + 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: - value = kw[key] - datatype = type(value) - kw[key] = prepare_value(value, datatype) - content = json.dumps(kw) + 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', } @@ -173,14 +183,15 @@ return r else: raise wsme.tests.protocol.CallException( - r['faultcode'], - r['faultstring'], - r.get('debuginfo')) + r['faultcode'], + r['faultstring'], + r.get('debuginfo') + ) return json.loads(res.text) def test_fromjson(self): - assert fromjson(str, None) == None + assert fromjson(str, None) is None def test_keyargs(self): r = self.app.get('/argtypes/setint.json?value=2') @@ -200,8 +211,11 @@ 'value[1].inner.aint': 55 } body = urlencode(params) - r = self.app.post('/argtypes/setnestedarray.json', body, - headers={'Content-Type': 'application/x-www-form-urlencoded'}) + r = self.app.post( + '/argtypes/setnestedarray.json', + body, + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) print(r) assert json.loads(r.text) == [ @@ -209,10 +223,9 @@ {'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) + 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'] == \ @@ -231,21 +244,21 @@ assert json.loads(r.text) == 2 def test_unknown_arg(self): - r = self.app.post('/returntypes/getint.json', - '{"a": 2}', - headers={"Content-Type": "application/json"}, - expect_errors=True) + 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:") + "Unknown argument:" + ) - r = self.app.get('/returntypes/getint.json?a=2', - expect_errors=True) + 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:") + "Unknown argument:" + ) def test_unset_attrs(self): class AType(object): @@ -267,14 +280,12 @@ def test_None_tojson(self): for dt in (datetime.date, datetime.time, datetime.datetime, - decimal.Decimal): + 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}): + for dt in (str, int, datetime.date, datetime.time, datetime.datetime, + decimal.Decimal, [int], {int: int}): assert fromjson(dt, None) is None def test_nest_result(self): @@ -297,8 +308,8 @@ 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) + 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 diff -Nru python-wsme-0.5b1/wsme/tests/test_restxml.py python-wsme-0.6/wsme/tests/test_restxml.py --- python-wsme-0.5b1/wsme/tests/test_restxml.py 2013-01-30 17:00:07.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_restxml.py 2014-02-06 14:49:22.000000000 +0000 @@ -40,7 +40,7 @@ el.text = six.text_type(obj) elif type(obj) in (datetime.date, datetime.time, datetime.datetime): el.text = obj.isoformat() - elif type(obj) == type(None): + elif isinstance(obj, type(None)): el.set('nil', 'true') elif hasattr(datatype, '_wsme_attributes'): for attr in datatype._wsme_attributes: @@ -117,12 +117,15 @@ return datatype(el.text) -class TestRestXML(wsme.tests.protocol.ProtocolTestCase): +class TestRestXML(wsme.tests.protocol.RestOnlyProtocolTestCase): protocol = 'restxml' - def call(self, fpath, _rt=None, _accept=None, - _no_result_decode=False, **kw): - el = dumpxml('parameters', kw) + 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', @@ -142,10 +145,11 @@ 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) + 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) diff -Nru python-wsme-0.5b1/wsme/tests/test_spore.py python-wsme-0.6/wsme/tests/test_spore.py --- python-wsme-0.5b1/wsme/tests/test_spore.py 2013-01-30 17:00:33.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_spore.py 2014-02-06 14:49:22.000000000 +0000 @@ -18,7 +18,7 @@ spore = json.loads(spore) - assert len(spore['methods']) == 47, str(len(spore['methods'])) + assert len(spore['methods']) == 49, str(len(spore['methods'])) m = spore['methods']['argtypes_setbytesarray'] assert m['path'] == 'argtypes/setbytesarray', m['path'] diff -Nru python-wsme-0.5b1/wsme/tests/test_types.py python-wsme-0.6/wsme/tests/test_types.py --- python-wsme-0.5b1/wsme/tests/test_types.py 2013-01-18 22:02:52.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_types.py 2014-02-06 14:49:22.000000000 +0000 @@ -1,7 +1,11 @@ -import unittest -import sys +import re +try: + import unittest2 as unittest +except ImportError: + import unittest import six +from wsme import exc from wsme import types @@ -47,7 +51,7 @@ assert attrs[0].name == 'aint' assert isinstance(attrs[0], types.wsattr) assert attrs[0].datatype == int - assert attrs[0].mandatory == False + assert attrs[0].mandatory is False assert attrs[1].key == 'abytes' assert attrs[1].name == 'abytes' assert attrs[2].key == 'atext' @@ -88,7 +92,7 @@ types.register_type(c) - assert c._wsme_attributes[0].key == 'a1' + 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' @@ -177,13 +181,13 @@ obj.a = 'v1' assert obj.a == 'v1', repr(obj.a) - try: - obj.a = 'v3' - assert False, 'ValueError was not raised' - except ValueError: - e = sys.exc_info()[1] - assert str(e) == \ - "a: Value 'v3' is invalid (should be one of: v1, v2)", e + 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): @@ -199,8 +203,8 @@ obj.aint = 5 assert obj.aint == 5 - self.assertRaises(ValueError, setattr, obj, 'alist', 12) - self.assertRaises(ValueError, setattr, obj, 'alist', [2, 'a']) + self.assertRaises(exc.InvalidInput, setattr, obj, 'alist', 12) + self.assertRaises(exc.InvalidInput, setattr, obj, 'alist', [2, 'a']) def test_text_attribute_conversion(self): class SType(object): @@ -220,15 +224,14 @@ assert isinstance(obj.abytes, types.bytes) def test_named_attribute(self): - class AType(object): + class ABCDType(object): a_list = types.wsattr([int], name='a.list') astr = str - types.register_type(AType) + types.register_type(ABCDType) - assert len(AType._wsme_attributes) == 2 - attrs = AType._wsme_attributes - print(attrs) + 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 @@ -251,19 +254,13 @@ def test_validate_dict(self): assert types.validate_value({int: str}, {1: '1', 5: '5'}) - try: - types.validate_value({int: str}, []) - assert False, "No ValueError raised" - except ValueError: - pass + self.assertRaises(ValueError, types.validate_value, + {int: str}, []) assert types.validate_value({int: str}, {'1': '1', 5: '5'}) - try: - types.validate_value({int: str}, {1: 1, 5: '5'}) - assert False, "No ValueError raised" - except ValueError: - pass + 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]) @@ -277,43 +274,83 @@ assert v.validate(None) is None def test_validate_list_invalid_member(self): - try: - assert types.validate_value([int], ['not-a-number']) - assert False, "No ValueError raised" - except ValueError: - pass + self.assertRaises(ValueError, types.validate_value, [int], + ['not-a-number']) def test_validate_list_invalid_type(self): - try: - assert types.validate_value([int], 1) - assert False, "No ValueError raised" - except ValueError: - pass + 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) - try: - types.validate_value(float, []) - assert False, "No ValueError raised" - except ValueError: - pass - try: - types.validate_value(float, 'not-a-float') - assert False, "No ValueError raised" - except ValueError: - pass + 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) - try: - types.validate_value(int, 1.1) - assert False, "No ValueError raised" - except ValueError: - pass + 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_ipv4_address_type(self): + v = types.IPv4AddressType() + v.validate('127.0.0.1') + v.validate('192.168.0.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') + + def test_validate_ipv6_address_type(self): + v = types.IPv6AddressType() + v.validate('0:0:0:0:0:0:0:1') + v.validate('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') + + def test_validate_uuid_type(self): + v = types.UuidType() + v.validate('6a0a707c-45ef-4758-b533-e55adddba8ce') + v.validate('6a0a707c45ef4758b533e55adddba8ce') + 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, []) @@ -323,9 +360,9 @@ def test_register_invalid_dict(self): self.assertRaises(ValueError, types.register_type, {}) self.assertRaises(ValueError, types.register_type, - {int: str, str: int}) + {int: str, str: int}) self.assertRaises(ValueError, types.register_type, - {types.Unset: str}) + {types.Unset: str}) def test_list_attribute_no_auto_register(self): class MyType(object): @@ -333,11 +370,7 @@ assert not hasattr(MyType, '_wsme_attributes') - try: - types.list_attributes(MyType) - assert False, "TypeError was not raised" - except TypeError: - pass + self.assertRaises(TypeError, types.list_attributes, MyType) assert not hasattr(MyType, '_wsme_attributes') @@ -455,6 +488,7 @@ class buffer: def read(self): return 'from-file' + class fieldstorage: filename = 'static.json' file = buffer() @@ -466,6 +500,7 @@ class buffer: def read(self): return 'from-file' + class fieldstorage: filename = 'static.json' file = None @@ -488,3 +523,107 @@ 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) diff -Nru python-wsme-0.5b1/wsme/tests/test_utils.py python-wsme-0.6/wsme/tests/test_utils.py --- python-wsme-0.5b1/wsme/tests/test_utils.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/tests/test_utils.py 2014-02-06 14:49:22.000000000 +0000 @@ -1,7 +1,7 @@ import datetime import unittest -from wsme.utils import parse_isodate, parse_isotime, parse_isodatetime +from wsme import utils class TestUtils(unittest.TestCase): @@ -18,9 +18,9 @@ '2012-02-30', ] for s, d in good_dates: - assert parse_isodate(s) == d + assert utils.parse_isodate(s) == d for s in ill_formatted_dates + out_of_range_dates: - self.assertRaises(ValueError, parse_isodate, s) + self.assertRaises(ValueError, utils.parse_isodate, s) def test_parse_isotime(self): good_times = [ @@ -35,9 +35,9 @@ '00:54:60', ] for s, t in good_times: - assert parse_isotime(s) == t + assert utils.parse_isotime(s) == t for s in ill_formatted_times + out_of_range_times: - self.assertRaises(ValueError, parse_isotime, s) + self.assertRaises(ValueError, utils.parse_isotime, s) def test_parse_isodatetime(self): good_datetimes = [ @@ -54,6 +54,27 @@ '2012-13-12T00:54:60', ] for s, t in good_datetimes: - assert parse_isodatetime(s) == t + assert utils.parse_isodatetime(s) == t for s in ill_formatted_datetimes + out_of_range_datetimes: - self.assertRaises(ValueError, parse_isodatetime, s) + 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" + ) diff -Nru python-wsme-0.5b1/wsme/tg2.py python-wsme-0.6/wsme/tg2.py --- python-wsme-0.5b1/wsme/tg2.py 2012-11-26 15:14:57.000000000 +0000 +++ python-wsme-0.6/wsme/tg2.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -#from wsme.tg1 import wsexpose, wsvalidate -#import wsme.tg1 -import tg.controllers - - -__all__ = ['adapt', 'wsexpose', 'wsvalidate'] - - -def scan_api(root=None): - for baseurl, instance in cherrypy.tree.apps.items(): - path = [token for token in baseurl.split('/') if token] - for i in wsme.tg1._scan_api(instance.root, path): - yield i - - -def adapt(wsroot): - wsroot._scan_api = scan_api - controller = tg.controllers.WSGIAppController(wsroot.wsgiapp()) - return controller diff -Nru python-wsme-0.5b1/wsme/types.py python-wsme-0.6/wsme/types.py --- python-wsme-0.5b1/wsme/types.py 2013-01-30 17:00:45.000000000 +0000 +++ python-wsme-0.6/wsme/types.py 2014-02-06 14:49:22.000000000 +0000 @@ -3,10 +3,19 @@ import decimal import inspect import logging +import re import six import sys +import uuid import weakref +try: + import ipaddress +except ImportError: + import ipaddr as ipaddress + +from wsme import exc + log = logging.getLogger(__name__) #: The 'str' (python 2) or 'bytes' (python 3) type. @@ -49,7 +58,7 @@ return if not isinstance(value, list): raise ValueError("Wrong type. Expected '[%s]', got '%s'" % ( - self.item_type, type(value) + self.item_type, type(value) )) return [ validate_value(self.item_type, item) @@ -85,8 +94,8 @@ 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) - )) + self.key_type, self.value_type, type(value) + )) return dict(( ( validate_value(self.key_type, key), @@ -134,6 +143,139 @@ 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 + raise ValueError(error) + + return value + + +class IPv4AddressType(UserType): + """ + A simple IPv4 type. + """ + basetype = six.string_types + name = "ipv4address" + + @staticmethod + def validate(value): + try: + ipaddress.IPv4Address(value) + except ipaddress.AddressValueError: + error = 'Value should be IPv4 format' + raise ValueError(error) + + +class IPv6AddressType(UserType): + """ + A simple IPv6 type. + """ + basetype = six.string_types + name = "ipv6address" + + @staticmethod + def validate(value): + try: + ipaddress.IPv6Address(value) + except ipaddress.AddressValueError: + error = 'Value should be IPv6 format' + raise ValueError(error) + + +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: + 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. @@ -154,13 +296,13 @@ self.values = set(values) name = kw.pop('name', None) if name is None: - name = "Enum(%s)" % ', '.join((str(v) for v in values)) + 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 '%s' is invalid (should be one of: %s)" % ( - value, ', '.join(self.values))) + raise ValueError("Value should be one of: %s" % + ', '.join(map(six.text_type, self.values))) return value def tobasetype(self, value): @@ -178,8 +320,16 @@ 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) @@ -192,7 +342,7 @@ def iscomplex(datatype): return inspect.isclass(datatype) \ - and '_wsme_attributes' in datatype.__dict__ + and '_wsme_attributes' in datatype.__dict__ def isarray(datatype): @@ -254,7 +404,7 @@ A specialised :class:`property` to define typed-property on complex types. Example:: - class MyComplexType(object): + class MyComplexType(wsme.types.Base): def get_aint(self): return self._aint @@ -284,20 +434,21 @@ Example:: - class MyComplexType(object): + 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 replace, and + After inspection, the non-wsattr attributes will be replaced, and the above class will be equivalent to:: - class MyComplexType(object): + class MyComplexType(wsme.types.Base): optionalvalue = wsattr(int) mandatoryvalue = wsattr(int, mandatory=True) """ - def __init__(self, datatype, mandatory=False, name=None, default=Unset): + 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 @@ -310,25 +461,38 @@ #: 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(instance, '_' + self.key, self.default) + return getattr( + self._get_dataholder(instance), + self.key, + self.default + ) def __set__(self, instance, value): try: value = validate_value(self.datatype, value) - except ValueError: - e = sys.exc_info()[1] - raise ValueError("%s: %s" % (self.name, e)) + except ValueError as e: + raise exc.InvalidInput(self.name, value, six.text_type(e)) + dataholder = self._get_dataholder(instance) if value is Unset: - if hasattr(instance, '_' + self.key): - delattr(instance, '_' + self.key) + if hasattr(dataholder, self.key): + delattr(dataholder, self.key) else: - setattr(instance, '_' + self.key, value) + setattr(dataholder, self.key, value) def __delete__(self, instance): self.__set__(instance, Unset) @@ -351,7 +515,7 @@ #: attribute data type. Can be either an actual type, #: or a type name, in which case the actual type will be - #: determined when needed (generaly just before scaning the api). + #: determined when needed (generally just before scanning the api). datatype = property(_get_datatype, _set_datatype) @@ -369,7 +533,7 @@ 3 mechanisms are attempted : #. Look for a _wsme_attr_order attribute on the class_. This allow - to define an arbitrary order of the attributes (usefull for + to define an arbitrary order of the attributes (useful for generated types). #. Access the object source code to find the declaration order. @@ -415,18 +579,17 @@ for name, attr in inspect.getmembers(class_, iswsattr): if name.startswith('_'): continue + if inspect.isroutine(attr): + continue - if isinstance(attr, wsattr): - attrdef = attr - elif isinstance(attr, wsproperty): + if isinstance(attr, (wsattr, wsproperty)): attrdef = attr else: if attr not in native_types and ( inspect.isclass(attr) - or isinstance(attr, list) - or isinstance(attr, dict)): + or isinstance(attr, (list, dict))): register_type(attr) - attrdef = wsattr(attr) + attrdef = getattr(class_, '__wsattrclass__', wsattr)(attr) attrdef.key = name if attrdef.name is None: @@ -448,6 +611,19 @@ 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 = [] @@ -491,11 +667,47 @@ 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 @@ -519,8 +731,9 @@ self.array_types.add(type_) elif isinstance(type_, DictType): type_ = DictType( - type_.key_type, - self.resolve_type(type_.value_type)) + type_.key_type, + self.resolve_type(type_.value_type) + ) self.dict_types.add(type_) else: type_ = self.register(type_) @@ -536,16 +749,17 @@ class BaseMeta(type): def __new__(cls, name, bases, dct): - if bases[0] is not object and '__registry__' not in 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[0] is not object: + if bases and bases[0] is not object: 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): @@ -577,7 +791,7 @@ content = wsproperty(binary, _get_content, _set_content) def __init__(self, filename=None, file=None, content=None, - contenttype=None, fieldstorage=None): + contenttype=None, fieldstorage=None): self.filename = filename self.contenttype = contenttype self._file = file @@ -596,3 +810,26 @@ 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) diff -Nru python-wsme-0.5b1/wsme/utils.py python-wsme-0.6/wsme/utils.py --- python-wsme-0.5b1/wsme/utils.py 2013-01-15 10:43:37.000000000 +0000 +++ python-wsme-0.6/wsme/utils.py 2014-02-06 14:49:22.000000000 +0000 @@ -1,6 +1,12 @@ import decimal import datetime import re +from six.moves import builtins, http_client + +try: + import dateutil.parser +except: + dateutil = None # noqa date_re = r'(?P-?\d{4,})-(?P\d{2})-(?P\d{2})' time_re = r'(?P\d{2}):(?P\d{2}):(?P\d{2})' + \ @@ -14,6 +20,13 @@ time_re = re.compile(time_re) +if hasattr(builtins, '_'): + _ = builtins._ +else: + def _(s): + return s + + def parse_isodate(value): m = date_re.match(value) if m is None: @@ -48,6 +61,8 @@ # TODO handle timezone 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)) @@ -69,263 +84,19 @@ raise ValueError("'%s' is a out-of-range datetime" % (value)) +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: - # Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy. - # Passes Python2.7's test suite and incorporates all the latest updates. - - try: - from thread import get_ident as _get_ident - except ImportError: - from dummy_thread import get_ident as _get_ident - - try: - from _abcoll import KeysView, ValuesView, ItemsView - except ImportError: - pass - - class OrderedDict(dict): - 'Dictionary that remembers insertion order' - # An inherited dict maps keys to values. - # The inherited dict provides __getitem__, __len__, __contains__, and get. - # The remaining methods are order-aware. - # Big-O running times for all methods are the same as for regular dictionaries. - - # The internal self.__map dictionary maps keys to links in a doubly linked list. - # The circular doubly linked list starts and ends with a sentinel element. - # The sentinel element never gets deleted (this simplifies the algorithm). - # Each link is stored as a list of length three: [PREV, NEXT, KEY]. - - def __init__(self, *args, **kwds): - '''Initialize an ordered dictionary. Signature is the same as for - regular dictionaries, but keyword arguments are not recommended - because their insertion order is arbitrary. - - ''' - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__root - except AttributeError: - self.__root = root = [] # sentinel node - root[:] = [root, root, None] - self.__map = {} - self.__update(*args, **kwds) - - def __setitem__(self, key, value, dict_setitem=dict.__setitem__): - 'od.__setitem__(i, y) <==> od[i]=y' - # Setting a new item creates a new link which goes at the end of the linked - # list, and the inherited dictionary is updated with the new key/value pair. - if key not in self: - root = self.__root - last = root[0] - last[1] = root[0] = self.__map[key] = [last, root, key] - dict_setitem(self, key, value) - - def __delitem__(self, key, dict_delitem=dict.__delitem__): - 'od.__delitem__(y) <==> del od[y]' - # Deleting an existing item uses self.__map to find the link which is - # then removed by updating the links in the predecessor and successor nodes. - dict_delitem(self, key) - link_prev, link_next, key = self.__map.pop(key) - link_prev[1] = link_next - link_next[0] = link_prev - - def __iter__(self): - 'od.__iter__() <==> iter(od)' - root = self.__root - curr = root[1] - while curr is not root: - yield curr[2] - curr = curr[1] - - def __reversed__(self): - 'od.__reversed__() <==> reversed(od)' - root = self.__root - curr = root[0] - while curr is not root: - yield curr[2] - curr = curr[0] - - def clear(self): - 'od.clear() -> None. Remove all items from od.' - try: - for node in self.__map.itervalues(): - del node[:] - root = self.__root - root[:] = [root, root, None] - self.__map.clear() - except AttributeError: - pass - dict.clear(self) - - def popitem(self, last=True): - '''od.popitem() -> (k, v), return and remove a (key, value) pair. - Pairs are returned in LIFO order if last is true or FIFO order if false. - - ''' - if not self: - raise KeyError('dictionary is empty') - root = self.__root - if last: - link = root[0] - link_prev = link[0] - link_prev[1] = root - root[0] = link_prev - else: - link = root[1] - link_next = link[1] - root[1] = link_next - link_next[0] = root - key = link[2] - del self.__map[key] - value = dict.pop(self, key) - return key, value - - # -- the following methods do not depend on the internal structure -- - - def keys(self): - 'od.keys() -> list of keys in od' - return list(self) - - def values(self): - 'od.values() -> list of values in od' - return [self[key] for key in self] - - def items(self): - 'od.items() -> list of (key, value) pairs in od' - return [(key, self[key]) for key in self] - - def iterkeys(self): - 'od.iterkeys() -> an iterator over the keys in od' - return iter(self) - - def itervalues(self): - 'od.itervalues -> an iterator over the values in od' - for k in self: - yield self[k] - - def iteritems(self): - 'od.iteritems -> an iterator over the (key, value) items in od' - for k in self: - yield (k, self[k]) - - def update(*args, **kwds): - '''od.update(E, **F) -> None. Update od from dict/iterable E and F. - - If E is a dict instance, does: for k in E: od[k] = E[k] - If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] - Or if E is an iterable of items, does: for k, v in E: od[k] = v - In either case, this is followed by: for k, v in F.items(): od[k] = v - - ''' - if len(args) > 2: - raise TypeError('update() takes at most 2 positional ' - 'arguments (%d given)' % (len(args),)) - elif not args: - raise TypeError('update() takes at least 1 argument (0 given)') - self = args[0] - # Make progressively weaker assumptions about "other" - other = () - if len(args) == 2: - other = args[1] - if isinstance(other, dict): - for key in other: - self[key] = other[key] - elif hasattr(other, 'keys'): - for key in other.keys(): - self[key] = other[key] - else: - for key, value in other: - self[key] = value - for key, value in kwds.items(): - self[key] = value - - __update = update # let subclasses override update without breaking __init__ - - __marker = object() - - def pop(self, key, default=__marker): - '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. - If key is not found, d is returned if given, otherwise KeyError is raised. - - ''' - if key in self: - result = self[key] - del self[key] - return result - if default is self.__marker: - raise KeyError(key) - return default - - def setdefault(self, key, default=None): - 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' - if key in self: - return self[key] - self[key] = default - return default - - def __repr__(self, _repr_running={}): - 'od.__repr__() <==> repr(od)' - call_key = id(self), _get_ident() - if call_key in _repr_running: - return '...' - _repr_running[call_key] = 1 - try: - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - finally: - del _repr_running[call_key] - - def __reduce__(self): - 'Return state information for pickling' - items = [[k, self[k]] for k in self] - inst_dict = vars(self).copy() - for k in vars(OrderedDict()): - inst_dict.pop(k, None) - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def copy(self): - 'od.copy() -> a shallow copy of od' - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S - and values equal to v (which defaults to None). - - ''' - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive - while comparison to a regular mapping is order-insensitive. - - ''' - if isinstance(other, OrderedDict): - return len(self)==len(other) and self.items() == other.items() - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other - - # -- the following methods are only used in Python 2.7 -- - - def viewkeys(self): - "od.viewkeys() -> a set-like object providing a view on od's keys" - return KeysView(self) - - def viewvalues(self): - "od.viewvalues() -> an object providing a view on od's values" - return ValuesView(self) - - def viewitems(self): - "od.viewitems() -> a set-like object providing a view on od's items" - return ItemsView(self) + from ordereddict import OrderedDict # noqa diff -Nru python-wsme-0.5b1/WSME.egg-info/namespace_packages.txt python-wsme-0.6/WSME.egg-info/namespace_packages.txt --- python-wsme-0.5b1/WSME.egg-info/namespace_packages.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/WSME.egg-info/namespace_packages.txt 2014-02-06 14:49:32.000000000 +0000 @@ -0,0 +1 @@ +wsmeext diff -Nru python-wsme-0.5b1/WSME.egg-info/not-zip-safe python-wsme-0.6/WSME.egg-info/not-zip-safe --- python-wsme-0.5b1/WSME.egg-info/not-zip-safe 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/WSME.egg-info/not-zip-safe 2014-02-06 14:49:27.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru python-wsme-0.5b1/WSME.egg-info/PKG-INFO python-wsme-0.6/WSME.egg-info/PKG-INFO --- python-wsme-0.5b1/WSME.egg-info/PKG-INFO 2013-01-30 17:21:39.000000000 +0000 +++ python-wsme-0.6/WSME.egg-info/PKG-INFO 2014-02-06 14:49:32.000000000 +0000 @@ -1,8 +1,7 @@ Metadata-Version: 1.1 Name: WSME -Version: 0.5b1 -Summary: Web Services Made Easy makes it easy to -implement multi-protocol webservices. +Version: 0.6 +Summary: Simplify the writing of REST APIs, and extend them with additional protocols. Home-page: UNKNOWN Author: "Christophe de Vienne" Author-email: "python-wsme@googlegroups.com" @@ -18,7 +17,7 @@ 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 prefered way of routing your REST + (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 ones supporting batch-calls @@ -32,11 +31,11 @@ Here is a standalone wsgi example:: - from wsme import WSRoot, expose, validate + from wsme import WSRoot, expose class MyService(WSRoot): - @expose(unicode) - @validate(unicode) + @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) @@ -69,9 +68,9 @@ - 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 independance : adapters are provided to easily integrate + - Framework independence : adapters are provided to easily integrate your API in any web framework, for example a wsgi container, - Pecan_, TurboGears_, cornice_... + 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 @@ -79,6 +78,7 @@ .. _Pecan: http://pecanpy.org/ .. _TurboGears: http://www.turbogears.org/ + .. _Flask: http://flask.pocoo.org/ .. _cornice: http://pypi.python.org/pypi/cornice Install @@ -86,6 +86,12 @@ :: + pip install WSME + + or, if you do not have pip on your system or virtualenv + + :: + easy_install WSME Changes @@ -103,13 +109,13 @@ ~~~~~~~~~~ :Report issues: `WSME issue tracker`_ - :Source code: hg clone https://bitbucket.org/cdevienne/wsme/ - :Jenkins: https://jenkins.shiningpanda.com/wsme/ + :Source code: git clone https://github.com/stackforge/wsme/ + :Gerrit: https://review.openstack.org/#/q/project:stackforge/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://bitbucket.org/cdevienne/wsme/issues?status=new&status=open + .. _WSME issue tracker: https://bugs.launchpad.net/wsme/+bugs .. _Sphinx: http://sphinx.pocoo.org/ @@ -117,10 +123,10 @@ Classifier: Development Status :: 3 - Alpha Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: MIT License diff -Nru python-wsme-0.5b1/WSME.egg-info/requires.txt python-wsme-0.6/WSME.egg-info/requires.txt --- python-wsme-0.5b1/WSME.egg-info/requires.txt 2013-01-30 17:21:39.000000000 +0000 +++ python-wsme-0.6/WSME.egg-info/requires.txt 2014-02-06 14:49:32.000000000 +0000 @@ -1,3 +1,4 @@ six simplegeneric -WebOb \ No newline at end of file +WebOb +ipaddr \ No newline at end of file diff -Nru python-wsme-0.5b1/WSME.egg-info/SOURCES.txt python-wsme-0.6/WSME.egg-info/SOURCES.txt --- python-wsme-0.5b1/WSME.egg-info/SOURCES.txt 2013-01-30 17:21:45.000000000 +0000 +++ python-wsme-0.6/WSME.egg-info/SOURCES.txt 2014-02-06 14:49:32.000000000 +0000 @@ -1,29 +1,69 @@ +.hgtags +AUTHORS +ChangeLog LICENSE -MANIFEST.in README.rst setup.cfg setup.py +tox-tmpl.ini +tox.ini +toxgen.py WSME.egg-info/PKG-INFO WSME.egg-info/SOURCES.txt WSME.egg-info/dependency_links.txt WSME.egg-info/entry_points.txt +WSME.egg-info/namespace_packages.txt +WSME.egg-info/not-zip-safe WSME.egg-info/requires.txt WSME.egg-info/top_level.txt -examples/WSMECorniceDemo/setup.py -examples/WSMECorniceDemo/wsmecornicedemo/__init__.py -examples/WSMECorniceDemo/wsmecornicedemo/views.py +doc/Makefile +doc/api.rst +doc/changes.rst +doc/conf.py +doc/document.rst +doc/functions.rst +doc/gettingstarted.rst +doc/index.rst +doc/integrate.rst +doc/make.bat +doc/protocols.rst +doc/requirements.txt +doc/todo.rst +doc/types.rst +doc/_static/toggle.css +doc/_static/toggle.js +doc/_static/wsme.css examples/demo/client.py examples/demo/demo.py examples/demo/setup.cfg examples/demo/setup.py examples/demo/sporeclient.py +tests/test_cornice.py +tests/test_flask.py +tests/test_sphinxext.py +tests/test_tg1.py +tests/test_tg15.py +tests/pecantest/setup.cfg +tests/pecantest/setup.py +tests/pecantest/test/__init__.py +tests/pecantest/test/app.py +tests/pecantest/test/controllers/__init__.py +tests/pecantest/test/controllers/root.py +tests/pecantest/test/controllers/ws.py +tests/pecantest/test/model/__init__.py +tests/pecantest/test/tests/__init__.py +tests/pecantest/test/tests/config.py +tests/pecantest/test/tests/test_ws.py +tests/sphinxexample/conf.py +tests/sphinxexample/document.rst +tests/sphinxexample/index.rst wsme/__init__.py wsme/api.py wsme/exc.py wsme/protocol.py wsme/root.py +wsme/runtime.py wsme/spore.py -wsme/tg2.py wsme/types.py wsme/utils.py wsme/rest/__init__.py @@ -45,8 +85,25 @@ wsme/tests/test_utils.py wsmeext/__init__.py wsmeext/cornice.py +wsmeext/flask.py wsmeext/pecan.py wsmeext/sphinxext.py wsmeext/tg1.py wsmeext/tg11.py -wsmeext/tg15.py \ No newline at end of file +wsmeext/tg15.py +wsmeext/extdirect/__init__.py +wsmeext/extdirect/datastore.py +wsmeext/extdirect/protocol.py +wsmeext/extdirect/sadatastore.py +wsmeext/soap/__init__.py +wsmeext/soap/protocol.py +wsmeext/soap/simplegeneric.py +wsmeext/soap/wsdl.py +wsmeext/sqlalchemy/__init__.py +wsmeext/sqlalchemy/controllers.py +wsmeext/sqlalchemy/types.py +wsmeext/tests/__init__.py +wsmeext/tests/test_extdirect.py +wsmeext/tests/test_soap.py +wsmeext/tests/test_sqlalchemy_controllers.py +wsmeext/tests/test_sqlalchemy_types.py \ No newline at end of file diff -Nru python-wsme-0.5b1/wsmeext/cornice.py python-wsme-0.6/wsmeext/cornice.py --- python-wsme-0.5b1/wsmeext/cornice.py 2013-01-30 16:40:29.000000000 +0000 +++ python-wsme-0.6/wsmeext/cornice.py 2014-02-06 14:49:22.000000000 +0000 @@ -15,18 +15,19 @@ return Message(text='Hello %s' % who) """ from __future__ import absolute_import -import json -import xml.etree.ElementTree as et +import inspect +import sys import wsme -import wsme.protocols -from wsme.protocols import restjson -from wsme.protocols import restxml +from wsme.rest import json as restjson +from wsme.rest import xml as restxml +import wsme.runtime +import wsme.api import functools -from wsme.protocols.commons import ( - args_from_params, args_from_body, combine_args +from wsme.rest.args import ( + args_from_args, args_from_params, args_from_body, combine_args ) @@ -37,11 +38,21 @@ def __call__(self, data, context): response = context['request'].response response.content_type = 'application/json' - data = restjson.tojson( - data['datatype'], - data['result'] - ) - return json.dumps(data) + 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): @@ -50,39 +61,82 @@ 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' - data = restxml.toxml( - data['datatype'], - 'result', - data['result'] - ) - return et.tostring(data) + 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.sig(*args, **kwargs) + sig = wsme.signature(*args, **kwargs) def decorate(f): - sig(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(request): - args, kwargs = combine_args( - funcdef, - args_from_params(funcdef, request.params), - args_from_body(funcdef, request.body, request.content_type) - ) - if 'application/json' in request.headers['Accept']: - request.override_renderer = 'wsmejson' - elif 'text/xml' in request.headers['Accept']: - request.override_renderer = 'wsmexml' + 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.override_renderer = 'wsmejson' - return { - 'datatype': funcdef.return_type, - 'result': f(*args, **kwargs) - } + 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 diff -Nru python-wsme-0.5b1/wsmeext/extdirect/datastore.py python-wsme-0.6/wsmeext/extdirect/datastore.py --- python-wsme-0.5b1/wsmeext/extdirect/datastore.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/extdirect/datastore.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,121 @@ +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,), {} +) diff -Nru python-wsme-0.5b1/wsmeext/extdirect/__init__.py python-wsme-0.6/wsmeext/extdirect/__init__.py --- python-wsme-0.5b1/wsmeext/extdirect/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/extdirect/__init__.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1 @@ +from wsmeext.extdirect.protocol import ExtDirectProtocol # noqa diff -Nru python-wsme-0.5b1/wsmeext/extdirect/protocol.py python-wsme-0.6/wsmeext/extdirect/protocol.py --- python-wsme-0.5b1/wsmeext/extdirect/protocol.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/extdirect/protocol.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,450 @@ +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) diff -Nru python-wsme-0.5b1/wsmeext/extdirect/sadatastore.py python-wsme-0.6/wsmeext/extdirect/sadatastore.py --- python-wsme-0.5b1/wsmeext/extdirect/sadatastore.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/extdirect/sadatastore.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,19 @@ +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 + ) diff -Nru python-wsme-0.5b1/wsmeext/flask.py python-wsme-0.6/wsmeext/flask.py --- python-wsme-0.5b1/wsmeext/flask.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/flask.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,101 @@ +from __future__ import absolute_import + +import functools +import logging +import sys + +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): + sig(f) + funcdef = wsme.api.FunctionDefinition.get(f) + funcdef.resolve_types(wsme.types.registry) + + @functools.wraps(f) + def wrapper(*args, **kwargs): + 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: + 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 diff -Nru python-wsme-0.5b1/wsmeext/pecan.py python-wsme-0.6/wsmeext/pecan.py --- python-wsme-0.5b1/wsmeext/pecan.py 2013-01-30 16:59:33.000000000 +0000 +++ python-wsme-0.6/wsmeext/pecan.py 2014-02-06 14:49:22.000000000 +0000 @@ -11,6 +11,8 @@ import pecan +from wsme.utils import is_valid_code + class JSonRenderer(object): def __init__(self, path, extra_vars): @@ -51,6 +53,11 @@ content_type='application/xml', generic=False ) + pecan_text_xml_decorate = pecan.expose( + template='wsmexml:', + content_type='text/xml', + generic=False + ) sig = wsme.signature(*args, **kwargs) def decorate(f): @@ -62,24 +69,50 @@ def callfunction(self, *args, **kwargs): try: args, kwargs = wsme.rest.args.get_args( - funcdef, args, kwargs, pecan.request.params, + 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 + result = result.obj + except: - data = wsme.api.format_exception(sys.exc_info()) - if data['faultcode'] == 'Client': - pecan.response.status = 400 + 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 funcdef.return_type is None: + pecan.request.pecan['content_type'] = None + pecan.response.content_type = None + return '' + return dict( datatype=funcdef.return_type, result=result ) pecan_xml_decorate(callfunction) + pecan_text_xml_decorate(callfunction) pecan_json_decorate(callfunction) pecan.util._cfg(callfunction)['argspec'] = inspect.getargspec(f) callfunction._wsme_definition = funcdef diff -Nru python-wsme-0.5b1/wsmeext/soap/__init__.py python-wsme-0.6/wsmeext/soap/__init__.py --- python-wsme-0.5b1/wsmeext/soap/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/soap/__init__.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,5 @@ +from __future__ import absolute_import + +from wsmeext.soap.protocol import SoapProtocol + +__all__ = ['SoapProtocol'] diff -Nru python-wsme-0.5b1/wsmeext/soap/protocol.py python-wsme-0.6/wsmeext/soap/protocol.py --- python-wsme-0.5b1/wsmeext/soap/protocol.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/soap/protocol.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,475 @@ +""" +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", + #unsigned: "xs:unsignedInt", + 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): + print('bytes_tosoap', 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): + print(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: + 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) + )) + print('soap_response', 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): + print('encode_result', 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 diff -Nru python-wsme-0.5b1/wsmeext/soap/simplegeneric.py python-wsme-0.6/wsmeext/soap/simplegeneric.py --- python-wsme-0.5b1/wsmeext/soap/simplegeneric.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/soap/simplegeneric.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,107 @@ +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()) diff -Nru python-wsme-0.5b1/wsmeext/soap/wsdl.py python-wsme-0.6/wsmeext/soap/wsdl.py --- python-wsme-0.5b1/wsmeext/soap/wsdl.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/soap/wsdl.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,296 @@ +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='%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, + wsdl_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) diff -Nru python-wsme-0.5b1/wsmeext/sphinxext.py python-wsme-0.6/wsmeext/sphinxext.py --- python-wsme-0.5b1/wsmeext/sphinxext.py 2013-01-18 10:38:56.000000000 +0000 +++ python-wsme-0.6/wsmeext/sphinxext.py 2014-02-06 14:49:22.000000000 +0000 @@ -2,6 +2,8 @@ import re import sys +import six + from sphinx import addnodes from sphinx.ext import autodoc from sphinx.domains.python import PyClasslike, PyClassmember @@ -37,7 +39,7 @@ def make_sample_object(datatype): if datatype is wsme.types.bytes: - return 'samplestring' + return six.b('samplestring') if datatype is wsme.types.text: return u'sample unicode' if datatype is int: @@ -76,7 +78,7 @@ @classmethod def sample(cls): - return SampleType(10) + return cls(10) class SampleService(wsme.WSRoot): @@ -158,7 +160,7 @@ class AttributeDirective(PyClassmember): doc_field_types = [ Field('datatype', label=l_('Type'), has_arg=False, - names=('type', 'datatype')) + names=('type', 'datatype')) ] @@ -194,8 +196,8 @@ 'samples-slot': check_samples_slot, }) - @classmethod - def can_document_member(cls, member, membername, isattr, parent): + @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 @@ -223,8 +225,6 @@ # Check where to include the samples samples_slot = self.options.samples_slot or self.default_samples_slot - print 'SAMPLES SLOT:', self.options.samples_slot - def add_docstring(): super(TypeDocumenter, self).add_content( more_content, no_docstring) @@ -250,8 +250,9 @@ u' .. code-block:: ' + language, u'', ]) - content.extend(( - u' ' * 8 + line for line in sample.split('\n'))) + content.extend( + u' ' * 8 + line + for line in six.text_type(sample).split('\n')) for line in content: self.add_line(line, u' 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 not attr.name in attrs: + continue + if attr.isrelation and not attr.name 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 diff -Nru python-wsme-0.5b1/wsmeext/tests/test_extdirect.py python-wsme-0.6/wsmeext/tests/test_extdirect.py --- python-wsme-0.5b1/wsmeext/tests/test_extdirect.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/tests/test_extdirect.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,243 @@ +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 + } + }] + } diff -Nru python-wsme-0.5b1/wsmeext/tests/test_soap.py python-wsme-0.6/wsmeext/tests/test_soap.py --- python-wsme-0.5b1/wsmeext/tests/test_soap.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/tests/test_soap.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,424 @@ +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 = """ + + + + <%(method)s> + %(params)s + + + + +""" % 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 not dt 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): + #assert res.body.find('NestedOuter_Array') != -1 + #assert 'returntypesGettext' in res.body + #assert 'returntypesGettextResponse' in res.body +# + c = self.sudsclient + print(c) + assert c.wsdl.tns[1] == tns, c.wsdl.tns + + sd = c.sd[0] + + assert len(sd.ports) == 1 + port, methods = sd.ports[0] + self.assertEquals(len(methods), 49) + + 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. diff -Nru python-wsme-0.5b1/wsmeext/tests/test_sqlalchemy_controllers.py python-wsme-0.6/wsmeext/tests/test_sqlalchemy_controllers.py --- python-wsme-0.5b1/wsmeext/tests/test_sqlalchemy_controllers.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/tests/test_sqlalchemy_controllers.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,209 @@ +import datetime + +try: + import json +except ImportError: + import simplejson as json + +from webtest import TestApp + +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, Unicode, Date, ForeignKey +from sqlalchemy.orm import relation + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session + +from wsme import WSRoot +import wsme.types + +from wsmeext.sqlalchemy.types import generate_types +from wsmeext.sqlalchemy.controllers import CRUDController + +from six import u + +engine = create_engine('sqlite:///') +DBSession = scoped_session(sessionmaker(autocommit=False, autoflush=False, + bind=engine)) +DBBase = declarative_base() + +registry = wsme.types.Registry() + + +class DBPerson(DBBase): + __tablename__ = 'person' + + id = Column(Integer, primary_key=True) + name = Column(Unicode(50)) + birthdate = Column(Date) + + addresses = relation('DBAddress') + + +class DBAddress(DBBase): + __tablename__ = 'address' + + id = Column(Integer, primary_key=True) + + _person_id = Column('person_id', ForeignKey(DBPerson.id)) + + street = Column(Unicode(50)) + city = Column(Unicode(50)) + + person = relation(DBPerson) + + +globals().update( + generate_types(DBPerson, DBAddress, makename=lambda s: s[2:], + registry=registry)) + + +class PersonController(CRUDController): + __saclass__ = DBPerson + __dbsession__ = DBSession + __registry__ = registry + + +class AddressController(CRUDController): + __saclass__ = DBAddress + __dbsession__ = DBSession + __registry__ = registry + + +class Root(WSRoot): + __registry__ = registry + + person = PersonController() + address = AddressController() + + +class TestCRUDController(): + def setUp(self): + DBBase.metadata.create_all(DBSession.bind) + + self.root = Root() + self.root.getapi() + self.root.addprotocol('restjson') + + self.app = TestApp(self.root.wsgiapp()) + + def tearDown(self): + DBBase.metadata.drop_all(DBSession.bind) + + def test_create(self): + data = dict(data=dict( + name=u('Pierre-Joseph'), + birthdate=u('1809-01-15') + )) + r = self.app.post('/person/create', json.dumps(data), + headers={'Content-Type': 'application/json'}) + r = json.loads(r.text) + print(r) + assert r['name'] == u('Pierre-Joseph') + assert r['birthdate'] == u('1809-01-15') + + def test_PUT(self): + data = dict(data=dict( + name=u('Pierre-Joseph'), + birthdate=u('1809-01-15') + )) + r = self.app.put('/person', json.dumps(data), + headers={'Content-Type': 'application/json'}) + r = json.loads(r.text) + print(r) + assert r['name'] == u('Pierre-Joseph') + assert r['birthdate'] == u('1809-01-15') + + def test_read(self): + p = DBPerson( + name=u('Pierre-Joseph'), + birthdate=datetime.date(1809, 1, 15)) + DBSession.add(p) + DBSession.flush() + pid = p.id + r = self.app.post('/person/read', '{"ref": {"id": %s}}' % pid, + headers={'Content-Type': 'application/json'}) + r = json.loads(r.text) + print(r) + assert r['name'] == u('Pierre-Joseph') + assert r['birthdate'] == u('1809-01-15') + + def test_GET(self): + p = DBPerson( + name=u('Pierre-Joseph'), + birthdate=datetime.date(1809, 1, 15)) + DBSession.add(p) + DBSession.flush() + pid = p.id + r = self.app.get('/person?ref.id=%s' % pid, + headers={'Content-Type': 'application/json'}) + r = json.loads(r.text) + print(r) + assert r['name'] == u('Pierre-Joseph') + assert r['birthdate'] == u('1809-01-15') + + def test_update(self): + p = DBPerson( + name=u('Pierre-Joseph'), + birthdate=datetime.date(1809, 1, 15)) + DBSession.add(p) + DBSession.flush() + pid = p.id + data = { + "id": pid, + "name": u('Pierre-Joseph Proudon') + } + r = self.app.post('/person/update', json.dumps(dict(data=data)), + headers={'Content-Type': 'application/json'}) + r = json.loads(r.text) + print(r) + assert r['name'] == u('Pierre-Joseph Proudon') + assert r['birthdate'] == u('1809-01-15') + + def test_POST(self): + p = DBPerson( + name=u('Pierre-Joseph'), + birthdate=datetime.date(1809, 1, 15)) + DBSession.add(p) + DBSession.flush() + pid = p.id + data = { + "id": pid, + "name": u('Pierre-Joseph Proudon') + } + r = self.app.post('/person', json.dumps(dict(data=data)), + headers={'Content-Type': 'application/json'}) + r = json.loads(r.text) + print(r) + assert r['name'] == u('Pierre-Joseph Proudon') + assert r['birthdate'] == u('1809-01-15') + + def test_delete(self): + p = DBPerson( + name=u('Pierre-Joseph'), + birthdate=datetime.date(1809, 1, 15)) + DBSession.add(p) + DBSession.flush() + pid = p.id + r = self.app.post('/person/delete', json.dumps( + dict(ref=dict(id=pid))), + headers={ + 'Content-Type': 'application/json' + }) + print(r) + assert DBSession.query(DBPerson).get(pid) is None + + def test_DELETE(self): + p = DBPerson( + name=u('Pierre-Joseph'), + birthdate=datetime.date(1809, 1, 15)) + DBSession.add(p) + DBSession.flush() + pid = p.id + r = self.app.delete('/person?ref.id=%s' % pid, + headers={'Content-Type': 'application/json'}) + print(r) + assert DBSession.query(DBPerson).get(pid) is None + + def test_nothing(self): + pass diff -Nru python-wsme-0.5b1/wsmeext/tests/test_sqlalchemy_types.py python-wsme-0.6/wsmeext/tests/test_sqlalchemy_types.py --- python-wsme-0.5b1/wsmeext/tests/test_sqlalchemy_types.py 1970-01-01 00:00:00.000000000 +0000 +++ python-wsme-0.6/wsmeext/tests/test_sqlalchemy_types.py 2014-02-06 14:49:22.000000000 +0000 @@ -0,0 +1,72 @@ +import datetime + +import wsmeext.sqlalchemy.types + +from wsme.types import text, Unset, isarray + +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, String, Date, ForeignKey +from sqlalchemy.orm import relation + +from six import u + +SABase = declarative_base() + + +class SomeClass(SABase): + __tablename__ = 'some_table' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + + adate = Column(Date) + + +def test_complextype(): + class AType(wsmeext.sqlalchemy.types.Base): + __saclass__ = SomeClass + + assert AType.id.datatype is int + assert AType.name.datatype is text + assert AType.adate.datatype is datetime.date + + a = AType() + s = SomeClass(name=u('aname'), adate=datetime.date(2012, 6, 26)) + assert s.name == u('aname') + + a.from_instance(s) + assert a.name == u('aname') + assert a.adate == datetime.date(2012, 6, 26) + + a.name = u('test') + del a.adate + assert a.adate is Unset + + a.to_instance(s) + assert s.name == u('test') + assert s.adate == datetime.date(2012, 6, 26) + + +def test_generate(): + class A(SABase): + __tablename__ = 'a' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + + _b_id = Column(ForeignKey('b.id')) + + b = relation('B') + + class B(SABase): + __tablename__ = 'b' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + + alist = relation(A) + + newtypes = wsmeext.sqlalchemy.types.generate_types(A, B) + + assert newtypes['A'].id.datatype is int + assert newtypes['A'].b.datatype is newtypes['B'] + assert newtypes['B'].id.datatype is int + assert isarray(newtypes['B'].alist.datatype) + assert newtypes['B'].alist.datatype.item_type is newtypes['A'] diff -Nru python-wsme-0.5b1/wsmeext/tg1.py python-wsme-0.6/wsmeext/tg1.py --- python-wsme-0.5b1/wsmeext/tg1.py 2013-01-17 14:54:10.000000000 +0000 +++ python-wsme-0.6/wsmeext/tg1.py 2014-02-06 14:49:22.000000000 +0000 @@ -4,15 +4,17 @@ import simplejson as json # noqa import functools +import sys import cherrypy import webob -from turbogears import expose +from turbogears import expose, util from wsme.rest import validate as wsvalidate import wsme.api import wsme.rest.args import wsme.rest.json +from wsme.utils import is_valid_code import inspect @@ -49,11 +51,41 @@ def callfunction(self, *args, **kwargs): args, kwargs = wsme.rest.args.get_args( funcdef, args, kwargs, - cherrypy.request.params, + cherrypy.request.params, None, cherrypy.request.body, cherrypy.request.headers['Content-Type'] ) - result = f(self, *args, **kwargs) + if funcdef.pass_request: + kwargs[funcdef.pass_request] = cherrypy.request + try: + result = f(self, *args, **kwargs) + except: + try: + exception_info = sys.exc_info() + orig_exception = exception_info[1] + if isinstance(orig_exception, cherrypy.HTTPError): + orig_code = getattr(orig_exception, 'status', None) + else: + orig_code = getattr(orig_exception, 'code', None) + data = wsme.api.format_exception(exception_info) + finally: + del exception_info + + cherrypy.response.status = 500 + if data['faultcode'] == 'client': + cherrypy.response.status = 400 + elif orig_code and is_valid_code(orig_code): + cherrypy.response.status = orig_code + + accept = cherrypy.request.headers.get('Accept', "").lower() + accept = util.simplify_http_accept_header(accept) + + decorators = {'text/xml': wsme.rest.xml.encode_error} + return decorators.get( + accept, + wsme.rest.json.encode_error + )(None, data) + return dict( datatype=funcdef.return_type, result=result @@ -114,7 +146,6 @@ cherrypy.response.status = res.status return res.body - import wsme.rest