[Grok-dev] Grok with existing SQLAlchemy mapped classes
Kevin Teague
kevin at bud.ca
Thu Jul 30 19:35:02 EDT 2009
Yes, I had the same requirements, that the classes that the ORM is
mapping onto don't have any Grok (or web-specific) dependencies. In my
case, this was for a bioinformatic setting, where some developer's
were working with the model from command-line scripts, and other
developer's needed to display that same model in an internal-only web
application.
For the "core" model, I made a package who's only dependency is on SQL
Alchemy. Inside that package the mappings are defined. There are
functions for:
def setup_tables(metadata, tables):
...
def setup_mappers(tables, mappers):
...
Then in the web-specific package which depends upon Grok and
collective.lead there is a class to provide the IDatabase interface
for collective.lead which simply references the core package:
class SbsDatabase(grok.GlobalUtility, Database):
grok.provides(IDatabase)
grok.name('sbs')
@property
def _url(self):
conf = SolexaConfig()
db = conf.solexa_metadata_db
return URL( drivername='postgres',
username=db['username'],
password=db['password'],
host=db['host'],
port=None,
database=db['name']
)
def _setup_tables(self, metadata, tables):
"""
Map the database structure to SQLAlchemy Table objects
"""
solexa.model.saconfig.setup_tables(metadata, tables)
def _setup_mappers(self, tables, mappers):
"""
Map the database Tables to SQLAlchemy Mapper objects
"""
solexa.model.saconfig.setup_mappers(tables, mappers)
Then the next step is to map the model classes to URLs, so that
traversal and view.url() works as expected. Here I wrote module which
is kinda messy (some of the code below is just noise ... it could use
a bit of a clean-up) but it does the job for our somewhat modest
requirements. The key is to provide traversers that know how to get
from a URL to an SQL Alchemy model, and then when those model objects
are instantiated use the zope.location.location.locate() function to
give the object the correct __name__ and __parent__ attribtues.
Overall, I'm pretty happy with this solution though ... we've had many
projects in my eight years in bioinformatics where we have "command-
line only" developers and "web only" developers, and they've always
ended up developing their own logic specific to "web" or "command-
line" and maintenance always becomes a royal PITA. By keeping the
dependency on the core model package to just SQL Alchemy the command-
line only people don't get freaked out by a bunch of web-specific code
and we can get the dev's to collaborate on a shared model :)
"""
The sbs.locate module handles mapping from SQL Alchemy objects
into our URL structure.
"""
import grok
import zope.location.location
from collective.lead.interfaces import IDatabase
import solexa.model
import sbs.app
import sbs.interfaces
mappings = {
'libraries': (sbs.interfaces.ILibraries, solexa.model.Library,
'name'),
'projects': (sbs.interfaces.IProjects, solexa.model.Project,
'id'),
'protocols': (sbs.interfaces.IProtocols,
solexa.model.Protocol, 'id'),
'adaptor_protocols': (sbs.interfaces.IProtocols,
solexa.model.AdaptorProtocol, 'id'),
'flowcells': (sbs.interfaces.IFlowcells,
solexa.model.Flowcell, 'lims_flowcell_code'),
'runqueue': (sbs.interfaces.IRunQueue, solexa.model.RunQueue,
None),
'maqqueue': (sbs.interfaces.IMaqQueue, solexa.model.MaqQueue,
None),
}
def locate(context, obj):
"""
Locate an SQL Alchemy object with our URL scheme
"""
container = sbs.app.SQLContainer()
# walk up the __parent__'s of the context until we get
# to the ISequencingTechnology object (e.g. Illumina or SOLID)
seqtech = context
while not sbs.interfaces.ISequencingTechnology.providedBy
(seqtech):
seqtech = seqtech.__parent__
for key, value in mappings.items():
klass = value[1]
if isinstance(obj, klass):
name = key
primary_key_name = value[2]
zope.interface.alsoProvides(container, value[0])
container.__parent__ = seqtech
container.__name__ = name
obj.__name__ = str(getattr(obj, primary_key_name))
obj.__parent__ = container
class SQLContainerTraverser(grok.Traverser):
"""
Each SBS technology (e.g. Illumina, SOLID) has a simple URL
space that is
the name of the table, then the name of the id for a record in
that table.
For example:
/projects/10 - Project #10
/libraries/MM0246 - Library MM0246
/protocols/SLX-SAGE - Procotol SLX-SAGE
This traverser will support traversal to just the table
objects.
A second traverser will support navigation into individual SQL
Alchemy
objects.
"""
grok.context(sbs.interfaces.ISequencingTechnology)
mappings = mappings
def traverse(self, name):
container = sbs.app.SQLContainer()
# assign __parent__ and __name__ attributes
# URL stands for Uniform Resource Location
zope.location.location.locate(container, self.context,
name)
if self.mappings.has_key(name):
# declare that the generic container has a specific
interface
zope.interface.alsoProvides(container, self.mappings
[name][0])
container.model_class = self.mappings[name][1]
container.primary_key_fieldname = self.mappings[name]
[2]
return container
else:
return None
class SQLModelTraverser(grok.Traverser):
"""
Based on the Container that represents a SQL Table, fetch a
model object
from that table, and give that model a location. Also mark the
model
object as providing the ISQLModel interface so that Views for
that model
can have the default view name of 'index'.
"""
grok.context(sbs.app.SQLContainer)
def traverse(self, name):
session = zope.component.getUtility(IDatabase,
'sbs').session
model = session.query(
self.context.model_class).filter(
getattr(self.context.model_class,
self.context.primary_key_fieldname) == name
).first()
if model:
zope.interface.alsoProvides(model,
sbs.interfaces.ISQLModel)
zope.location.location.locate(model, self.context,
name)
return model
More information about the Grok-dev
mailing list