diff --git a/doc/api.rst b/doc/api.rst index 62f0134..5086d53 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -1,8 +1,39 @@ API === -.. autoclass:: wsme.controller.WSRoot +Public API +---------- + +:mod:`wsme` -- Essentials +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. module:: wsme .. autoclass:: wsme.expose .. autoclass:: wsme.validate +.. autoclass:: wsme.wsproperty +.. autoclass:: wsme.wsattr + + +Internals +--------- + +:mod:`wsme.types` -- Types +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: wsme.types + :members: register_type + +:mod:`wsme.controller` -- Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: wsme.controller + :members: scan_api, FunctionArgument, FunctionDefinition, WSRoot + +:mod:`wsme.rest` -- REST protocol commons +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: wsme.rest + :members: + diff --git a/doc/changes.rst b/doc/changes.rst index ef87e4e..fa86129 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -4,14 +4,16 @@ Changes next ---- -* 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`). +* 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. 0.1.0a2 (2011-10-07) -------------------- -* Added support for arrays in all the protocols +* Added support for arrays in all the protocols 0.1.0a1 (2011-10-04) -------------------- diff --git a/doc/conf.py b/doc/conf.py index 81f566b..dff2348 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,7 @@ sys.path.insert(0, os.path.abspath('..')) # 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'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -92,7 +92,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'agogo' # 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 @@ -215,3 +215,6 @@ man_pages = [ ('index', 'webservicesmadeeasy', u'Web Services Made Easy Documentation', [u'Christophe de Vienne'], 1) ] + + +autodoc_member_order = 'bysource' diff --git a/doc/protocols.rst b/doc/protocols.rst index 56e97c8..c342811 100644 --- a/doc/protocols.rst +++ b/doc/protocols.rst @@ -1,20 +1,23 @@ Protocols ========= -REST+Json ---------- +:mod:`wsme.protocols.restjson` -- REST+Json +------------------------------------------- -.. autoclass:: wsme.protocols.restjson.RestJsonProtocol +.. automodule:: wsme.protocols.restjson + :members: -REST+XML --------- +:mod:`wsme.protocols.restxml` -- REST+XML +----------------------------------------- -.. autoclass:: wsme.protocols.restxml.RestXmlProtocol +.. automodule:: wsme.protocols.restxml + :members: -SOAP ----- +:mod:`wsme.protocols.soap` -- SOAP +---------------------------------- -.. autoclass:: wsme.protocols.soap.SoapProtocol +.. automodule:: wsme.protocols.soap + :members: diff --git a/wsme/controller.py b/wsme/controller.py index e445822..388a889 100644 --- a/wsme/controller.py +++ b/wsme/controller.py @@ -32,6 +32,10 @@ html_body = """ def scan_api(controller, path=[]): + """ + Recursively iterate a controller api entries, while setting + their :attr:`FunctionDefinition.path`. + """ for name in dir(controller): if name.startswith('_'): continue @@ -50,25 +54,56 @@ def scan_api(controller, path=[]): class FunctionArgument(object): + """ + An argument definition of an api entry + """ def __init__(self, name, datatype, mandatory, default): + #: argument name self.name = name + + #: Data type self.datatype = datatype + + #: True if the argument is mandatory self.mandatory = mandatory + + #: Default value if argument is omitted self.default = default class FunctionDefinition(object): + """ + An api entry definition + """ def __init__(self, func): + #: Function name self.name = func.__name__ + + #: Function documentation self.doc = func.__doc__ + + #: Return type self.return_type = None + + #: The function arguments (list of :class:`FunctionArgument`) self.arguments = [] + + #: True if this function is exposed by a protocol and not in + #: the api tree, which means it is not part of the api. self.protocol_specific = False + + #: Override the contenttype of the returned value. + #: Make sense only with :attr:`protocol_specific` functions. self.contenttype = None + + #: Path of the function in the api tree. self.path = None @classmethod def get(cls, func): + """ + Returns the :class:`FunctionDefinition` of a method. + """ fd = getattr(func, '_wsme_definition', None) if fd is None: fd = FunctionDefinition(func) @@ -76,6 +111,9 @@ class FunctionDefinition(object): return fd def get_arg(self, name): + """ + Returns a :class:`FunctionArgument` from its name + """ for arg in self.arguments: if arg.name == name: return arg @@ -88,6 +126,18 @@ def register_protocol(protocol): class expose(object): + """ + Decorator that expose a function. + + :param return_type: Return type of the function + + Example:: + + class MyController(object): + @expose(int) + def getint(self): + return 1 + """ def __init__(self, return_type=None): self.return_type = return_type register_type(return_type) @@ -113,8 +163,20 @@ class pexpose(object): class validate(object): - def __init__(self, *args, **kw): - self.param_types = args + """ + Decorator that define the arguments types of a function. + + + Example:: + + class MyController(object): + @expose(str) + @validate(datetime.date, datetime.time) + def format(self, d, t): + return d.isoformat() + ' ' + t.isoformat() + """ + def __init__(self, *param_types): + self.param_types = param_types def __call__(self, func): fd = FunctionDefinition.get(func) @@ -133,6 +195,12 @@ class validate(object): class WSRoot(object): + """ + Root controller for webservices. + + :param protocols: A list of protocols to enable (see :meth:`addprotocol`) + :param webpath: The web path where the webservice is published. + """ def __init__(self, protocols=[], webpath=''): self._debug = True self._webpath = webpath @@ -143,12 +211,23 @@ class WSRoot(object): self._api = None def addprotocol(self, protocol): + """ + Enable a new protocol on the controller. + + :param protocol: A registered protocol name or an instance + of a protocol. + """ if isinstance(protocol, str): protocol = registered_protocols[protocol]() self.protocols[protocol.name] = protocol protocol.root = weakref.proxy(self) def getapi(self): + """ + Returns the api description. + + :rtype: list of :class:`FunctionDefinition` + """ if self._api is None: self._api = [i for i in scan_api(self)] return self._api diff --git a/wsme/protocols/restjson.py b/wsme/protocols/restjson.py index 7ac5db6..50cb310 100644 --- a/wsme/protocols/restjson.py +++ b/wsme/protocols/restjson.py @@ -1,3 +1,6 @@ +""" +REST+Json protocol implementation. +""" import base64 import datetime import decimal @@ -16,6 +19,20 @@ except ImportError: @generic def tojson(datatype, value): + """ + A generic converter from python to jsonify-able datatypes. + + If a non-complex user specific type is to be used in the api, + a specific tojson should be added:: + + from wsme.protocol.restjson import tojson + + myspecialtype = object() + + @tojson.when_object(myspecialtype) + def myspecialtype_tojson(datatype, value): + return str(value) + """ if wsme.types.iscomplex(datatype): d = dict() for name, attr in wsme.types.list_attributes(datatype): @@ -56,6 +73,21 @@ def datetime_tojson(datatype, value): @generic def fromjson(datatype, value): + """ + A generic converter from json base types to python datatype. + + If a non-complex user specific type is to be used in the api, + a specific fromjson should be added:: + + from wsme.protocol.restjson import fromjson + + class MySpecialType(object): + pass + + @fromjson.when_object(MySpecialType) + def myspecialtype_fromjson(datatype, value): + return MySpecialType(value) + """ if value is None: return None if wsme.types.iscomplex(datatype): @@ -98,6 +130,14 @@ def binary_fromjson(datatype, value): class RestJsonProtocol(RestProtocol): + """ + REST+Json protocol. + + .. autoattribute:: name + .. autoattribute:: dataformat + .. autoattribute:: content_types + """ + name = 'REST+Json' dataformat = 'json' content_types = [ diff --git a/wsme/protocols/restxml.py b/wsme/protocols/restxml.py index 32146e4..b1aa924 100644 --- a/wsme/protocols/restxml.py +++ b/wsme/protocols/restxml.py @@ -20,6 +20,25 @@ time_re = re.compile(r'(?P[0-2][0-9]):(?P[0-5][0-9]):(?P[0-6][0-9])') @generic def toxml(datatype, key, value): + """ + A generic converter from python to xml elements. + + If a non-complex user specific type is to be used in the api, + a specific toxml should be added:: + + from wsme.protocol.restxml import toxml + + myspecialtype = object() + + @toxml.when_object(myspecialtype) + def myspecialtype_toxml(datatype, key, value): + el = et.Element(key) + if value is None: + el.set('nil', 'true') + else: + el.text = str(value) + return el + """ el = et.Element(key) if value is None: el.set('nil', 'true') @@ -34,6 +53,23 @@ def toxml(datatype, key, value): @generic def fromxml(datatype, element): + """ + A generic converter from xml elements to python datatype. + + If a non-complex user specific type is to be used in the api, + a specific fromxml should be added:: + + from wsme.protocol.restxml import fromxml + + class MySpecialType(object): + pass + + @fromxml.when_object(MySpecialType) + def myspecialtype_fromxml(datatype, element): + if element.get('nil', False): + return None + return MySpecialType(element.text) + """ if element.get('nil', False): return None if wsme.types.iscomplex(datatype): @@ -130,6 +166,13 @@ def binary_fromxml(datatype, element): class RestXmlProtocol(RestProtocol): + """ + REST+XML protocol. + + .. autoattribute:: name + .. autoattribute:: dataformat + .. autoattribute:: content_types + """ name = 'REST+XML' dataformat = 'xml' content_types = ['text/xml'] diff --git a/wsme/protocols/soap.py b/wsme/protocols/soap.py index d26ada5..67de492 100644 --- a/wsme/protocols/soap.py +++ b/wsme/protocols/soap.py @@ -95,7 +95,22 @@ def make_soap_element(datatype, tag, value): @generic def tosoap(datatype, tag, value): """Converts a value into xml Element objects for inclusion in the SOAP - response output""" + 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] = 'xsd:MySpecialType' + + @tosoap.when_object(MySpecialType) + def myspecialtype_tosoap(datatype, tag, value): + return make_soap_element(datatype, tag, str(value)) + """ return make_soap_element(datatype, tag, value) @@ -130,6 +145,12 @@ def None_tosoap(datatype, tag, value): @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 soaptype = el.get(type_qn) @@ -188,6 +209,13 @@ def binary_fromsoap(datatype, el, ns): class SoapProtocol(object): + """ + REST+XML protocol. + + .. autoattribute:: name + .. autoattribute:: dataformat + .. autoattribute:: content_types + """ name = 'SOAP' content_types = ['application/soap+xml'] diff --git a/wsme/types.py b/wsme/types.py index 57c6e41..57b6e88 100644 --- a/wsme/types.py +++ b/wsme/types.py @@ -19,6 +19,20 @@ def iscomplex(datatype): class wsproperty(property): + """ + A specialised :class:`property` to define typed-property on complex types. + Example:: + + class MyComplexType(object): + def get_aint(self): + return self._aint + + def set_aint(self, value): + assert avalue < 10 # Dummy input validation + self._aint = value + + aint = wsproperty(int, get_aint, set_aint, mandatory=True) + """ def __init__(self, datatype, fget, fset=None, mandatory=False, doc=None): property.__init__(self, fget, fset) @@ -27,6 +41,16 @@ class wsproperty(property): class wsattr(object): + """ + Complex type attribute definition when the datatype only is not + enough. + Example:: + + class MyComplexType(object): + optionalvalue = int + mandatoryvalue = wsattr(int, mandatory=True) + + """ def __init__(self, datatype, mandatory=False): self.datatype = datatype self.mandatory = mandatory @@ -100,6 +124,14 @@ def inspect_class(class_): def register_type(class_): + """ + Make sure a type is registered. + + It is automatically called by :class:`expose() ` + and :class:`validate() `. + Unless you want to control when the class inspection is done there + is no need to call it. + """ if class_ is None or \ class_ in native_types or \ hasattr(class_, '_wsme_attributes'): @@ -118,6 +150,9 @@ def register_type(class_): def list_attributes(class_): + """ + Returns a list of a complex type attributes. + """ if not hasattr(class_, '_wsme_attributes'): register_type(class_) return class_._wsme_attributes