Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

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

Merged
merged 10 commits into from
This page is out of date. Refresh to see the latest.
View
104 doc/source/pages/hooks.rst
@@ -1,104 +0,0 @@
-
-Hooks
-=====
-
-This example is an enhanced version of the HelloWorld example that uses service
-'hooks' to apply cross-cutting behavior to the service. In this example, the
-service hooks are used to gather performance information on both the method
-execution as well as the duration of the entire call, including serialization
-and deserialization. The available hooks are:
-
- * on_call
-
- This is the first thing called in the service
-
- * on_wsdl
-
- Called before the wsdl is requested
-
- * on_wsdl_exception
-
- Called after an exception was thrown when generating the wsdl (shouldn't happen very much)
-
- * on_method_call
-
- Called right before the service method is executed
-
- * on_method_return
-
- Called right after the service method is executed
-
- * on_method_exception_object
-
- Called when an exception occurred in a service method before the exception is serialized.
-
- * on_method_exception_xml
-
- Called after an exception occurred in either the service method or in serialization
-
- * on_return
-
- This is the very last thing called before the wsgi app exits
-
-These method can be used to easily apply cross-cutting functionality accross all
-methods in the service to do things like database transaction management,
-logging and measuring performance. This example also employs the threadlocal
-request (rpclib.wsgi_soap.request) object to hold the data points for this
-request. ::
-
- from rpclib.service import rpc, DefinitionBase
- from rpclib.model.primitive import String, Integer
- from rpclib.model.clazz import Array
-
- from time import time
-
- class HelloWorldService(DefinitionBase):
-
- @rpc(String,Integer,_returns=Array(String))
- def say_hello(self,name,times):
- results = []
- for i in range(0,times):
- results.append('Hello, %s'%name)
- return results
-
- def on_call(self,environ):
- request.additional['call_start'] = time()
-
- def on_method_exec(self,environ,body,py_params,soap_params):
- request.additional['method_start'] = time()
-
- def on_results(self,environ,py_results,soap_results,http_headers):
- request.additional['method_end'] = time()
-
- def on_return(self,environ,returnString):
- call_start = request.additional['call_start']
- call_end = time()
- method_start = request.additional['method_start']
- method_end = request.additional['method_end']
-
- print 'Method took [%s] - total execution time[%s]'%(method_end-method_start,call_end-call_start)
-
-
- if __name__=='__main__':
- from wsgiref.simple_server import make_server
- server = make_server('localhost', 7789, Application([HelloWorldService]))
- server.serve_forever()
-
-
-Running this produces:
-
-Method took [0.000195980072021] - total execution time[0.00652194023132]
-Method took [0.000250101089478] - total execution time[0.00567507743835]
-Method took [0.000144004821777] - total execution time[0.00521206855774]
-Method took [0.000141859054565] - total execution time[0.00512409210205]
-Method took [0.00377607345581] - total execution time[0.00511980056763]
-Method took [0.00118803977966] - total execution time[0.00673604011536]
-Method took [0.000146150588989] - total execution time[0.00157499313354]
-Method took [0.0231170654297] - total execution time[0.0245010852814]
-Method took [0.000166893005371] - total execution time[0.01802110672]
-
-
-These may be helpful in finding bottlenecks in process, but this technique can
-also be used to commit/rollback transactions or do setup/teardown operations for
-all methods in a service.
-
View
43 doc/source/reference/base.rst
@@ -0,0 +1,43 @@
+
+.. _reference-base:
+
+Fundamental Data Structures
+===========================
+
+MethodContext
+-------------
+
+.. autoclass:: rpclib.MethodContext
+ :members:
+ :inherited-members:
+
+MethodDescriptor
+----------------
+
+.. autoclass:: rpclib.MethodDescriptor
+ :members:
+ :inherited-members:
+
+.. _reference-eventmanager:
+
+
+EventManager
+------------
+
+Rpclib supports a simple event system that can be used to have repetitive boiler
+plate code that has to run for every method call nicely tucked away in one or
+more event handlers. The popular use-cases include things like database
+transaction management, logging and measuring performance.
+
+Various Rpclib components support firing events at various stages during the
+processing of the request, which are documented in the relevant classes.
+
+The classes that support events are:
+ * :class:`rpclib.application.Application`
+ * :class:`rpclib.service.ServiceBase`
+ * :class:`rpclib.protocol.ProtocolBase`
+ * :class:`rpclib.server.WsgiApplication`
+
+.. autoclass:: rpclib.EventManager
+ :members:
+ :inherited-members:
View
21 doc/source/reference/index.rst
@@ -1,16 +1,19 @@
+.. _reference-index:
+
====================
Rpclib API Reference
====================
.. toctree::
- :maxdepth: 2
+ :maxdepth: 2
- model
- interface
- protocol
- client
- server
- util
- service
- application
+ base
+ model
+ interface
+ protocol
+ client
+ server
+ util
+ service
+ application
View
1  doc/source/reference/interface.rst
@@ -25,4 +25,3 @@ Wsdl 1.1
.. automodule:: rpclib.interface.wsdl.wsdl11
:members:
:inherited-members:
-
View
6 doc/source/tutorial/helloworld.rst
@@ -1,4 +1,6 @@
+.. _tutorial-helloworld:
+
Hello World
===========
@@ -229,5 +231,5 @@ The command's output would be as follows: ::
What's next?
------------
-See the next :ref:`tutorial-user-manager` tutorial that will walk you through defining complex
-objects and using events.
+See the next :ref:`tutorial-user-manager` tutorial that will walk you through
+defining complex objects and using events.
View
15 doc/source/tutorial/index.rst
@@ -1,14 +1,15 @@
+.. _tutorial-index:
+
Rpclib Tutorials
================
-Here we document introductory tutorials that aim to introduce you most of the
-rpclib concepts with examples.
-
+Here we document tutorials that aim to introduce you most of the rpclib concepts
+with examples.
.. toctree::
- :maxdepth: 2
+ :maxdepth: 2
- helloworld
- usermanager
- sqlalchemy
+ helloworld
+ usermanager
+ sqlalchemy
View
41 doc/source/tutorial/sqlalchemy.rst
@@ -1,10 +1,18 @@
+.. _tutorial-sqlalchemy:
+
SQLAlchemy Integration
----------------------
-Let's try a more complicated example than just strings and integers!
-The following is an simple example using complex, nested data. It's available
-here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_sqlalchemy.py
+This tutorial builds on the :ref:`tutorial-user-manager` tutorial. If you haven't
+done so, we recommended you to read it first.
+
+Let's try a more complicated example than storing our data in a mere dictionary.
+
+The following example shows how to integrate SQLAlchemy and Rpclib objects, and
+how to do painless transaction management using Rpclib events.
+
+The full example is available here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_sqlalchemy.py
::
@@ -112,7 +120,8 @@ here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_s
server.serve_forever()
-Again, focusing on what's different from previous example: ::
+Again, focusing on what's different from previous :ref:`tutorial-user-manager`
+example: ::
class User(TableModel, DeclarativeBase):
__namespace__ = 'rpclib.examples.user_manager'
@@ -136,8 +145,8 @@ The SQLAlchemy integration is far from perfect at the moment:
* SQL constraints are not reflected to the interface document.
* It's not possible to define additional schema constraints.
- * Object attributes defined by mechanisms other than Column are not directly
- supported.
+ * Object attributes defined by mechanisms other than Column and a limited
+ form of `relationship` (no string arguments) are not supported.
If you need any of the above features, you need to separate the rpclib and
sqlalchemy object definitions.
@@ -149,8 +158,7 @@ Rpclib supports this with the following syntax: ::
__table__ = User.__table__
Here, The AlternativeUser object is automatically populated using columns from
-the table definition. You should explicitly re-define attributes that are not
-directly derivable from the table definition like the relationship()-based ones.
+the table definition.
The context object is also a little bit different -- we start a transaction for
every call in the constructor of the UserDefinedContext object, and close it in
@@ -163,14 +171,14 @@ its destructor: ::
def __del__(self):
self.session.close()
-And we register an event that instantiates the UserDefinedContext object for
-every method call: ::
+We implement an event handler that instantiates the UserDefinedContext object
+for every method call: ::
def _on_method_call(ctx):
ctx.udc = UserDefinedContext()
-We also implement an event that commits the transaction once the method call is
-complete. ::
+We also implement an event handler that commits the transaction once the method
+call is complete. ::
def _on_method_return_object(ctx):
ctx.udc.session.commit()
@@ -180,13 +188,12 @@ We register those handlers to the application's 'method_call' handler: ::
application.event_manager.add_listener('method_call', _on_method_call)
application.event_manager.add_listener('method_return_object', _on_method_return_object)
-Using events to do transaction management prevents us from littering code with
-repetitive code.
+Note that the ``method_return_object`` event is only run when the method call
+was completed without throwing any exceptions.
What's next?
^^^^^^^^^^^^
This tutorial walks you through most of what you need to know to expose your
-services. You can refer to the rest of the documentation or the mailing list
-if you have further questions.
-
+services. You can refer to the reference of the documentation or the mailing
+list if you have further questions.
View
6 doc/source/tutorial/usermanager.rst
@@ -4,6 +4,9 @@
User Manager
------------
+This tutorial builds on the :ref:`tutorial-helloworld` tutorial. If you haven't
+done so, we recommended you to read it first.
+
Let's try a more complicated example than just strings and integers!
The following is an simple example using complex, nested data. It's available
here: http://github.com/arskom/rpclib/blob/master/examples/user_manager/server_basic.py
@@ -158,7 +161,6 @@ What's next?
^^^^^^^^^^^^
This tutorial walks you through most of what you need to know to expose your
-services. You can read the SQLAlchemy & Rpclib integration tutorial if you plan
+services. You can read the :ref:`tutorial-sqlalchemy` tutorial if you plan
to expose your database application using rpclib. Otherwise, you should refer to
the rest of the documentation or the mailing list if you have further questions.
-
View
49 src/rpclib/_base.py
@@ -176,40 +176,89 @@ def __init__(self, function, in_message, out_message, doc,
port_type=None, no_ctx=False):
self.function = function
+ """The original function object to be called when the method is remotely
+ invoked."""
+
self.in_message = in_message
+ """Automatically generated complex object based on incoming arguments to
+ the function."""
+
self.out_message = out_message
+ """Automatically generated complex object based on the return type of
+ the function."""
+
self.doc = doc
+ """The function docstring."""
+
self.is_callback = is_callback
self.is_async = is_async
+
self.mtom = mtom
+ """Flag to indicate whether to use MTOM transport with SOAP."""
+
self.in_header = in_header
+ """The incoming header object this function could accept."""
+
self.out_header = out_header
+ """The outgoing header object this function could send."""
+
self.faults = faults
+ """The exceptions that this function can throw."""
+
self.port_type = port_type
+ """The portType this function belongs to."""
+
self.no_ctx = no_ctx
+ """Whether the function receives the method context as the first
+ argument implicitly."""
@property
def name(self):
+ """The public name of the function. Equals to the type_name of the
+ in_message."""
return self.in_message.get_type_name()
@property
def key(self):
+ """The function identifier in '{namespace}name' form."""
+
assert not (self.in_message.get_namespace() is DEFAULT_NS)
return '{%s}%s' % (
self.in_message.get_namespace(), self.in_message.get_type_name())
class EventManager(object):
+ """The event manager for all rpclib events. The events are stored in an
+ ordered set -- so the events are ran in the order they were added and
+ adding a handler twice does not cause it to run twice.
+ """
+
def __init__(self, parent, handlers={}):
self.parent = parent
self.handlers = dict(handlers)
def add_listener(self, event_name, handler):
+ """Register a handler for the given event name.
+
+ :param event_name: The event identifier, indicated by the documentation.
+ Usually, this is a string.
+ :param handler: A static python function that receives a single
+ MethodContext argument.
+ """
+
handlers = self.handlers.get(event_name, oset())
handlers.add(handler)
self.handlers[event_name] = handlers
def fire_event(self, event_name, ctx):
+ """Run all the handlers for a given event name.
+
+ :param event_name: The event identifier, indicated by the documentation.
+ Usually, this is a string.
+ :param handler: The method context. Event-related data is conventionally
+ stored in ctx.event attribute.
+ """
+
handlers = self.handlers.get(event_name, oset())
for handler in handlers:
handler(ctx)
View
19 src/rpclib/application.py
@@ -17,8 +17,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
#
-"""This module contains the Application class that exposes multiple service
-definitions to the outside world.
+"""This module contains the Application class, to which every other rpclib
+component is integrated.
"""
@@ -45,6 +45,17 @@ class Application(object):
:param name: The optional name attribute of the exposed service.
The default is the name of the application class
which is, by default, 'Application'.
+
+ Supported events:
+ * method_call
+ Called right before the service method is executed
+
+ * method_return_object
+ Called right after the service method is executed
+
+ * method_exception_object
+ Called when an exception occurred in a service method, before the
+ exception is serialized.
'''
transport = None
@@ -104,7 +115,7 @@ def process_request(self, ctx):
self.event_manager.fire_event('method_exception_object', ctx)
if ctx.service_class != None:
ctx.service_class.event_manager.fire_event(
- 'method_return_object', ctx)
+ 'method_exception_object', ctx)
except Exception, e:
logger.exception(e)
@@ -115,7 +126,7 @@ def process_request(self, ctx):
self.event_manager.fire_event('method_exception_object', ctx)
if ctx.service_class != None:
ctx.service_class.event_manager.fire_event(
- 'method_return_object', ctx)
+ 'method_exception_object', ctx)
def call_wrapper(self, ctx):
"""This method calls the call_wrapper method in the service definition.
View
23 src/rpclib/client/_base.py
@@ -101,8 +101,28 @@ def get_out_string(self):
assert self.ctx.out_string is None
self.app.out_protocol.serialize(self.ctx)
+
+ if self.ctx.service_class != None:
+ if self.ctx.out_error is None:
+ self.ctx.service_class.event_manager.fire_event(
+ 'method_return_document', self.ctx)
+ else:
+ self.ctx.service_class.event_manager.fire_event(
+ 'method_exception_document', self.ctx)
+
self.app.out_protocol.create_out_string(self.ctx, string_encoding)
+ if self.ctx.service_class != None:
+ if self.ctx.out_error is None:
+ self.ctx.service_class.event_manager.fire_event(
+ 'method_return_string', self.ctx)
+ else:
+ self.ctx.service_class.event_manager.fire_event(
+ 'method_exception_string', self.ctx)
+
+ if self.ctx.out_string is None:
+ self.ctx.out_string = [""]
+
def get_in_object(self):
"""Deserializes the response bytestream to input document and native
python object.
@@ -112,6 +132,9 @@ def get_in_object(self):
assert self.ctx.in_document is None
self.app.in_protocol.create_in_document(self.ctx)
+ if self.ctx.service_class != None:
+ self.ctx.service_class.event_manager.fire_event(
+ 'method_accept_document', self.ctx)
# sets the ctx.in_body_doc and ctx.in_header_doc properties
self.app.in_protocol.decompose_incoming_envelope(self.ctx)
View
41 src/rpclib/model/table.py
@@ -33,7 +33,10 @@
logger = logging.getLogger(__name__)
import sqlalchemy
+
+from warnings import warn
from sqlalchemy import Column
+from sqlalchemy.orm import RelationshipProperty
from sqlalchemy.ext.declarative import DeclarativeMeta
@@ -65,16 +68,36 @@
def _process_item(v):
"""This function maps sqlalchemy types to rpclib types."""
- if v.type in _type_map:
- rpc_type = _type_map[v.type]
- elif type(v.type) in _type_map:
- rpc_type = _type_map[type(v.type)]
- else:
- raise Exception("soap_type was not found. maybe _type_map needs a new "
- "entry. %r" % v)
+ rpc_type = None
+ if isinstance(v, Column):
+ if v.type in _type_map:
+ rpc_type = _type_map[v.type]
+ elif type(v.type) in _type_map:
+ rpc_type = _type_map[type(v.type)]
+ else:
+ raise Exception("soap_type was not found. maybe _type_map needs a new "
+ "entry. %r" % v)
+ elif isinstance(v, RelationshipProperty):
+ rpc_type = v.argument
return rpc_type
+def _is_interesting(k, v):
+ if k.startswith('__'):
+ return False
+
+ if isinstance(v, Column):
+ return True
+
+ if isinstance(v, RelationshipProperty):
+ if getattr(v.argument,'_type_info', None) is None:
+ warn("the argument to relationship should be a reference to the real"
+ "column, not a string.")
+ return False
+
+ else:
+ return True
+
class TableSerializerMeta(DeclarativeMeta, ComplexModelMeta):
"""This class uses the information in class definition dictionary to build
the _type_info dictionary that rpclib relies on. It otherwise leaves
@@ -91,7 +114,7 @@ def __new__(cls, cls_name, cls_bases, cls_dict):
# mixin inheritance
for b in cls_bases:
for k,v in vars(b).items():
- if isinstance(v, Column):
+ if _is_interesting(k,v):
_type_info[k] = _process_item(v)
# same table inheritance
@@ -110,7 +133,7 @@ def __new__(cls, cls_name, cls_bases, cls_dict):
# own attributes
for k, v in cls_dict.items():
- if (not k.startswith('__')) and isinstance(v, Column):
+ if _is_interesting(k,v):
_type_info[k] = _process_item(v)
return DeclarativeMeta.__new__(cls, cls_name, cls_bases, cls_dict)
View
10 src/rpclib/protocol/_base.py
@@ -35,7 +35,15 @@
class ProtocolBase(object):
"""This is the abstract base class for all protocol implementations. Child
- classes can implement only the required subset of the public methods
+ classes can implement only the required subset of the public methods.
+
+ The ProtocolBase class supports the following events:
+ * ``deserialize``
+ Called right after the deserialization operation is finished.
+
+ * ``serialize``
+ Called right after the serialization operation is finished.
+
"""
allowed_http_verbs = ['GET','POST']
View
10 src/rpclib/server/_base.py
@@ -49,14 +49,14 @@ def get_in_object(self, ctx, in_string_charset=None):
to set ctx.in_object."""
self.app.in_protocol.create_in_document(ctx, in_string_charset)
+ if ctx.service_class != None:
+ ctx.service_class.event_manager.fire_event('method_accept_document',ctx)
try:
- # sets the ctx.in_body_doc and ctx.in_header_doc properties
+ # sets the ctx.in_body_doc and ctx.in_header_doc
self.app.in_protocol.decompose_incoming_envelope(ctx)
- if ctx.service_class != None:
- ctx.service_class.event_manager.fire_event('decompose_envelope',
- ctx)
+ # sets the ctx.in_object and ctx.in_header
self.app.in_protocol.deserialize(ctx)
except Fault,e:
@@ -68,6 +68,8 @@ def get_out_object(self, ctx):
"""Calls the matched method using the ctx.in_object to get
ctx.out_object."""
+
+ # event firing is done in the rpclib.application.Application
self.app.process_request(ctx)
def get_out_string(self, ctx):
View
67 src/rpclib/server/wsgi.py
@@ -17,7 +17,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
#
-"""An rpc server that uses http as transport, and wsgi as bridge api"""
+"""An rpc server that uses http as transport, and wsgi as bridge api."""
# FIXME: this is maybe still too soap-centric.
@@ -62,27 +62,69 @@ def reconstruct_wsgi_request(http_env):
class WsgiTransportContext(TransportContext):
+ """The class that is used in the transport attribute of the
+ :class:`WsgiMethodContext` class."""
+
def __init__(self, req_env, content_type):
TransportContext.__init__(self, 'wsgi')
self.req_env = req_env
+ """WSGI Request environment"""
+
self.resp_headers = {
'Content-Type': content_type,
'Content-Length': '0',
}
+ """HTTP Response headers."""
+
self.resp_code = None
+ """HTTP Response code."""
+
self.req_method = req_env.get('REQUEST_METHOD', None)
+ """HTTP Request verb, as a convenience to users."""
+
+ self.wsdl = None
+ """The WSDL document that is being returned. Only relevant when handling
+ WSDL requests."""
+
self.wsdl_error = None
+ """The error when handling WSDL requests."""
class WsgiMethodContext(MethodContext):
+ """The WSGI-Specific method context. WSGI-Specific information is stored in
+ the transport attribute using the :class:`WsgiTransportContext` class."""
+
def __init__(self, app, req_env, content_type):
MethodContext.__init__(self, app)
self.transport = WsgiTransportContext(req_env, content_type)
-
+ """Holds the WSGI-specific information"""
class WsgiApplication(ServerBase):
+ '''A `PEP-3333 <http://www.python.org/dev/peps/pep-3333/#preface-for-readers-of-pep-333>`_
+ compliant callable class.
+
+ Supported events:
+ * ``wsdl``
+ Called right before the wsdl data is returned to the client.
+
+ * ``wsdl_exception``
+ Called right after an exception is thrown during wsdl generation. The
+ exception object is stored in ctx.transport.wsdl_error attribute.
+
+ * ``wsgi_call``
+ Called first when the incoming http request is identified as a rpc
+ request.
+
+ * ``wsgi_return``
+ Called right before the output stream is returned to the WSGI handler.
+
+ * ``wsgi_resource_not_found``
+ Called right before returning a 404 when the requested resource was not
+ found.
+ '''
+
transport = 'http://schemas.xmlsoap.org/soap/http'
def __init__(self, app):
@@ -129,21 +171,20 @@ def __is_wsdl_request(self, req_env):
def __handle_wsdl_request(self, req_env, start_response, url):
ctx = WsgiMethodContext(self.app, req_env, 'text/xml; charset=utf-8')
-
try:
- wsdl = self.app.interface.get_interface_document()
- if wsdl is None:
+ ctx.transport.wsdl = self.app.interface.get_interface_document()
+ if ctx.transport.wsdl is None:
self.app.interface.build_interface_document(url)
- wsdl = self.app.interface.get_interface_document()
+ ctx.transport.wsdl = self.app.interface.get_interface_document()
- assert wsdl != None
+ assert ctx.transport.wsdl != None
- self.event_manager.fire_event('wsdl',ctx) # implementation hook
+ self.event_manager.fire_event('wsdl', ctx) # implementation hook
- ctx.transport.resp_headers['Content-Length'] = str(len(wsdl))
+ ctx.transport.resp_headers['Content-Length'] = str(len(ctx.transport.wsdl))
start_response(HTTP_200, ctx.transport.resp_headers.items())
- return [wsdl]
+ return [ctx.transport.wsdl]
except Exception, e:
logger.exception(e)
@@ -194,9 +235,6 @@ def __handle_rpc(self, req_env, start_response):
self.get_out_string(ctx)
- # implementation hook
- self.event_manager.fire_event('wsgi_return', ctx)
-
if ctx.descriptor and ctx.descriptor.mtom:
# when there is more than one return type, the result is
# encapsulated inside a list. when there's just one, the result
@@ -213,6 +251,9 @@ def __handle_rpc(self, req_env, start_response):
out_object
)
+ # implementation hook
+ self.event_manager.fire_event('wsgi_return', ctx)
+
# We can't set the content-length if we want to support any kind of
# python iterable as output. We can't iterate and count, that defeats
# the whole point.
View
31 src/rpclib/service.py
@@ -61,6 +61,37 @@ class ServiceBase(object):
It is a natural abstract base class, because it's of no use without any
method definitions, hence the 'Base' suffix in the name.
+
+ The WsgiApplication class supports the following events:
+ * ``method_call``
+ Called right before the service method is executed
+
+ * ``method_return_object``
+ Called right after the service method is executed
+
+ * ``method_exception_object``
+ Called when an exception occurred in a service method, before the
+ exception is serialized.
+
+ * ``method_accept_document``
+ Called by the transport right after the incoming stream is parsed to
+ the incoming protocol's document type.
+
+ * ``method_return_document``
+ Called by the transport right after the outgoing object is
+ serialized to the outgoing protocol's document type.
+
+ * ``method_exception_document``
+ Called by the transport right before the outgoing exception object
+ is serialized to the outgoing protocol's document type.
+
+ * ``method_return_string``
+ Called by the transport right before passing the return string to
+ the client.
+
+ * ``method_exception_string``
+ Called by the transport right before passing the exception string to
+ the client.
'''
__metaclass__ = ServiceBaseMeta
View
25 src/rpclib/test/test_sqla.py
@@ -281,5 +281,30 @@ class UserMail(User):
assert 'name' in UserMail._type_info
assert 'id' in UserMail._type_info
+ def test_relationship(self):
+ import sqlalchemy
+
+ class User(self.DeclarativeBase, TableSerializer):
+ __tablename__ = 'rpclib_user'
+
+ id = Column(sqlalchemy.Integer, primary_key=True)
+ name = Column(sqlalchemy.String(256))
+
+ class Address(self.DeclarativeBase, TableSerializer):
+ __tablename__ = 'rpclib_address'
+ id = Column(sqlalchemy.Integer, primary_key=True)
+ address = Column(sqlalchemy.String(256))
+ user_id = Column(sqlalchemy.Integer, ForeignKey(User.id), nullable=False)
+ user = relationship(User)
+
+ assert 'user' in Address._type_info
+ assert Address._type_info['user'] is User
+
+ u = User()
+ a = Address()
+ a.user = u
+
+
+
if __name__ == '__main__':
unittest.main()
Something went wrong with that request. Please try again.