"""Adapter for google appengine source.:organization: Logilab:copyright: 2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"fromlogilab.common.decoratorsimportcached,clear_cachefromcubicwebimportAuthenticationError,UnknownEid,serverfromcubicweb.server.sourcesimportAbstractSource,ConnectionWrapperfromcubicweb.server.poolimportSingleOperationfromcubicweb.server.utilsimportcrypt_passwordfromcubicweb.goa.dbinitimportset_user_groupsfromcubicweb.goa.rqlinterpreterimportRQLInterpreterfromgoogle.appengine.api.datastoreimportKey,Entity,Get,Put,Deletefromgoogle.appengine.api.datastoreimportQueryfromgoogle.appengine.apiimportdatastore_errors,usersdef_init_groups(guser,euser):# set default groupsifguserisNone:groups=['guests']else:groups=['users']ifusers.is_current_user_admin():groups.append('managers')set_user_groups(euser,groups)def_clear_related_cache(session,gaesubject,rtype,gaeobject):subject,object=str(gaesubject.key()),str(gaeobject.key())foreid,rolein((subject,'subject'),(object,'object')):# clear related cache if necessarytry:entity=session.entity_cache(eid)exceptKeyError:passelse:entity.clear_related_cache(rtype,role)ifgaesubject.kind()=='EUser':forasessioninsession.repo._sessions.itervalues():ifasession.user.eid==subject:asession.user.clear_related_cache(rtype,'subject')ifgaeobject.kind()=='EUser':forasessioninsession.repo._sessions.itervalues():ifasession.user.eid==object:asession.user.clear_related_cache(rtype,'object')def_mark_modified(session,gaeentity):modified=session.query_data('modifiedentities',{},setdefault=True)modified[str(gaeentity.key())]=gaeentityDatastorePutOp(session)def_rinfo(session,subject,rtype,object):gaesubj=session.datastore_get(subject)gaeobj=session.datastore_get(object)rschema=session.vreg.schema.rschema(rtype)cards=rschema.rproperty(gaesubj.kind(),gaeobj.kind(),'cardinality')returngaesubj,gaeobj,cardsdef_radd(session,gaeentity,targetkey,relation,card):ifcardin'?1':gaeentity[relation]=targetkeyelse:try:related=gaeentity[relation]exceptKeyError:related=[]else:ifrelatedisNone:related=[]related.append(targetkey)gaeentity[relation]=related_mark_modified(session,gaeentity)def_rdel(session,gaeentity,targetkey,relation,card):ifcardin'?1':gaeentity[relation]=Noneelse:related=gaeentity[relation]ifrelatedisnotNone:related=[keyforkeyinrelatedifnotkey==targetkey]gaeentity[relation]=relatedorNone_mark_modified(session,gaeentity)classDatastorePutOp(SingleOperation):"""delayed put of entities to have less datastore write api calls * save all modified entities at precommit (should be the first operation processed, hence the 0 returned by insert_index()) * in case others precommit operations modify some entities, resave modified entities at commit. This suppose that no db changes will occurs during commit event but it should be the case. """definsert_index(self):return0def_put_entities(self):pending=self.session.query_data('pendingeids',())modified=self.session.query_data('modifiedentities',{})foreid,gaeentityinmodified.iteritems():assertnoteidinpendingPut(gaeentity)modified.clear()defcommit_event(self):self._put_entities()defprecommit_event(self):self._put_entities()classGAESource(AbstractSource):"""adapter for a system source on top of google appengine datastore"""passwd_rql="Any P WHERE X is EUser, X login %(login)s, X upassword P"auth_rql="Any X WHERE X is EUser, X login %(login)s, X upassword %(pwd)s"_sols=({'X':'EUser','P':'Password'},)options=()def__init__(self,repo,appschema,source_config,*args,**kwargs):AbstractSource.__init__(self,repo,appschema,source_config,*args,**kwargs)ifrepo.config['use-google-auth']:self.info('using google authentication service')self.authenticate=self.authenticate_gauthelse:self.authenticate=self.authenticate_localdefreset_caches(self):"""method called during test to reset potential source caches"""passdefinit_creating(self):passdefinit(self):# XXX unregister unsupported hooksfromcubicweb.server.hooksimportsync_owner_after_add_composite_relationself.repo.hm.unregister_hook(sync_owner_after_add_composite_relation,'after_add_relation','')defget_connection(self):returnConnectionWrapper()# ISource interface #######################################################defcompile_rql(self,rql):rqlst=self.repo.querier._rqlhelper.parse(rql)rqlst.restricted_vars=()rqlst.children[0].solutions=self._solsreturnrqlstdefset_schema(self,schema):"""set the application'schema"""self.interpreter=RQLInterpreter(schema)self.schema=schemaif'EUser'inschemaandnotself.repo.config['use-google-auth']:# rql syntax trees used to authenticate usersself._passwd_rqlst=self.compile_rql(self.passwd_rql)self._auth_rqlst=self.compile_rql(self.auth_rql)defsupport_entity(self,etype,write=False):"""return true if the given entity's type is handled by this adapter if write is true, return true only if it's a RW support """returnTruedefsupport_relation(self,rtype,write=False):"""return true if the given relation's type is handled by this adapter if write is true, return true only if it's a RW support """returnTruedefauthenticate_gauth(self,session,login,password):guser=users.get_current_user()# allowing or not anonymous connection should be done in the app.yaml# file, suppose it's authorized if we are thereifguserisNone:login=u'anonymous'else:login=unicode(guser.nickname())# XXX http://code.google.com/appengine/docs/users/userobjects.html# use a reference property to automatically work with email address# changes after the propagation feature is implementedkey=Key.from_path('EUser','key_'+login,parent=None)try:euser=session.datastore_get(key)# XXX fix user. Required until we find a better way to fix broken recordsifnoteuser.get('s_in_group'):_init_groups(guser,euser)Put(euser)returnstr(key)exceptdatastore_errors.EntityNotFoundError:# create a record for this usereuser=Entity('EUser',name='key_'+login)euser['s_login']=login_init_groups(guser,euser)Put(euser)returnstr(euser.key())defauthenticate_local(self,session,login,password):"""return EUser eid for the given login/password if this account is defined in this source, else raise `AuthenticationError` two queries are needed since passwords are stored crypted, so we have to fetch the salt first """args={'login':login,'pwd':password}ifpasswordisnotNone:rset=self.syntax_tree_search(session,self._passwd_rqlst,args)try:pwd=rset[0][0]exceptIndexError:raiseAuthenticationError('bad login')# passwords are stored using the bytea type, so we get a StringIOifpwdisnotNone:args['pwd']=crypt_password(password,pwd[:2])# get eid from login and (crypted) passwordrset=self.syntax_tree_search(session,self._auth_rqlst,args)try:returnrset[0][0]exceptIndexError:raiseAuthenticationError('bad password')defsyntax_tree_search(self,session,union,args=None,cachekey=None,varmap=None):"""return result from this source for a rql query (actually from a rql syntax tree and a solution dictionary mapping each used variable to a possible type). If cachekey is given, the query necessary to fetch the results (but not the results themselves) may be cached using this key. """results,description=self.interpreter.interpret(union,args,session.datastore_get)returnresults# XXX descriptiondefflying_insert(self,table,session,union,args=None,varmap=None):raiseNotImplementedErrordefadd_entity(self,session,entity):"""add a new entity to the source"""# do not delay add_entity as other modifications, new created entity# needs an eidentity.put()defupdate_entity(self,session,entity):"""replace an entity in the source"""gaeentity=entity.to_gae_model()_mark_modified(session,entity.to_gae_model())ifgaeentity.kind()=='EUser':forasessioninself.repo._sessions.itervalues():ifasession.user.eid==entity.eid:asession.user.update(dict(gaeentity))defdelete_entity(self,session,etype,eid):"""delete an entity from the source"""# do not delay delete_entity as other modifications to ensure# consistencykey=Key(eid)Delete(key)session.clear_datastore_cache(key)session.drop_entity_cache(eid)session.query_data('modifiedentities',{}).pop(eid,None)defadd_relation(self,session,subject,rtype,object):"""add a relation to the source"""gaesubj,gaeobj,cards=_rinfo(session,subject,rtype,object)_radd(session,gaesubj,gaeobj.key(),'s_'+rtype,cards[0])_radd(session,gaeobj,gaesubj.key(),'o_'+rtype,cards[1])_clear_related_cache(session,gaesubj,rtype,gaeobj)defdelete_relation(self,session,subject,rtype,object):"""delete a relation from the source"""gaesubj,gaeobj,cards=_rinfo(session,subject,rtype,object)pending=session.query_data('pendingeids',set(),setdefault=True)ifnotsubjectinpending:_rdel(session,gaesubj,gaeobj.key(),'s_'+rtype,cards[0])ifnotobjectinpending:_rdel(session,gaeobj,gaesubj.key(),'o_'+rtype,cards[1])_clear_related_cache(session,gaesubj,rtype,gaeobj)# system source interface #################################################defeid_type_source(self,session,eid):"""return a tuple (type, source, extid) for the entity with id <eid>"""try:key=Key(eid)exceptdatastore_errors.BadKeyError:raiseUnknownEid(eid)returnkey.kind(),'system',Nonedefcreate_eid(self,session):returnNone# let the datastore generating keydefadd_info(self,session,entity,source,extid=None):"""add type and source info for an eid into the system table"""passdefdelete_info(self,session,eid,etype,uri,extid):"""delete system information on deletion of an entity by transfering record from the entities table to the deleted_entities table """passdeffti_unindex_entity(self,session,eid):"""remove text content for entity with the given eid from the full text index """passdeffti_index_entity(self,session,entity):"""add text content of a created/modified entity to the full text index """pass