Skip to content
This repository

changes for rpclib-2.5.1 #117

Merged
merged 17 commits into from over 2 years ago

1 participant

Burak Arslan
This page is out of date. Refresh to see the latest.
8  CHANGELOG.rst
Source Rendered
@@ -2,6 +2,14 @@
2 2
 Changelog
3 3
 =========
4 4
 
  5
+rpclib-2.5.1-beta
  6
+-----------------
  7
+ * Switched to magic cookie constants instead of strings in protocol logic.
  8
+ * check_validator -> set_validator in ProtocolBase
  9
+ * Started parsing Http headers in HttpRpc protocol.
  10
+ * HttpRpc now properly validates nested value frequencies.
  11
+ * HttpRpc now works with arrays of simple types as well.
  12
+
5 13
 rpclib-2.5.0-beta
6 14
 -----------------
7 15
  * Implemented fanout support for transports and protocols that can handle 
36  doc/source/faq.rst
Source Rendered
@@ -85,3 +85,39 @@ method from running.
85 85
 Note that this property is initialized only when the process starts. So you
86 86
 should call :func:`ctx.descriptor.reset_function()` to restore it to its
87 87
 original value
  88
+
  89
+How do I use variable names that are also python keywords?
  90
+==========================================================
  91
+
  92
+Due to restrictions of the python language, you can't do this:
  93
+
  94
+    class SomeClass(ComplexModel):
  95
+        and = String
  96
+        or = Integer
  97
+        import = Datetime
  98
+
  99
+The workaround is as follows:
  100
+
  101
+    class SomeClass(ComplexModel):
  102
+        _type_info = {
  103
+            'and': String
  104
+            'or': Integer
  105
+            'import': Datetime
  106
+        }
  107
+
  108
+You also can't do this:
  109
+
  110
+    @rpc(String, String, String, _returns=String)
  111
+    def f(ctx, from, import):
  112
+        return '1234'
  113
+
  114
+The workaround is as follows:
  115
+
  116
+    @rpc(String, String, String, _returns=String,
  117
+        _in_variable_names={'_from': 'from',
  118
+            '_import': 'import'},
  119
+        _out_variable_name="return"
  120
+    def f(ctx, _from, _import):
  121
+        return '1234'
  122
+
  123
+See here: https://github.com/arskom/rpclib/blob/rpclib-2.5.0-beta/src/rpclib/test/test_service.py#L114
27  examples/events.py
@@ -55,11 +55,17 @@
55 55
 management, logging and measuring performance. This example also
56 56
 uses the user-defined context (udc) attribute of the MethodContext object
57 57
 to hold the data points for this request.
  58
+
  59
+You may notice that one construction of MethodContext instance is followed by
  60
+two destructions. This is because the fanout code creates shallow copies of
  61
+the context instance in an early stage of the method processing pipeline. As the
  62
+python's shallow-copying operator does not let us customize copy constructor,
  63
+it's not possible to cleanly log this event.
58 64
 '''
59 65
 
60 66
 class UserDefinedContext(object):
61 67
     def __init__(self):
62  
-        self.call_start = None
  68
+        self.call_start = time()
63 69
         self.call_end = None
64 70
         self.method_start = None
65 71
         self.method_end = None
@@ -77,7 +83,6 @@ def say_hello(name, times):
77 83
 def _on_wsgi_call(ctx):
78 84
     print("_on_wsgi_call")
79 85
     ctx.udc = UserDefinedContext()
80  
-    ctx.udc.call_start = time()
81 86
 
82 87
 def _on_method_call(ctx):
83 88
     print("_on_method_call")
@@ -90,10 +95,19 @@ def _on_method_return_object(ctx):
90 95
 def _on_wsgi_return(ctx):
91 96
     print("_on_wsgi_return")
92 97
     call_end = time()
93  
-    print('Method took [%s] - total execution time[%s]'% (
  98
+    print('Method took [%0.8f] - total execution time[%0.8f]'% (
94 99
         ctx.udc.method_end - ctx.udc.method_start,
95 100
         call_end - ctx.udc.call_start))
96 101
 
  102
+def _on_method_context_destroyed(ctx):
  103
+    print("_on_method_context_destroyed")
  104
+    print('MethodContext(%d) lived for [%0.8f] seconds' % (id(ctx),
  105
+                                                ctx.call_end - ctx.call_start))
  106
+def _on_method_context_constructed(ctx):
  107
+    print("_on_method_context_constructed")
  108
+    print('Hello, this is MethodContext(%d). Time now: %0.8f' % (id(ctx),
  109
+                                                                ctx.call_start))
  110
+
97 111
 if __name__=='__main__':
98 112
     try:
99 113
         from wsgiref.simple_server import make_server
@@ -107,7 +121,12 @@ def _on_wsgi_return(ctx):
107 121
                 interface=Wsdl11(), in_protocol=Soap11(), out_protocol=Soap11())
108 122
 
109 123
     application.event_manager.add_listener('method_call', _on_method_call)
110  
-    application.event_manager.add_listener('method_return_object', _on_method_return_object)
  124
+    application.event_manager.add_listener('method_return_object',
  125
+                                                _on_method_return_object)
  126
+    application.event_manager.add_listener('method_context_constructed',
  127
+                                                _on_method_context_constructed)
  128
+    application.event_manager.add_listener('method_context_destroyed',
  129
+                                                _on_method_context_destroyed)
111 130
 
112 131
     wsgi_wrapper = WsgiApplication(application)
113 132
     wsgi_wrapper.event_manager.add_listener('wsgi_call', _on_wsgi_call)
2  src/rpclib/__init__.py
@@ -17,7 +17,7 @@
17 17
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
18 18
 #
19 19
 
20  
-__version__ = '2.5.0-beta'
  20
+__version__ = '2.5.1-beta'
21 21
 
22 22
 from rpclib._base import TransportContext
23 23
 from rpclib._base import EventContext
26  src/rpclib/_base.py
@@ -20,6 +20,8 @@
20 20
 import logging
21 21
 logger = logging.getLogger(__name__)
22 22
 
  23
+from time import time
  24
+
23 25
 from collections import deque
24 26
 
25 27
 from rpclib.const.xml_ns import DEFAULT_NS
@@ -54,6 +56,19 @@ def method_name(self):
54 56
             return self.descriptor.name
55 57
 
56 58
     def __init__(self, app):
  59
+        # metadata
  60
+        self.call_start = time()
  61
+        """The time the rpc operation was initiated in seconds-since-epoch
  62
+        format.
  63
+
  64
+        Useful for benchmarking purposes."""
  65
+
  66
+        self.call_end = None
  67
+        """The time the rpc operation was completed in seconds-since-epoch
  68
+        format.
  69
+
  70
+        Useful for benchmarking purposes."""
  71
+
57 72
         self.app = app
58 73
         """The parent application."""
59 74
 
@@ -146,10 +161,10 @@ def __init__(self, app):
146 161
         """
147 162
         self.out_header = None
148 163
         """Native python object set by the function in the service definition
149  
-        class"""
  164
+        class."""
150 165
         self.out_error = None
151 166
         """Native exception thrown by the function in the service definition
152  
-        class"""
  167
+        class."""
153 168
 
154 169
         # parsed
155 170
         self.out_body_doc = None
@@ -164,10 +179,12 @@ def __init__(self, app):
164 179
         """Outgoing bytestream (i.e. an iterable of strings)"""
165 180
 
166 181
         self.frozen = True
167  
-        """when this is set, no new attribute can be added to this class
  182
+        """When this is set, no new attribute can be added to this class
168 183
         instance. This is mostly for internal use.
169 184
         """
170 185
 
  186
+        self.app.event_manager.fire_event("method_context_constructed", self)
  187
+
171 188
     def __setattr__(self, k, v):
172 189
         if self.frozen == False or k in self.__dict__:
173 190
             object.__setattr__(self, k, v)
@@ -193,6 +210,9 @@ def __repr__(self):
193 210
 
194 211
         return ''.join((self.__class__.__name__, '(', ', '.join(retval), ')'))
195 212
 
  213
+    def __del__(self):
  214
+        self.call_end = time()
  215
+        self.app.event_manager.fire_event("method_context_destroyed", self)
196 216
 
197 217
 class MethodDescriptor(object):
198 218
     '''This class represents the method signature of a soap method,
6  src/rpclib/application.py
@@ -57,6 +57,12 @@ class Application(object):
57 57
         * method_exception_object
58 58
             Called when an exception occurred in a service method, before the
59 59
             exception is serialized.
  60
+
  61
+        * method_context_constructed
  62
+            Called from the constructor of the MethodContext instance.
  63
+
  64
+        * method_context_destroyed
  65
+            Called from the destructor of the MethodContext instance.
60 66
     '''
61 67
 
62 68
     transport = None
12  src/rpclib/client/_base.py
@@ -103,7 +103,7 @@ def get_out_string(self, ctx):
103 103
         assert ctx.out_document is None
104 104
         assert ctx.out_string is None
105 105
 
106  
-        self.app.out_protocol.serialize(ctx, message='request')
  106
+        self.app.out_protocol.serialize(ctx, self.app.out_protocol.REQUEST)
107 107
 
108 108
         if ctx.service_class != None:
109 109
             if ctx.out_error is None:
@@ -127,8 +127,8 @@ def get_out_string(self, ctx):
127 127
             ctx.out_string = [""]
128 128
 
129 129
     def get_in_object(self, ctx):
130  
-        """Deserializes the response bytestream to input document and native
131  
-        python object.
  130
+        """Deserializes the response bytestream first as a document and then
  131
+        as a native python object.
132 132
         """
133 133
 
134 134
         assert ctx.in_string is not None
@@ -140,10 +140,12 @@ def get_in_object(self, ctx):
140 140
                                             'method_accept_document', ctx)
141 141
 
142 142
         # sets the ctx.in_body_doc and ctx.in_header_doc properties
143  
-        self.app.in_protocol.decompose_incoming_envelope(ctx, message='response')
  143
+        self.app.in_protocol.decompose_incoming_envelope(ctx,
  144
+                                        message=self.app.in_protocol.RESPONSE)
144 145
 
145 146
         # this sets ctx.in_object
146  
-        self.app.in_protocol.deserialize(ctx, message='response')
  147
+        self.app.in_protocol.deserialize(ctx,
  148
+                                        message=self.app.in_protocol.RESPONSE)
147 149
 
148 150
         type_info = ctx.descriptor.out_message._type_info
149 151
 
61  src/rpclib/model/complex.py
@@ -31,6 +31,15 @@
31 31
 from rpclib.util.odict import odict as TypeInfo
32 32
 from rpclib.const import xml_ns as namespace
33 33
 
  34
+
  35
+class _SimpleTypeInfoElement(object):
  36
+    __slots__ = ['path', 'parent', 'type']
  37
+    def __init__(self, path, parent, type_):
  38
+        self.path = path
  39
+        self.parent = parent
  40
+        self.type = type_
  41
+
  42
+
34 43
 class XmlAttribute(ModelBase):
35 44
     """Items which are marshalled as attributes of the parent element."""
36 45
 
@@ -155,17 +164,22 @@ def __getitem__(self, i):
155 164
         return getattr(self, self._type_info.keys()[i], None)
156 165
 
157 166
     def __repr__(self):
158  
-        return "%s(%r)" % (self.get_type_name(),
159  
-                           ['%s=%s' % (k, getattr(self, k, None))
160  
-                           for k in self.__class__._type_info])
  167
+        return "%s(%s)" % (self.get_type_name(), ', '.join(
  168
+                           ['%s=%r' % (k, getattr(self, k, None))
  169
+                                            for k in self.__class__._type_info]))
161 170
 
162 171
     @classmethod
163 172
     def get_serialization_instance(cls, value):
  173
+        """The value argument can be:
  174
+            * A list of native types aligned with cls._type_info.
  175
+            * A dict of native types
  176
+            * The native type itself.
  177
+        """
164 178
         # if the instance is a list, convert it to a cls instance.
165  
-        # this is only useful when deserializing method arguments which is the
166  
-        # only time when the member order is not arbitrary (as the members
167  
-        # are declared and passed around as sequences of arguments, unlike
168  
-        # dictionaries in a regular class definition).
  179
+        # this is only useful when deserializing method arguments for a client
  180
+        # request which is the only time when the member order is not arbitrary
  181
+        # (as the members are declared and passed around as sequences of
  182
+        # arguments, unlike dictionaries in a regular class definition).
169 183
         if isinstance(value, list) or isinstance(value, tuple):
170 184
             assert len(value) <= len(cls._type_info)
171 185
 
@@ -188,6 +202,9 @@ def get_serialization_instance(cls, value):
188 202
 
189 203
     @classmethod
190 204
     def get_deserialization_instance(cls):
  205
+        """Get an empty native type so that the deserialization logic can set
  206
+        its attributes.
  207
+        """
191 208
         return cls()
192 209
 
193 210
     @classmethod
@@ -232,7 +249,7 @@ def get_flat_type_info(cls, retval=None):
232 249
         return retval
233 250
 
234 251
     @staticmethod
235  
-    def get_simple_type_info(cls, retval=None, prefix=None):
  252
+    def get_simple_type_info(cls, retval=None, prefix=None, parent=None):
236 253
         """Returns a _type_info dict that includes members from all base classes
237 254
         and whose types are only primitives.
238 255
         """
@@ -241,23 +258,28 @@ def get_simple_type_info(cls, retval=None, prefix=None):
241 258
 
242 259
         if retval is None:
243 260
             retval = {}
244  
-
245  
-        if prefix:
246  
-            prefix += "_"
247  
-        else:
248  
-            prefix = ""
  261
+        if prefix is None:
  262
+            prefix = []
249 263
 
250 264
         fti = cls.get_flat_type_info(cls)
251 265
         for k, v in fti.items():
252 266
             if getattr(v, 'get_flat_type_info', None) is None:
253  
-                key = prefix + k
  267
+                new_prefix = list(prefix)
  268
+                new_prefix.append(k)
  269
+                key = '_'.join(new_prefix)
254 270
                 value = retval.get(key, None)
  271
+
255 272
                 if value:
256 273
                     raise ValueError("%r.%s conflicts with %r" % (cls, k, value))
  274
+
257 275
                 else:
258  
-                    retval[key] = v
  276
+                    retval[key] = _SimpleTypeInfoElement(
  277
+                                        path=tuple(new_prefix), parent=parent, type_=v)
  278
+
259 279
             else:
260  
-                v.get_simple_type_info(v, retval, k)
  280
+                new_prefix = list(prefix)
  281
+                new_prefix.append(k)
  282
+                v.get_simple_type_info(v, retval, new_prefix, parent=cls)
261 283
 
262 284
         return retval
263 285
 
@@ -331,7 +353,7 @@ class Array(ComplexModel):
331 353
     """
332 354
 
333 355
     def __new__(cls, serializer, ** kwargs):
334  
-        retval = cls.customize( ** kwargs)
  356
+        retval = cls.customize(**kwargs)
335 357
 
336 358
         # hack to default to unbounded arrays when the user didn't specify
337 359
         # max_occurs. We should find a better way.
@@ -378,6 +400,11 @@ def get_serialization_instance(cls, value):
378 400
 
379 401
         return inst
380 402
 
  403
+    @classmethod
  404
+    def get_deserialization_instance(cls):
  405
+        return []
  406
+
  407
+
381 408
 class Iterable(Array):
382 409
     """This class generates a ComplexModel child that has one attribute that has
383 410
     the same name as the serialized class. It's contained in a Python iterable.
19  src/rpclib/protocol/_base.py
@@ -36,6 +36,7 @@
36 36
 from rpclib.const.http import HTTP_404
37 37
 from rpclib.const.http import HTTP_413
38 38
 from rpclib.const.http import HTTP_500
  39
+
39 40
 from rpclib.error import ResourceNotFoundError
40 41
 from rpclib.error import RequestTooLongError
41 42
 from rpclib.error import Fault
@@ -62,13 +63,16 @@ class ProtocolBase(object):
62 63
     allowed_http_verbs = ['GET', 'POST']
63 64
     mime_type = 'application/octet-stream'
64 65
 
  66
+    SOFT_VALIDATION = type("soft", (object,), {})
  67
+    REQUEST = type("request", (object,), {})
  68
+    RESPONSE = type("response", (object,), {})
  69
+
65 70
     def __init__(self, app=None, validator=None):
66 71
         self.__app = None
67 72
 
68 73
         self.set_app(app)
69 74
         self.event_manager = EventManager(self)
70  
-        self.validator = validator
71  
-        self.check_validator()
  75
+        self.set_validator(validator)
72 76
 
73 77
     @property
74 78
     def app(self):
@@ -155,6 +159,8 @@ def generate_method_contexts(self, ctx):
155 159
         for sc, d in call_handles:
156 160
             c = copy(ctx)
157 161
 
  162
+            assert d != None
  163
+
158 164
             c.descriptor = d
159 165
             c.service_class = sc
160 166
 
@@ -167,13 +173,16 @@ def fault_to_http_response_code(self, fault):
167 173
             return HTTP_413
168 174
         if isinstance(fault, ResourceNotFoundError):
169 175
             return HTTP_404
170  
-        if isinstance(fault, Fault) and fault.faultcode.startswith('Client.'):
  176
+        if isinstance(fault, Fault) and (fault.faultcode.startswith('Client.')
  177
+                                                or fault.faultcode == 'Client'):
171 178
             return HTTP_400
172 179
         else:
173 180
             return HTTP_500
174 181
 
175  
-    def check_validator(self):
  182
+    def set_validator(self, validator):
176 183
         """You must override this function if your protocol supports validation.
177 184
         """
178 185
 
179  
-        assert self.validator is None
  186
+        assert validator is None
  187
+
  188
+        self.validator = None
230  src/rpclib/protocol/http.py
@@ -17,10 +17,10 @@
17 17
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
18 18
 #
19 19
 
20  
-"""This module contains the HttpRpc protocol implementation."""
  20
+"""This module contains the HttpRpc protocol implementation. This is not exactly
  21
+Rest, because it ignores Http verbs.
  22
+"""
21 23
 
22  
-from rpclib.error import Fault
23  
-from rpclib.error import ValidationError
24 24
 import logging
25 25
 logger = logging.getLogger(__name__)
26 26
 
@@ -29,28 +29,35 @@
29 29
 except ImportError: # Python 3
30 30
     from urllib.parse import parse_qs
31 31
 
  32
+from rpclib.error import ValidationError
  33
+from rpclib.model.complex import Array
  34
+from rpclib.model.fault import Fault
32 35
 from rpclib.protocol import ProtocolBase
33 36
 
34  
-# this is not exactly ReST, because it ignores http verbs.
35  
-
36 37
 def _get_http_headers(req_env):
37 38
     retval = {}
38 39
 
39 40
     for k, v in req_env.items():
40 41
         if k.startswith("HTTP_"):
41  
-            retval[k[5:].lower()]= v
  42
+            retval[k[5:].lower()]= [v]
42 43
 
43 44
     return retval
44 45
 
45 46
 class HttpRpc(ProtocolBase):
46  
-    """The so-called ReST-minus-the-verbs HttpRpc protocol implementation.
  47
+    """The so-called REST-minus-the-verbs HttpRpc protocol implementation.
47 48
     It only works with the http server (wsgi) transport.
48 49
 
49  
-    It only parses GET requests where the whole data is in the 'QUERY_STRING'.
  50
+    It only parses requests where the whole data is in the 'QUERY_STRING', i.e.
  51
+    the part after '?' character in a URI string.
50 52
     """
51 53
 
52  
-    def check_validator(self):
53  
-        assert self.validator in ('soft', None)
  54
+    def set_validator(self, validator):
  55
+        if validator == 'soft' or validator is self.SOFT_VALIDATION:
  56
+            self.validator = self.SOFT_VALIDATION
  57
+        elif validator is None:
  58
+            self.validator = None
  59
+        else:
  60
+            raise ValueError(validator)
54 61
 
55 62
     def create_in_document(self, ctx, in_string_encoding=None):
56 63
         assert ctx.transport.type == 'wsgi', ("This protocol only works with "
@@ -66,81 +73,148 @@ def decompose_incoming_envelope(self, ctx):
66 73
         ctx.in_header_doc = _get_http_headers(ctx.in_document)
67 74
         ctx.in_body_doc = parse_qs(ctx.in_document['QUERY_STRING'])
68 75
 
69  
-        logger.debug(repr(ctx.in_body_doc))
  76
+        logger.debug('\theader : %r' % (ctx.in_header_doc))
  77
+        logger.debug('\tbody   : %r' % (ctx.in_body_doc))
  78
+
  79
+    def dict_to_object(self, doc, inst_class):
  80
+        simple_type_info = inst_class.get_simple_type_info(inst_class)
  81
+        inst = inst_class.get_deserialization_instance()
  82
+
  83
+        # this is for validating cls.Attributes.{min,max}_occurs
  84
+        frequencies = {}
  85
+
  86
+        for k, v in doc.items():
  87
+            member = simple_type_info.get(k, None)
  88
+            if member is None:
  89
+                logger.debug("discarding field %r" % k)
  90
+                continue
  91
+
  92
+            mo = member.type.Attributes.max_occurs
  93
+            value = getattr(inst, k, None)
  94
+            if value is None:
  95
+                value = []
  96
+            elif mo == 1:
  97
+                raise Fault('Client.ValidationError',
  98
+                        '"%s" member must occur at most %d times' % (k, max_o))
  99
+
  100
+            for v2 in v:
  101
+                if (self.validator is self.SOFT_VALIDATION and not
  102
+                            member.type.validate_string(member.type, v2)):
  103
+                    raise ValidationError(v2)
  104
+                native_v2 = member.type.from_string(v2)
  105
+                if (self.validator is self.SOFT_VALIDATION and not
  106
+                            member.type.validate_native(member.type, native_v2)):
  107
+                    raise ValidationError(v2)
  108
+
  109
+                value.append(native_v2)
  110
+
  111
+                # set frequencies of parents.
  112
+                if not (member.path[:-1] in frequencies):
  113
+                    for i in range(1,len(member.path)):
  114
+                        logger.debug("\tset freq %r = 1" % (member.path[:i],))
  115
+                        frequencies[member.path[:i]] = 1
  116
+
  117
+                freq = frequencies.get(member.path, 0)
  118
+                freq += 1
  119
+                frequencies[member.path] = freq
  120
+                logger.debug("\tset freq %r = %d" % (member.path, freq))
  121
+
  122
+            if mo == 1:
  123
+                value = value[0]
  124
+
  125
+            cinst = inst
  126
+            ctype_info = inst_class.get_flat_type_info(inst_class)
  127
+            pkey = member.path[0]
  128
+            for i in range(len(member.path) - 1):
  129
+                pkey = member.path[i]
  130
+                if not (ctype_info[pkey].Attributes.max_occurs in (0,1)):
  131
+                    raise Exception("HttpRpc deserializer does not support "
  132
+                                    "non-primitives with max_occurs > 1")
  133
+
  134
+                ninst = getattr(cinst, pkey, None)
  135
+                if ninst is None:
  136
+                    ninst = ctype_info[pkey].get_deserialization_instance()
  137
+                    setattr(cinst, pkey, ninst)
  138
+                cinst = ninst
  139
+
  140
+                ctype_info = ctype_info[pkey]._type_info
  141
+
  142
+            if isinstance(cinst, list):
  143
+                cinst.extend(value)
  144
+                logger.debug("\tset array   %r(%r) = %r" %
  145
+                                                    (member.path, pkey, value))
  146
+            else:
  147
+                setattr(cinst, member.path[-1], value)
  148
+                logger.debug("\tset default %r(%r) = %r" %
  149
+                                                    (member.path, pkey, value))
  150
+
  151
+        try:
  152
+            print inst.qo.vehicles
  153
+        except Exception,e:
  154
+            print e
  155
+
  156
+        if self.validator is self.SOFT_VALIDATION:
  157
+            sti = simple_type_info.values()
  158
+            sti.sort(key=lambda x: (len(x.path), x.path))
  159
+            pfrag = None
  160
+            for s in sti:
  161
+                if len(s.path) > 1 and pfrag != s.path[:-1]:
  162
+                    pfrag = s.path[:-1]
  163
+                    ctype_info = inst_class.get_flat_type_info(inst_class)
  164
+                    for i in range(len(pfrag)):
  165
+                        f = pfrag[i]
  166
+                        ntype_info = ctype_info[f]
  167
+
  168
+                        min_o = ctype_info[f].Attributes.min_occurs
  169
+                        max_o = ctype_info[f].Attributes.max_occurs
  170
+                        val = frequencies.get(pfrag[:i+1], 0)
  171
+                        if val < min_o:
  172
+                            raise Fault('Client.ValidationError',
  173
+                                '"%s" member must occur at least %d times'
  174
+                                              % ('_'.join(pfrag[:i+1]), min_o))
  175
+
  176
+                        if max_o != 'unbounded' and val > max_o:
  177
+                            raise Fault('Client.ValidationError',
  178
+                                '"%s" member must occur at most %d times'
  179
+                                             % ('_'.join(pfrag[:i+1]), max_o))
  180
+
  181
+                        ctype_info = ntype_info.get_flat_type_info(ntype_info)
  182
+
  183
+                val = frequencies.get(s.path, 0)
  184
+                min_o = s.type.Attributes.min_occurs
  185
+                max_o = s.type.Attributes.max_occurs
  186
+                if val < min_o:
  187
+                    raise Fault('Client.ValidationError',
  188
+                                '"%s" member must occur at least %d times'
  189
+                                                    % ('_'.join(s.path), min_o))
  190
+                if max_o != 'unbounded' and val > max_o:
  191
+                    raise Fault('Client.ValidationError',
  192
+                                '"%s" member must occur at most %d times'
  193
+                                                    % ('_'.join(s.path), max_o))
  194
+
  195
+        return inst
70 196
 
71 197
     def deserialize(self, ctx, message):
72  
-        assert message in ('request',)
  198
+        assert message in (self.REQUEST,)
73 199
 
74 200
         self.event_manager.fire_event('before_deserialize', ctx)
75 201
 
76  
-        body_class = ctx.descriptor.in_message
77  
-        flat_type_info = body_class.get_flat_type_info(body_class)
78  
-
79  
-        if ctx.in_body_doc is not None and len(ctx.in_body_doc) > 0:
80  
-            inst = body_class.get_deserialization_instance()
81  
-
82  
-            # this is for validating cls.Attributes.{min,max}_occurs
83  
-            frequencies = {}
84  
-
85  
-            for k, v in ctx.in_body_doc.items():
86  
-                member = flat_type_info.get(k, None)
87  
-                if member is None:
88  
-                    continue
  202
+        if ctx.in_header_doc is not None:
  203
+            ctx.in_header = self.dict_to_object(ctx.in_header_doc,
  204
+                                                    ctx.descriptor.in_header)
89 205
 
90  
-                mo = member.Attributes.max_occurs
91  
-                if mo == 'unbounded' or mo > 1:
92  
-                    value = getattr(inst, k, None)
93  
-                    if value is None:
94  
-                        value = []
95  
-
96  
-                    for v2 in v:
97  
-                        if self.validator == 'soft' and not member.validate_string(member, v2):
98  
-                            raise ValidationError(v2)
99  
-                        native_v2 = member.from_string(v2)
100  
-                        if self.validator == 'soft' and not member.validate_native(member, native_v2):
101  
-                            raise ValidationError(v2)
102  
-
103  
-                        value.append(native_v2)
104  
-                        freq = frequencies.get(k, 0)
105  
-                        freq += 1
106  
-                        frequencies[k] = freq
107  
-
108  
-                    setattr(inst, k, value)
109  
-
110  
-                else:
111  
-                    v,  = v
112  
-                    if self.validator == 'soft' and not member.validate_string(member, v):
113  
-                        raise ValidationError(v)
114  
-                    native_v = member.from_string(v)
115  
-                    if self.validator == 'soft' and not member.validate_native(member, native_v):
116  
-                        raise ValidationError(native_v)
117  
-
118  
-                    if native_v is None:
119  
-                        setattr(inst, k, member.Attributes.default)
120  
-                    else:
121  
-                        setattr(inst, k, native_v)
122  
-
123  
-                    freq = frequencies.get(k, 0)
124  
-                    freq += 1
125  
-                    frequencies[k] = freq
126  
-
127  
-            if self.validator == 'soft':
128  
-                for k, c in flat_type_info.items():
129  
-                    val = frequencies.get(k, 0)
130  
-                    if val < c.Attributes.min_occurs \
131  
-                            or  (c.Attributes.max_occurs != 'unbounded'
132  
-                                            and val > c.Attributes.max_occurs ):
133  
-                        raise Fault('Client.ValidationError',
134  
-                            '%r member does not respect frequency constraints' % k)
135  
-
136  
-            ctx.in_object = inst
  206
+        if ctx.in_body_doc is not None:
  207
+            ctx.in_object = self.dict_to_object(ctx.in_body_doc,
  208
+                                                    ctx.descriptor.in_message)
137 209
         else:
138  
-            ctx.in_object = [None] * len(flat_type_info)
  210
+            ctx.in_object = [None] * len(
  211
+                        ctx.descriptor.in_message.get_flat_type_info(
  212
+                                                    ctx.descriptor.in_message))
139 213
 
140 214
         self.event_manager.fire_event('after_deserialize', ctx)
141 215
 
142 216
     def serialize(self, ctx, message):
143  
-        assert message in ('response',)
  217
+        assert message in (self.RESPONSE,)
144 218
 
145 219
         if ctx.out_error is None:
146 220
             result_message_class = ctx.descriptor.out_message
@@ -152,10 +226,14 @@ def serialize(self, ctx, message):
152 226
                 if ctx.out_object is None:
153 227
                     ctx.out_document = ['']
154 228
                 else:
155  
-                    ctx.out_document = out_class.to_string_iterable(ctx.out_object[0])
  229
+                    if hasattr(out_class, 'to_string_iterable'):
  230
+                        ctx.out_document = out_class.to_string_iterable(ctx.out_object[0])
  231
+                    else:
  232
+                        raise ValueError("HttpRpc protocol can only serialize primitives. %r" % out_class)
156 233
             else:
157  
-                raise ValueError("HttpRpc protocol can only serialize primitives.")
  234
+                raise ValueError("HttpRpc protocol can only serialize simple return values.")
158 235
         else:
  236
+            ctx.transport.mime_type = 'text/plain'
159 237
             ctx.out_document = ctx.out_error.to_string_iterable(ctx.out_error)
160 238
 
161 239
         self.event_manager.fire_event('serialize', ctx)
18  src/rpclib/protocol/soap/soap11.py
@@ -150,7 +150,7 @@ def create_in_document(self, ctx, charset=None):
150 150
 
151 151
         ctx.in_document = _parse_xml_string(ctx.in_string, charset)
152 152
 
153  
-    def decompose_incoming_envelope(self, ctx, message='request'):
  153
+    def decompose_incoming_envelope(self, ctx, message=XmlObject.REQUEST):
154 154
         envelope_xml, xmlids = ctx.in_document
155 155
         header_document, body_document = _from_soap(envelope_xml, xmlids)
156 156
 
@@ -173,7 +173,7 @@ def deserialize(self, ctx, message):
173 173
         Not meant to be overridden.
174 174
         """
175 175
 
176  
-        assert message in ('request', 'response')
  176
+        assert message in (self.REQUEST, self.RESPONSE)
177 177
 
178 178
         self.event_manager.fire_event('before_deserialize', ctx)
179 179
 
@@ -182,11 +182,11 @@ def deserialize(self, ctx, message):
182 182
             ctx.in_error = self.from_element(Fault, ctx.in_body_doc)
183 183
 
184 184
         else:
185  
-            if message == 'request':
  185
+            if message is self.REQUEST:
186 186
                 header_class = ctx.descriptor.in_header
187 187
                 body_class = ctx.descriptor.in_message
188 188
 
189  
-            elif message == 'response':
  189
+            elif message is self.RESPONSE:
190 190
                 header_class = ctx.descriptor.out_header
191 191
                 body_class = ctx.descriptor.out_message
192 192
 
@@ -221,7 +221,7 @@ def serialize(self, ctx, message):
221 221
         Not meant to be overridden.
222 222
         """
223 223
 
224  
-        assert message in ('request', 'response')
  224
+        assert message in (self.REQUEST, self.RESPONSE)
225 225
 
226 226
         self.event_manager.fire_event('before_serialize', ctx)
227 227
 
@@ -237,11 +237,11 @@ def serialize(self, ctx, message):
237 237
                                     self.app.interface.get_tns(), out_body_doc)
238 238
 
239 239
         else:
240  
-            if message == 'request':
  240
+            if message is self.REQUEST:
241 241
                 header_message_class = ctx.descriptor.in_header
242 242
                 body_message_class = ctx.descriptor.in_message
243 243
 
244  
-            elif message == 'response':
  244
+            elif message is self.RESPONSE:
245 245
                 header_message_class = ctx.descriptor.out_header
246 246
                 body_message_class = ctx.descriptor.out_message
247 247
 
@@ -305,9 +305,9 @@ def serialize(self, ctx, message):
305 305
                                     self.app.interface.get_tns(), out_body_doc)
306 306
 
307 307
         if self.log_messages:
308  
-            if message == 'request':
  308
+            if message is self.REQUEST:
309 309
                 line_header = '%sRequest%s' % (LIGHT_GREEN, END_COLOR)
310  
-            elif message == 'response':
  310
+            elif message is self.RESPONSE:
311 311
                 line_header = '%sResponse%s' % (LIGHT_RED, END_COLOR)
312 312
             logger.debug('%s %s' % (line_header, etree.tostring(ctx.out_document,
313 313
                                         xml_declaration=True, pretty_print=True)))
50  src/rpclib/protocol/xml/_base.py
@@ -74,6 +74,8 @@ class XmlObject(ProtocolBase):
74 74
     :param validator: One of (None, 'soft', 'lxml').
75 75
     """
76 76
 
  77
+    SCHEMA_VALIDATION = type("schema", (object,), {})
  78
+
77 79
     def __init__(self, app=None, validator=None, xml_declaration=True):
78 80
         ProtocolBase.__init__(self, app, validator)
79 81
         self.xml_declaration = xml_declaration
@@ -105,17 +107,23 @@ def __init__(self, app=None, validator=None, xml_declaration=True):
105 107
 
106 108
         self.log_messages = (logger.level == logging.DEBUG)
107 109
 
108  
-    def check_validator(self):
109  
-        self.validation_schema = None
110  
-
111  
-        if self.validator == 'lxml':
  110
+    def set_validator(self, validator):
  111
+        if validator in ('lxml', 'schema') or \
  112
+                                    validator is self.SCHEMA_VALIDATION:
112 113
             self.validate_document = self.__validate_lxml
  114
+            self.validator = self.SCHEMA_VALIDATION
  115
+
  116
+        elif validator is self.SOFT_VALIDATION or \
  117
+                                    validator is ProtocolBase.SOFT_VALIDATION:
  118
+            self.validator = self.SOFT_VALIDATION
113 119
 
114  
-        elif self.validator in (None, 'soft'):
  120
+        elif validator is None:
115 121
             pass
116 122
 
117 123
         else:
118  
-            raise ValueError(self.validator)
  124
+            raise ValueError(validator)
  125
+
  126
+        self.validation_schema = None
119 127
 
120 128
     def from_element(self, cls, element):
121 129
         handler = self.deserialization_handlers[cls]
@@ -125,16 +133,16 @@ def to_parent_element(self, cls, value, tns, parent_elt, * args, ** kwargs):
125 133
         handler = self.serialization_handlers[cls]
126 134
         handler(self, cls, value, tns, parent_elt, * args, ** kwargs)
127 135
 
128  
-    def validate_body(self, ctx, message='request'):
  136
+    def validate_body(self, ctx, message):
129 137
         """Sets ctx.method_request_string and calls :func:`generate_contexts`
130 138
         for validation."""
131 139
 
132  
-        assert message in ('request', 'response')
  140
+        assert message in (self.REQUEST, self.RESPONSE), message
133 141
 
134 142
         line_header = LIGHT_RED + "Error:" + END_COLOR
135 143
         try:
136 144
             self.validate_document(ctx.in_body_doc)
137  
-            if message == 'request':
  145
+            if message is self.REQUEST:
138 146
                 line_header = LIGHT_GREEN + "Method request string:" + END_COLOR
139 147
             else:
140 148
                 line_header = LIGHT_RED + "Response:" + END_COLOR
@@ -150,11 +158,11 @@ def create_in_document(self, ctx, charset=None):
150 158
         try:
151 159
             ctx.in_document = etree.fromstring(_bytes_join(ctx.in_string))
152 160
         except ValueError:
153  
-            ctx.in_document = etree.fromstring(_bytes_join([s.decode(charset) for s in ctx.in_string]))
154  
-
  161
+            ctx.in_document = etree.fromstring(_bytes_join([s.decode(charset)
  162
+                                                        for s in ctx.in_string]))
155 163
 
156  
-    def decompose_incoming_envelope(self, ctx, message='request'):
157  
-        assert message in ('request', 'response')
  164
+    def decompose_incoming_envelope(self, ctx, message):
  165
+        assert message in (self.REQUEST, self.RESPONSE)
158 166
 
159 167
         ctx.in_header_doc = None # If you need header support, you should use Soap
160 168
         ctx.in_body_doc = ctx.in_document
@@ -179,13 +187,13 @@ def deserialize(self, ctx, message):
179 187
         Not meant to be overridden.
180 188
         """
181 189
 
182  
-        assert message in ('request', 'response')
  190
+        assert message in (self.REQUEST, self.RESPONSE)
183 191
 
184 192
         self.event_manager.fire_event('before_deserialize', ctx)
185 193
 
186  
-        if message == 'request':
  194
+        if message is self.REQUEST:
187 195
             body_class = ctx.descriptor.in_message
188  
-        elif message == 'response':
  196
+        elif message is self.RESPONSE:
189 197
             body_class = ctx.descriptor.out_message
190 198
 
191 199
         # decode method arguments
@@ -195,9 +203,9 @@ def deserialize(self, ctx, message):
195 203
             ctx.in_object = [None] * len(body_class._type_info)
196 204
 
197 205
         if self.log_messages:
198  
-            if message == 'request':
  206
+            if message is self.REQUEST:
199 207
                 line_header = '%sRequest%s' % (LIGHT_GREEN, END_COLOR)
200  
-            elif message == 'response':
  208
+            elif message is self.RESPONSE:
201 209
                 line_header = '%sResponse%s' % (LIGHT_RED, END_COLOR)
202 210
 
203 211
             logger.debug("%s %s" % (line_header, etree.tostring(ctx.out_document,
@@ -213,14 +221,14 @@ def serialize(self, ctx, message):
213 221
         Not meant to be overridden.
214 222
         """
215 223
 
216  
-        assert message in ('request', 'response')
  224
+        assert message in (self.REQUEST, self.RESPONSE)
217 225
 
218 226
         self.event_manager.fire_event('before_serialize', ctx)
219 227
 
220 228
         # instantiate the result message
221  
-        if message == 'request':
  229
+        if message is self.REQUEST:
222 230
             result_message_class = ctx.descriptor.in_message
223  
-        elif message == 'response':
  231
+        elif message is self.RESPONSE:
224 232
             result_message_class = ctx.descriptor.out_message
225 233
 
226 234
         result_message = result_message_class()
9  src/rpclib/protocol/xml/model/_base.py
@@ -41,7 +41,8 @@ def wrapper(prot, cls, value, tns, parent_elt, *args, **kwargs):
41 41
 def nillable_element(func):
42 42
     def wrapper(prot, cls, element):
43 43
         if bool(element.get('{%s}nil' % _ns_xsi)):
44  
-            if prot.validator == 'soft' and (not cls.Attributes.nillable or
  44
+            if prot.validator is prot.SOFT_VALIDATION and (
  45
+                        not cls.Attributes.nillable or
45 46
                                     cls.Attributes._has_non_nillable_children):
46 47
                 raise ValidationError('')
47 48
             else:
@@ -52,10 +53,12 @@ def wrapper(prot, cls, element):
52 53
 
53 54
 @nillable_element
54 55
 def base_from_element(prot, cls, element):
55  
-    if prot.validator == 'soft' and not (cls.validate_string(cls, element.text)):
  56
+    if prot.validator is prot.SOFT_VALIDATION and not (
  57
+                                        cls.validate_string(cls, element.text)):
56 58
         raise ValidationError(element.text)
57 59
     retval = cls.from_string(element.text)
58  
-    if prot.validator == 'soft' and not (cls.validate_native(cls, retval)):
  60
+    if prot.validator is prot.SOFT_VALIDATION and not (
  61
+                                        cls.validate_native(cls, retval)):
59 62
         raise ValidationError(element.text)
60 63
     return retval
61 64
 
1  src/rpclib/protocol/xml/model/binary.py
@@ -17,7 +17,6 @@
17 17
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
18 18
 #
19 19
 
20  
-
21 20
 import logging
22 21
 logger = logging.getLogger(__name__)
23 22
 
5  src/rpclib/protocol/xml/model/complex.py
@@ -59,9 +59,6 @@ def complex_to_parent_element(prot, cls, value, tns, parent_elt, name=None):
59 59
         name = cls.get_type_name()
60 60
 
61 61
     element = etree.SubElement(parent_elt, "{%s}%s" % (tns, name))
62  
-
63  
-    # here, we try our best to match the incoming value with the class
64  
-    # definition in cls._type_map.
65 62
     inst = cls.get_serialization_instance(value)
66 63
 
67 64
     get_members_etree(prot, cls, inst, element)
@@ -112,7 +109,7 @@ def complex_from_element(prot, cls, element):
112 109
 
113 110
         setattr(inst, key, value)
114 111
 
115  
-    if prot.validator == 'soft':
  112
+    if prot.validator is prot.SOFT_VALIDATION:
116 113
         for key, c in flat_type_info.items():
117 114
             val = frequencies.get(key, 0)
118 115
             if (        val < c.Attributes.min_occurs
3  src/rpclib/protocol/xml/model/enum.py
@@ -31,6 +31,7 @@ def enum_to_parent_element(prot, cls, value, tns, parent_elt, name='retval'):
31 31
 
32 32
 @nillable_element
33 33
 def enum_from_element(prot, cls, element):
34  
-    if prot.validator == 'soft' and not (cls.validate_string(cls, element.text)):
  34
+    if prot.validator is prot.SOFT_VALIDATION and not (
  35
+                                        cls.validate_string(cls, element.text)):
35 36
         raise ValidationError(element.text)
36 37
     return getattr(cls, element.text)
6  src/rpclib/server/_base.py
@@ -84,7 +84,8 @@ def get_in_object(self, ctx):
84 84
 
85 85
         try:
86 86
             # sets ctx.in_object and ctx.in_header
87  
-            self.app.in_protocol.deserialize(ctx, message='request')
  87
+            self.app.in_protocol.deserialize(ctx,
  88
+                                        message=self.app.in_protocol.REQUEST)
88 89
 
89 90
         except Fault, e:
90 91
             ctx.in_object = None
@@ -107,7 +108,8 @@ def get_out_string(self, ctx):
107 108
         assert ctx.out_document is None
108 109
         assert ctx.out_string is None
109 110
 
110  
-        self.app.out_protocol.serialize(ctx, message='response')
  111
+        self.app.out_protocol.serialize(ctx,
  112
+                                        message=self.app.out_protocol.RESPONSE)
111 113
 
112 114
         if ctx.service_class != None:
113 115
             if ctx.out_error is None:
20  src/rpclib/server/wsgi.py
@@ -62,7 +62,7 @@ def _wsgi_input_to_iterable(http_env):
62 62
         yield data
63 63
 
64 64
 def reconstruct_wsgi_request(http_env):
65  
-    """Reconstruct http payload using information in the http header"""
  65
+    """Reconstruct http payload using information in the http header."""
66 66
 
67 67
     # fyi, here's what the parse_header function returns:
68 68
     # >>> import cgi; cgi.parse_header("text/xml; charset=utf-8")
@@ -102,10 +102,25 @@ def __init__(self, req_env, content_type):
102 102
         self.wsdl_error = None
103 103
         """The error when handling WSDL requests."""
104 104
 
  105
+    def get_mime_type(self):
  106
+        return self.resp_headers['Content-Type']
  107
+
  108
+    def set_mime_type(self, what):
  109
+        self.resp_headers['Content-Type'] = what
  110
+
  111
+    mime_type = property(get_mime_type, set_mime_type)
  112
+    """Provides an easy way to set outgoing mime type. Synonym for
  113
+    `content_type`"""
  114
+
  115
+    content_type = property(get_mime_type, set_mime_type)
  116
+    """Provides an easy way to set outgoing mime type. Synonym for
  117
+    `mime_type`"""
  118
+
105 119
 
106 120
 class WsgiMethodContext(MethodContext):
107 121
     """The WSGI-Specific method context. WSGI-Specific information is stored in
108  
-    the transport attribute using the :class:`WsgiTransportContext` class."""
  122
+    the transport attribute using the :class:`WsgiTransportContext` class.
  123
+    """
109 124
 
110 125
     def __init__(self, app, req_env, content_type):
111 126
         MethodContext.__init__(self, app)
@@ -244,6 +259,7 @@ def handle_rpc(self, req_env, start_response):
244 259
 
245 260
             self.get_in_object(ctx)
246 261
             if ctx.in_error:
  262
+                logger.error(ctx.in_error)
247 263
                 return self.handle_error(ctx, ctx.in_error, start_response)
248 264
 
249 265
             self.get_out_object(ctx)
21  src/rpclib/test/model/test_complex.py
@@ -326,7 +326,6 @@ class Parameter(ComplexModel):
326 326
 
327 327