Skip to content
This repository

support sqlalchemy.orm.relationship, better events, better documentation. #87

Merged
merged 10 commits into from over 2 years ago

1 participant

Burak Arslan
This page is out of date. Refresh to see the latest.
104  doc/source/pages/hooks.rst
Source Rendered
... ...
@@ -1,104 +0,0 @@
1  
-
2  
-Hooks
3  
-=====
4  
-
5  
-This example is an enhanced version of the HelloWorld example that uses service
6  
-'hooks' to apply cross-cutting behavior to the service. In this example, the
7  
-service hooks are used to gather performance information on both the method
8  
-execution as well as the duration of the entire call, including serialization
9  
-and deserialization. The available hooks are:
10  
-
11  
-    * on_call
12  
-
13  
-        This is the first thing called in the service
14  
-
15  
-    * on_wsdl
16  
-
17  
-        Called before the wsdl is requested
18  
-
19  
-    * on_wsdl_exception
20  
-
21  
-        Called after an exception was thrown when generating the wsdl (shouldn't happen very much)
22  
-
23  
-    * on_method_call
24  
-
25  
-        Called right before the service method is executed
26  
-
27  
-    * on_method_return
28  
-
29  
-        Called right after the service method is executed
30  
-
31  
-    * on_method_exception_object
32  
-
33  
-        Called when an exception occurred in a service method before the exception is serialized.
34  
-
35  
-    * on_method_exception_xml
36  
-
37  
-        Called after an exception occurred in either the service method or in serialization
38  
-
39  
-    * on_return
40  
-
41  
-        This is the very last thing called before the wsgi app exits
42  
-
43  
-These method can be used to easily apply cross-cutting functionality accross all
44  
-methods in the service to do things like database transaction management,
45  
-logging and measuring performance. This example also employs the threadlocal
46  
-request (rpclib.wsgi_soap.request) object to hold the data points for this
47  
-request. ::
48  
-
49  
-    from rpclib.service import rpc, DefinitionBase
50  
-    from rpclib.model.primitive import String, Integer
51  
-    from rpclib.model.clazz import Array
52  
-
53  
-    from time import time
54  
-
55  
-    class HelloWorldService(DefinitionBase):
56  
-
57  
-        @rpc(String,Integer,_returns=Array(String))
58  
-        def say_hello(self,name,times):
59  
-            results = []
60  
-            for i in range(0,times):
61  
-                results.append('Hello, %s'%name)
62  
-            return results
63  
-
64  
-        def on_call(self,environ):
65  
-            request.additional['call_start'] = time()
66  
-
67  
-        def on_method_exec(self,environ,body,py_params,soap_params):
68  
-            request.additional['method_start'] = time()
69  
-
70  
-        def on_results(self,environ,py_results,soap_results,http_headers):
71  
-            request.additional['method_end'] = time()
72  
-
73  
-        def on_return(self,environ,returnString):
74  
-            call_start = request.additional['call_start']
75  
-            call_end = time()
76  
-            method_start = request.additional['method_start']
77  
-            method_end = request.additional['method_end']
78  
-
79  
-            print 'Method took [%s] - total execution time[%s]'%(method_end-method_start,call_end-call_start)
80  
-
81  
-
82  
-    if __name__=='__main__':
83  
-        from wsgiref.simple_server import make_server
84  
-        server = make_server('localhost', 7789, Application([HelloWorldService]))
85  
-        server.serve_forever()
86  
-
87  
-
88  
-Running this produces:
89  
-
90  
-Method took [0.000195980072021] - total execution time[0.00652194023132]
91  
-Method took [0.000250101089478] - total execution time[0.00567507743835]
92  
-Method took [0.000144004821777] - total execution time[0.00521206855774]
93  
-Method took [0.000141859054565] - total execution time[0.00512409210205]
94  
-Method took [0.00377607345581] - total execution time[0.00511980056763]
95  
-Method took [0.00118803977966] - total execution time[0.00673604011536]
96  
-Method took [0.000146150588989] - total execution time[0.00157499313354]
97  
-Method took [0.0231170654297] - total execution time[0.0245010852814]
98  
-Method took [0.000166893005371] - total execution time[0.01802110672]
99  
-
100  
-
101  
-These may be helpful in finding bottlenecks in process, but this technique can
102  
-also be used to commit/rollback transactions or do setup/teardown operations for
103  
-all methods in a service.
104  
-
43  doc/source/reference/base.rst
Source Rendered
... ...
@@ -0,0 +1,43 @@
  1
+
  2
+.. _reference-base:
  3
+
  4
+Fundamental Data Structures
  5
+===========================
  6
+
  7
+MethodContext
  8
+-------------
  9
+
  10
+.. autoclass:: rpclib.MethodContext
  11
+    :members:
  12
+    :inherited-members:
  13
+
  14
+MethodDescriptor
  15
+----------------
  16
+
  17
+.. autoclass:: rpclib.MethodDescriptor
  18
+    :members:
  19
+    :inherited-members:
  20
+
  21
+.. _reference-eventmanager:
  22
+
  23
+
  24
+EventManager
  25
+------------
  26
+
  27
+Rpclib supports a simple event system that can be used to have repetitive boiler
  28
+plate code that has to run for every method call nicely tucked away in one or
  29
+more event handlers. The popular use-cases include things like database
  30
+transaction management, logging and measuring performance.
  31
+
  32
+Various Rpclib components support firing events at various stages during the
  33
+processing of the request, which are documented in the relevant classes.
  34
+
  35
+The classes that support events are:
  36
+    * :class:`rpclib.application.Application`
  37
+    * :class:`rpclib.service.ServiceBase`
  38
+    * :class:`rpclib.protocol.ProtocolBase`
  39
+    * :class:`rpclib.server.WsgiApplication`
  40
+
  41
+.. autoclass:: rpclib.EventManager
  42
+    :members:
  43
+    :inherited-members:
21  doc/source/reference/index.rst
Source Rendered
... ...
@@ -1,16 +1,19 @@
1 1
 
  2
+.. _reference-index:
  3
+
2 4
 ====================
3 5
 Rpclib API Reference
4 6
 ====================
5 7
 
6 8
 .. toctree::
7  
-   :maxdepth: 2
  9
+    :maxdepth: 2
8 10
 
9  
-   model
10  
-   interface
11  
-   protocol
12  
-   client
13  
-   server
14  
-   util
15  
-   service
16  
-   application
  11
+    base
  12
+    model
  13
+    interface
  14
+    protocol
  15
+    client
  16
+    server
  17
+    util
  18
+    service
  19
+    application
1  doc/source/reference/interface.rst
Source Rendered
@@ -25,4 +25,3 @@ Wsdl 1.1
25 25
 .. automodule:: rpclib.interface.wsdl.wsdl11
26 26
     :members:
27 27
     :inherited-members:
28  
-
6  doc/source/tutorial/helloworld.rst
Source Rendered
... ...
@@ -1,4 +1,6 @@
1 1
 
  2
+.. _tutorial-helloworld:
  3
+
2 4
 Hello World
3 5
 ===========
4 6
 
@@ -229,5 +231,5 @@ The command's output would be as follows: ::
229 231
 What's next?
230 232
 ------------
231 233
 
232  
-See the next :ref:`tutorial-user-manager` tutorial that will walk you through defining complex
233  
-objects and using events.
  234
+See the next :ref:`tutorial-user-manager` tutorial that will walk you through
  235
+defining complex objects and using events.
15  doc/source/tutorial/index.rst
Source Rendered
... ...
@@ -1,14 +1,15 @@
1 1
 
  2
+.. _tutorial-index:
  3
+
2 4
 Rpclib Tutorials
3 5
 ================
4 6
 
5  
-Here we document introductory tutorials that aim to introduce you most of the
6  
-rpclib concepts with examples.
7  
-
  7
+Here we document tutorials that aim to introduce you most of the rpclib concepts
  8
+with examples.
8 9
 
9 10
 .. toctree::
10  
-   :maxdepth: 2
  11
+    :maxdepth: 2
11 12
 
12  
-   helloworld
13  
-   usermanager
14  
-   sqlalchemy
  13
+    helloworld
  14
+    usermanager
  15
+    sqlalchemy
41  doc/source/tutorial/sqlalchemy.rst
Source Rendered
... ...
@@ -1,10 +1,18 @@
1 1
 
  2
+.. _tutorial-sqlalchemy:
  3
+
2 4
 SQLAlchemy Integration
3 5
 ----------------------
4 6
 
5  
-Let's try a more complicated example than just strings and integers!
6  
-The following is an simple example using complex, nested data. It's available
7  
-here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_sqlalchemy.py
  7
+This tutorial builds on the :ref:`tutorial-user-manager` tutorial. If you haven't
  8
+done so, we recommended you to read it first.
  9
+
  10
+Let's try a more complicated example than storing our data in a mere dictionary.
  11
+
  12
+The following example shows how to integrate SQLAlchemy and Rpclib objects, and
  13
+how to do painless transaction management using Rpclib events.
  14
+
  15
+The full example is available here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_sqlalchemy.py
8 16
 
9 17
 ::
10 18
 
@@ -112,7 +120,8 @@ here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_s
112 120
 
113 121
         server.serve_forever()
114 122
 
115  
-Again, focusing on what's different from previous example: ::
  123
+Again, focusing on what's different from previous :ref:`tutorial-user-manager`
  124
+example: ::
116 125
 
117 126
     class User(TableModel, DeclarativeBase):
118 127
         __namespace__ = 'rpclib.examples.user_manager'
@@ -136,8 +145,8 @@ The SQLAlchemy integration is far from perfect at the moment:
136 145
 
137 146
     * SQL constraints are not reflected to the interface document.
138 147
     * It's not possible to define additional schema constraints.
139  
-    * Object attributes defined by mechanisms other than Column are not directly
140  
-      supported.
  148
+    * Object attributes defined by mechanisms other than Column and a limited
  149
+      form of `relationship` (no string arguments) are not supported.
141 150
 
142 151
 If you need any of the above features, you need to separate the rpclib and
143 152
 sqlalchemy object definitions.
@@ -149,8 +158,7 @@ Rpclib supports this with the following syntax: ::
149 158
         __table__ = User.__table__
150 159
 
151 160
 Here, The AlternativeUser object is automatically populated using columns from
152  
-the table definition. You should explicitly re-define attributes that are not
153  
-directly derivable from the table definition like the relationship()-based ones.
  161
+the table definition.
154 162
 
155 163
 The context object is also a little bit different -- we start a transaction for
156 164
 every call in the constructor of the UserDefinedContext object, and close it in
@@ -163,14 +171,14 @@ its destructor: ::
163 171
         def __del__(self):
164 172
             self.session.close()
165 173
 
166  
-And we register an event that instantiates the UserDefinedContext object for
167  
-every method call: ::
  174
+We implement an event handler that instantiates the UserDefinedContext object
  175
+for every method call: ::
168 176
 
169 177
     def _on_method_call(ctx):
170 178
         ctx.udc = UserDefinedContext()
171 179
 
172  
-We also implement an event that commits the transaction once the method call is
173  
-complete. ::
  180
+We also implement an event handler that commits the transaction once the method
  181
+call is complete. ::
174 182
 
175 183
     def _on_method_return_object(ctx):
176 184
         ctx.udc.session.commit()
@@ -180,13 +188,12 @@ We register those handlers to the application's 'method_call' handler: ::
180 188
     application.event_manager.add_listener('method_call', _on_method_call)
181 189
     application.event_manager.add_listener('method_return_object', _on_method_return_object)
182 190
 
183  
-Using events to do transaction management prevents us from littering code with
184  
-repetitive code. 
  191
+Note that the ``method_return_object`` event is only run when the method call
  192
+was completed without throwing any exceptions.
185 193
 
186 194
 What's next?
187 195
 ^^^^^^^^^^^^
188 196
 
189 197
 This tutorial walks you through most of what you need to know to expose your
190  
-services. You can refer to the rest of the documentation or the mailing list
191  
-if you have further questions.
192  
-
  198
+services. You can refer to the reference of the documentation or the mailing
  199
+list if you have further questions.
6  doc/source/tutorial/usermanager.rst
Source Rendered
@@ -4,6 +4,9 @@
4 4
 User Manager
5 5
 ------------
6 6
 
  7
+This tutorial builds on the :ref:`tutorial-helloworld` tutorial. If you haven't
  8
+done so, we recommended you to read it first.
  9
+
7 10
 Let's try a more complicated example than just strings and integers!
8 11
 The following is an simple example using complex, nested data. It's available
9 12
 here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_basic.py
@@ -158,7 +161,6 @@ What's next?
158 161
 ^^^^^^^^^^^^
159 162
 
160 163
 This tutorial walks you through most of what you need to know to expose your
161  
-services. You can read the SQLAlchemy & Rpclib integration tutorial if you plan
  164
+services. You can read the :ref:`tutorial-sqlalchemy` tutorial if you plan
162 165
 to expose your database application using rpclib. Otherwise, you should refer to
163 166
 the rest of the documentation or the mailing list if you have further questions.
164  
-
49  src/rpclib/_base.py
@@ -176,40 +176,89 @@ def __init__(self, function, in_message, out_message, doc,
176 176
                  port_type=None, no_ctx=False):
177 177
 
178 178
         self.function = function
  179
+        """The original function object to be called when the method is remotely
  180
+        invoked."""
  181
+
179 182
         self.in_message = in_message
  183
+        """Automatically generated complex object based on incoming arguments to
  184
+        the function."""
  185
+
180 186
         self.out_message = out_message
  187
+        """Automatically generated complex object based on the return type of
  188
+        the function."""
  189
+
181 190
         self.doc = doc
  191
+        """The function docstring."""
  192
+
182 193
         self.is_callback = is_callback
183 194
         self.is_async = is_async
  195
+
184 196
         self.mtom = mtom
  197
+        """Flag to indicate whether to use MTOM transport with SOAP."""
  198
+
185 199
         self.in_header = in_header
  200
+        """The incoming header object this function could accept."""
  201
+
186 202
         self.out_header = out_header
  203
+        """The outgoing header object this function could send."""
  204
+
187 205
         self.faults = faults
  206
+        """The exceptions that this function can throw."""
  207
+
188 208
         self.port_type = port_type
  209
+        """The portType this function belongs to."""
  210
+
189 211
         self.no_ctx = no_ctx
  212
+        """Whether the function receives the method context as the first
  213
+        argument implicitly."""
190 214
 
191 215
     @property
192 216
     def name(self):
  217
+        """The public name of the function. Equals to the type_name of the
  218
+        in_message."""
193 219
         return self.in_message.get_type_name()
194 220
 
195 221
     @property
196 222
     def key(self):
  223
+        """The function identifier in '{namespace}name' form."""
  224
+
197 225
         assert not (self.in_message.get_namespace() is DEFAULT_NS)
198 226
 
199 227
         return '{%s}%s' % (
200 228
             self.in_message.get_namespace(), self.in_message.get_type_name())
201 229
 
202 230
 class EventManager(object):
  231
+    """The event manager for all rpclib events. The events are stored in an
  232
+    ordered set -- so the events are ran in the order they were added and
  233
+    adding a handler twice does not cause it to run twice.
  234
+    """
  235
+
203 236
     def __init__(self, parent, handlers={}):
204 237
         self.parent = parent
205 238
         self.handlers = dict(handlers)
206 239
 
207 240
     def add_listener(self, event_name, handler):
  241
+        """Register a handler for the given event name.
  242
+
  243
+        :param event_name: The event identifier, indicated by the documentation.
  244
+                           Usually, this is a string.
  245
+        :param handler: A static python function that receives a single
  246
+                        MethodContext argument.
  247
+        """
  248
+
208 249
         handlers = self.handlers.get(event_name, oset())
209 250
         handlers.add(handler)
210 251
         self.handlers[event_name] = handlers
211 252
 
212 253
     def fire_event(self, event_name, ctx):
  254
+        """Run all the handlers for a given event name.
  255
+
  256
+        :param event_name: The event identifier, indicated by the documentation.
  257
+                           Usually, this is a string.
  258
+        :param handler: The method context. Event-related data is conventionally
  259
+                        stored in ctx.event attribute.
  260
+        """
  261
+
213 262
         handlers = self.handlers.get(event_name, oset())
214 263
         for handler in handlers:
215 264
             handler(ctx)
19  src/rpclib/application.py
@@ -17,8 +17,8 @@
17 17
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
18 18
 #
19 19
 
20  
-"""This module contains the Application class that exposes multiple service
21  
-definitions to the outside world.
  20
+"""This module contains the Application class, to which every other rpclib
  21
+component is integrated.
22 22
 """
23 23
 
24 24
 
@@ -45,6 +45,17 @@ class Application(object):
45 45
     :param name:         The optional name attribute of the exposed service.
46 46
                          The default is the name of the application class
47 47
                          which is, by default, 'Application'.
  48
+
  49
+    Supported events:
  50
+        * method_call
  51
+            Called right before the service method is executed
  52
+
  53
+        * method_return_object
  54
+            Called right after the service method is executed
  55
+
  56
+        * method_exception_object
  57
+            Called when an exception occurred in a service method, before the
  58
+            exception is serialized.
48 59
     '''
49 60
 
50 61
     transport = None
@@ -104,7 +115,7 @@ def process_request(self, ctx):
104 115
             self.event_manager.fire_event('method_exception_object', ctx)
105 116
             if ctx.service_class != None:
106 117
                 ctx.service_class.event_manager.fire_event(
107  
-                                                    'method_return_object', ctx)
  118
+                                                'method_exception_object', ctx)
108 119
 
109 120
         except Exception, e:
110 121
             logger.exception(e)
@@ -115,7 +126,7 @@ def process_request(self, ctx):
115 126
             self.event_manager.fire_event('method_exception_object', ctx)
116 127
             if ctx.service_class != None:
117 128
                 ctx.service_class.event_manager.fire_event(
118  
-                                                    'method_return_object', ctx)
  129
+                                                'method_exception_object', ctx)
119 130
 
120 131
     def call_wrapper(self, ctx):
121 132
         """This method calls the call_wrapper method in the service definition.
23  src/rpclib/client/_base.py
@@ -101,8 +101,28 @@ def get_out_string(self):
101 101
         assert self.ctx.out_string is None
102 102
 
103 103
         self.app.out_protocol.serialize(self.ctx)
  104
+
  105
+        if self.ctx.service_class != None:
  106
+            if self.ctx.out_error is None:
  107
+                self.ctx.service_class.event_manager.fire_event(
  108
+                                        'method_return_document', self.ctx)
  109
+            else:
  110
+                self.ctx.service_class.event_manager.fire_event(
  111
+                                        'method_exception_document', self.ctx)
  112
+
104 113
         self.app.out_protocol.create_out_string(self.ctx, string_encoding)
105 114
 
  115
+        if self.ctx.service_class != None:
  116
+            if self.ctx.out_error is None:
  117
+                self.ctx.service_class.event_manager.fire_event(
  118
+                                            'method_return_string', self.ctx)
  119
+            else:
  120
+                self.ctx.service_class.event_manager.fire_event(
  121
+                                            'method_exception_string', self.ctx)
  122
+
  123
+        if self.ctx.out_string is None:
  124
+            self.ctx.out_string = [""]
  125
+
106 126
     def get_in_object(self):
107 127
         """Deserializes the response bytestream to input document and native
108 128
         python object.
@@ -112,6 +132,9 @@ def get_in_object(self):
112 132
         assert self.ctx.in_document is None
113 133
 
114 134
         self.app.in_protocol.create_in_document(self.ctx)
  135
+        if self.ctx.service_class != None:
  136
+            self.ctx.service_class.event_manager.fire_event(
  137
+                                            'method_accept_document', self.ctx)
115 138
 
116 139
         # sets the ctx.in_body_doc and ctx.in_header_doc properties
117 140
         self.app.in_protocol.decompose_incoming_envelope(self.ctx)
41  src/rpclib/model/table.py
@@ -33,7 +33,10 @@
33 33
 logger = logging.getLogger(__name__)
34 34
 
35 35
 import sqlalchemy
  36
+
  37
+from warnings import warn
36 38
 from sqlalchemy import Column
  39
+from sqlalchemy.orm import RelationshipProperty
37 40
 
38 41
 from sqlalchemy.ext.declarative import DeclarativeMeta
39 42
 
@@ -65,16 +68,36 @@
65 68
 def _process_item(v):
66 69
     """This function maps sqlalchemy types to rpclib types."""
67 70
 
68  
-    if v.type in _type_map:
69  
-        rpc_type = _type_map[v.type]
70  
-    elif type(v.type) in _type_map:
71  
-        rpc_type = _type_map[type(v.type)]
72  
-    else:
73  
-        raise Exception("soap_type was not found. maybe _type_map needs a new "
74  
-                        "entry. %r" % v)
  71
+    rpc_type = None
  72
+    if isinstance(v, Column):
  73
+        if v.type in _type_map:
  74
+            rpc_type = _type_map[v.type]
  75
+        elif type(v.type) in _type_map:
  76
+            rpc_type = _type_map[type(v.type)]
  77
+        else:
  78
+            raise Exception("soap_type was not found. maybe _type_map needs a new "
  79
+                            "entry. %r" % v)
  80
+    elif isinstance(v, RelationshipProperty):
  81
+        rpc_type = v.argument
75 82
 
76 83
     return rpc_type
77 84
 
  85
+def _is_interesting(k, v):
  86
+    if k.startswith('__'):
  87
+        return False
  88
+
  89
+    if isinstance(v, Column):
  90
+        return True
  91
+
  92
+    if isinstance(v, RelationshipProperty):
  93
+        if getattr(v.argument,'_type_info', None) is None:
  94
+            warn("the argument to relationship should be a reference to the real"
  95
+                 "column, not a string.")
  96
+            return False
  97
+
  98
+        else:
  99
+            return True
  100
+
78 101
 class TableSerializerMeta(DeclarativeMeta, ComplexModelMeta):
79 102
     """This class uses the information in class definition dictionary to build
80 103
     the _type_info dictionary that rpclib relies on. It otherwise leaves
@@ -91,7 +114,7 @@ def __new__(cls, cls_name, cls_bases, cls_dict):
91 114
             # mixin inheritance
92 115
             for b in cls_bases:
93 116
                 for k,v in vars(b).items():
94  
-                    if isinstance(v, Column):
  117
+                    if _is_interesting(k,v):
95 118
                         _type_info[k] = _process_item(v)
96 119
 
97 120
             # same table inheritance
@@ -110,7 +133,7 @@ def __new__(cls, cls_name, cls_bases, cls_dict):
110 133
 
111 134
             # own attributes
112 135
             for k, v in cls_dict.items():
113  
-                if (not k.startswith('__')) and isinstance(v, Column):
  136
+                if _is_interesting(k,v):
114 137
                     _type_info[k] = _process_item(v)
115 138
 
116 139
         return DeclarativeMeta.__new__(cls, cls_name, cls_bases, cls_dict)
10  src/rpclib/protocol/_base.py
@@ -35,7 +35,15 @@
35 35
 
36 36
 class ProtocolBase(object):
37 37
     """This is the abstract base class for all protocol implementations. Child
38  
-    classes can implement only the required subset of the public methods
  38
+    classes can implement only the required subset of the public methods.
  39
+
  40
+    The ProtocolBase class supports the following events:
  41
+    * ``deserialize``
  42
+        Called right after the deserialization operation is finished.
  43
+
  44
+    * ``serialize``
  45
+        Called right after the serialization operation is finished.
  46
+
39 47
     """
40 48
 
41 49
     allowed_http_verbs = ['GET','POST']
10  src/rpclib/server/_base.py
@@ -49,14 +49,14 @@ def get_in_object(self, ctx, in_string_charset=None):
49 49
         to set ctx.in_object."""
50 50
 
51 51
         self.app.in_protocol.create_in_document(ctx, in_string_charset)
  52
+        if ctx.service_class != None:
  53
+            ctx.service_class.event_manager.fire_event('method_accept_document',ctx)
52 54
 
53 55
         try:
54  
-            # sets the ctx.in_body_doc and ctx.in_header_doc properties
  56
+            # sets the ctx.in_body_doc and ctx.in_header_doc
55 57
             self.app.in_protocol.decompose_incoming_envelope(ctx)
56 58
 
57  
-            if ctx.service_class != None:
58  
-                ctx.service_class.event_manager.fire_event('decompose_envelope',
59  
-                                                                        ctx)
  59
+            # sets the ctx.in_object and ctx.in_header
60 60
             self.app.in_protocol.deserialize(ctx)
61 61
 
62 62
         except Fault,e:
@@ -68,6 +68,8 @@ def get_out_object(self, ctx):
68 68
         """Calls the matched method using the ctx.in_object to get
69 69
         ctx.out_object."""
70 70
 
  71
+
  72
+        # event firing is done in the rpclib.application.Application
71 73
         self.app.process_request(ctx)
72 74
 
73 75
     def get_out_string(self, ctx):
67  src/rpclib/server/wsgi.py
@@ -17,7 +17,7 @@
17 17
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
18 18
 #
19 19
 
20  
-"""An rpc server that uses http as transport, and wsgi as bridge api"""
  20
+"""An rpc server that uses http as transport, and wsgi as bridge api."""
21 21
 
22 22
 # FIXME: this is maybe still too soap-centric.
23 23
 
@@ -62,27 +62,69 @@ def reconstruct_wsgi_request(http_env):
62 62
 
63 63
 
64 64
 class WsgiTransportContext(TransportContext):
  65
+    """The class that is used in the transport attribute of the
  66
+    :class:`WsgiMethodContext` class."""
  67
+
65 68
     def __init__(self, req_env, content_type):
66 69
         TransportContext.__init__(self, 'wsgi')
67 70
 
68 71
         self.req_env = req_env
  72
+        """WSGI Request environment"""
  73
+
69 74
         self.resp_headers = {
70 75
             'Content-Type': content_type,
71 76
             'Content-Length': '0',
72 77
         }
  78
+        """HTTP Response headers."""
  79
+
73 80
         self.resp_code = None
  81
+        """HTTP Response code."""
  82
+
74 83
         self.req_method = req_env.get('REQUEST_METHOD', None)
  84
+        """HTTP Request verb, as a convenience to users."""
  85
+
  86
+        self.wsdl = None
  87
+        """The WSDL document that is being returned. Only relevant when handling
  88
+        WSDL requests."""
  89
+
75 90
         self.wsdl_error = None
  91
+        """The error when handling WSDL requests."""
76 92
 
77 93
 
78 94
 class WsgiMethodContext(MethodContext):
  95
+    """The WSGI-Specific method context. WSGI-Specific information is stored in
  96
+    the transport attribute using the :class:`WsgiTransportContext` class."""
  97
+
79 98
     def __init__(self, app, req_env, content_type):
80 99
         MethodContext.__init__(self, app)
81 100
 
82 101
         self.transport = WsgiTransportContext(req_env, content_type)
83  
-
  102
+        """Holds the WSGI-specific information"""
84 103
 
85 104
 class WsgiApplication(ServerBase):
  105
+    '''A `PEP-3333 <http://www.python.org/dev/peps/pep-3333/#preface-for-readers-of-pep-333>`_
  106
+    compliant callable class.
  107
+
  108
+    Supported events:
  109
+        * ``wsdl``
  110
+            Called right before the wsdl data is returned to the client.
  111
+
  112
+        * ``wsdl_exception``
  113
+            Called right after an exception is thrown during wsdl generation. The
  114
+            exception object is stored in ctx.transport.wsdl_error attribute.
  115
+
  116
+        * ``wsgi_call``
  117
+            Called first when the incoming http request is identified as a rpc
  118
+            request.
  119
+
  120
+        * ``wsgi_return``
  121
+            Called right before the output stream is returned to the WSGI handler.
  122
+
  123
+        * ``wsgi_resource_not_found``
  124
+            Called right before returning a 404 when the requested resource was not
  125
+            found.
  126
+    '''
  127
+
86 128
     transport = 'http://schemas.xmlsoap.org/soap/http'
87 129
 
88 130
     def __init__(self, app):
@@ -129,21 +171,20 @@ def __is_wsdl_request(self, req_env):
129 171
 
130 172
     def __handle_wsdl_request(self, req_env, start_response, url):
131 173
         ctx = WsgiMethodContext(self.app, req_env, 'text/xml; charset=utf-8')
132  
-
133 174
         try:
134  
-            wsdl = self.app.interface.get_interface_document()
135  
-            if wsdl is None:
  175
+            ctx.transport.wsdl = self.app.interface.get_interface_document()
  176
+            if ctx.transport.wsdl is None:
136 177
                 self.app.interface.build_interface_document(url)
137  
-                wsdl = self.app.interface.get_interface_document()
  178
+                ctx.transport.wsdl = self.app.interface.get_interface_document()
138 179
 
139  
-            assert wsdl != None
  180
+            assert ctx.transport.wsdl != None
140 181
 
141  
-            self.event_manager.fire_event('wsdl',ctx) # implementation hook
  182
+            self.event_manager.fire_event('wsdl', ctx) # implementation hook
142 183
 
143  
-            ctx.transport.resp_headers['Content-Length'] = str(len(wsdl))
  184
+            ctx.transport.resp_headers['Content-Length'] = str(len(ctx.transport.wsdl))
144 185
             start_response(HTTP_200, ctx.transport.resp_headers.items())
145 186
 
146  
-            return [wsdl]
  187
+            return [ctx.transport.wsdl]
147 188
 
148 189
         except Exception, e:
149 190
             logger.exception(e)
@@ -194,9 +235,6 @@ def __handle_rpc(self, req_env, start_response):
194 235
 
195 236
         self.get_out_string(ctx)
196 237
 
197  
-        # implementation hook
198  
-        self.event_manager.fire_event('wsgi_return', ctx)
199  
-
200 238
         if ctx.descriptor and ctx.descriptor.mtom:
201 239
             # when there is more than one return type, the result is
202 240
             # encapsulated inside a list. when there's just one, the result
@@ -213,6 +251,9 @@ def __handle_rpc(self, req_env, start_response):
213 251
                     out_object
214 252
                 )
215 253
 
  254
+        # implementation hook
  255
+        self.event_manager.fire_event('wsgi_return', ctx)
  256
+
216 257
         # We can't set the content-length if we want to support any kind of
217 258
         # python iterable as output. We can't iterate and count, that defeats
218 259
         # the whole point.
31  src/rpclib/service.py
@@ -61,6 +61,37 @@ class ServiceBase(object):
61 61
 
62 62
     It is a natural abstract base class, because it's of no use without any
63 63
     method definitions, hence the 'Base' suffix in the name.
  64
+
  65
+    The WsgiApplication class supports the following events:
  66
+        * ``method_call``
  67
+            Called right before the service method is executed
  68
+
  69
+        * ``method_return_object``
  70
+            Called right after the service method is executed
  71
+
  72
+        * ``method_exception_object``
  73
+            Called when an exception occurred in a service method, before the
  74
+            exception is serialized.
  75
+
  76
+        * ``method_accept_document``
  77
+            Called by the transport right after the incoming stream is parsed to
  78
+            the incoming protocol's document type.
  79
+
  80
+        * ``method_return_document``
  81
+            Called by the transport right after the outgoing object is
  82
+            serialized to the outgoing protocol's document type.
  83
+
  84
+        * ``method_exception_document``
  85
+            Called by the transport right before the outgoing exception object
  86
+            is serialized to the outgoing protocol's document type.
  87
+
  88
+        * ``method_return_string``
  89
+            Called by the transport right before passing the return string to
  90
+            the client.
  91
+
  92
+        * ``method_exception_string``
  93
+            Called by the transport right before passing the exception string to
  94
+            the client.
64 95
     '''
65 96
 
66 97
     __metaclass__ = ServiceBaseMeta
25  src/rpclib/test/test_sqla.py
@@ -281,5 +281,30 @@ class UserMail(User):
281 281
         assert 'name' in UserMail._type_info
282 282
         assert 'id' in UserMail._type_info
283 283
 
  284
+    def test_relationship(self):
  285
+        import sqlalchemy
  286
+
  287
+        class User(self.DeclarativeBase, TableSerializer):
  288
+            __tablename__ = 'rpclib_user'
  289
+
  290
+            id = Column(sqlalchemy.Integer, primary_key=True)
  291
+            name = Column(sqlalchemy.String(256))
  292
+
  293
+        class Address(self.DeclarativeBase, TableSerializer):
  294
+            __tablename__ = 'rpclib_address'
  295
+            id = Column(sqlalchemy.Integer, primary_key=True)
  296
+            address = Column(sqlalchemy.String(256))
  297
+            user_id = Column(sqlalchemy.Integer, ForeignKey(User.id), nullable=False)
  298
+            user = relationship(User)
  299
+
  300
+        assert 'user' in Address._type_info
  301
+        assert Address._type_info['user'] is User
  302
+
  303
+        u = User()
  304
+        a = Address()
  305
+        a.user = u
  306
+
  307
+
  308
+
284 309
 if __name__ == '__main__':
285 310
     unittest.main()
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.