[pkg] Set version to 3.22.2.dev0
So that cubes used in test dependencies do not install a released CubicWeb.
# copyright 2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr -- mailto:contact@logilab.fr## This program is free software: you can redistribute it and/or modify it under# the terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## This program is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with this program. If not, see <http://www.gnu.org/licenses/>."""Data import of external entities.Main entry points:.. autoclass:: ExtEntitiesImporter.. autoclass:: ExtEntityUtilities:.. autofunction:: cwuri2eid.. autoclass:: RelationMapping.. autofunction:: cubicweb.dataimport.importer.use_extid_as_cwuri"""fromcollectionsimportdefaultdictimportloggingfromlogilab.mtconverterimportxml_escapedefcwuri2eid(cnx,etypes,source_eid=None):"""Return a dictionary mapping cwuri to eid for entities of the given entity types and / or source. """assertsource_eidoretypes,'no entity types nor source specified'rql='Any U, X WHERE X cwuri U'args={}iflen(etypes)==1:rql+=', X is %s'%etypes[0]elifetypes:rql+=', X is IN (%s)'%','.join(etypes)ifsource_eidisnotNone:rql+=', X cw_source S, S eid %(s)s'args['s']=source_eidreturndict(cnx.execute(rql,args))defuse_extid_as_cwuri(extid2eid):"""Return a generator of :class:`ExtEntity` objects that will set `cwuri` using entity's extid if the entity does not exist yet and has no `cwuri` defined. `extid2eid` is an extid to eid dictionary coming from an :class:`ExtEntitiesImporter` instance. Example usage: .. code-block:: python importer = SKOSExtEntitiesImporter(cnx, store, import_log) set_cwuri = use_extid_as_cwuri(importer.extid2eid) importer.import_entities(set_cwuri(extentities)) """defuse_extid_as_cwuri_filter(extentities):forextentityinextentities:ifextentity.extidnotinextid2eid:extentity.values.setdefault('cwuri',set([extentity.extid.decode('utf-8')]))yieldextentityreturnuse_extid_as_cwuri_filterclassRelationMapping(object):"""Read-only mapping from relation type to set of related (subject, object) eids. If `source` is specified, only returns relations implying entities from this source. """def__init__(self,cnx,source=None):self.cnx=cnxself._rql_template='Any S,O WHERE S %s O'self._kwargs={}ifsourceisnotNone:self._rql_template+=', S cw_source SO, O cw_source SO, SO eid %%(s)s'self._kwargs['s']=source.eiddef__getitem__(self,rtype):"""Return a set of (subject, object) eids already related by `rtype`"""rql=self._rql_template%rtypereturnset(tuple(x)forxinself.cnx.execute(rql,self._kwargs))classExtEntity(object):"""Transitional representation of an entity for use in data importer. An external entity has the following properties: * ``extid`` (external id), an identifier for the ext entity, * ``etype`` (entity type), a string which must be the name of one entity type in the schema (eg. ``'Person'``, ``'Animal'``, ...), * ``values``, a dictionary whose keys are attribute or relation names from the schema (eg. ``'first_name'``, ``'friend'``), and whose values are *sets* For instance: .. code-block:: python ext_entity.extid = 'http://example.org/person/debby' ext_entity.etype = 'Person' ext_entity.values = {'first_name': set([u"Deborah", u"Debby"]), 'friend': set(['http://example.org/person/john'])} """def__init__(self,etype,extid,values=None):self.etype=etypeself.extid=extidifvaluesisNone:values={}self.values=valuesself._schema=Nonedef__repr__(self):return'<%s%s%s>'%(self.etype,self.extid,self.values)defiter_rdefs(self):"""Yield (key, rtype, role) defined in `.values` dict, with: * `key` is the original key in `.values` (i.e. the relation type or a 2-uple (relation type, role)) * `rtype` is a yams relation type, expected to be found in the schema (attribute or relation) * `role` is the role of the entity in the relation, 'subject' or 'object' Iteration is done on a copy of the keys so values may be inserted/deleted during it. """forkeyinlist(self.values):ifisinstance(key,tuple):rtype,role=keyassertrolein('subject','object'),keyyieldkey,rtype,roleelse:yieldkey,key,'subject'defprepare(self,schema):"""Prepare an external entity for later insertion: * ensure attributes and inlined relations have a single value * turn set([value]) into value and remove key associated to empty set * remove non inlined relations and return them as a [(e1key, relation, e2key)] list Return a list of non inlined relations that may be inserted later, each relations defined by a 3-tuple (subject extid, relation type, object extid). Take care the importer may call this method several times. """assertself._schemaisNone,'prepare() has already been called for %s'%selfself._schema=schemaeschema=schema.eschema(self.etype)deferred=[]entity_dict=self.valuesforkey,rtype,roleinself.iter_rdefs():rschema=schema.rschema(rtype)ifrschema.finalor(rschema.inlinedandrole=='subject'):assertlen(entity_dict[key])<=1, \"more than one value for %s: %s (%s)"%(rtype,entity_dict[key],self.extid)ifentity_dict[key]:entity_dict[rtype]=entity_dict[key].pop()ifkey!=rtype:delentity_dict[key]if(rschema.finalandeschema.has_metadata(rtype,'format')andnotrtype+'_format'inentity_dict):entity_dict[rtype+'_format']=u'text/plain'else:delentity_dict[key]else:fortarget_extidinentity_dict.pop(key):ifrole=='subject':deferred.append((self.extid,rtype,target_extid))else:deferred.append((target_extid,rtype,self.extid))returndeferreddefis_ready(self,extid2eid):"""Return True if the ext entity is ready, i.e. has all the URIs used in inlined relations currently existing. """assertself._schema,'prepare() method should be called first on %s'%self# as .prepare has been called, we know that .values only contains subject relation *type* as# key (no more (rtype, role) tuple)schema=self._schemaentity_dict=self.valuesforrtypeinentity_dict:rschema=schema.rschema(rtype)ifnotrschema.final:# .prepare() should drop other cases from the entity dictassertrschema.inlinedifnotentity_dict[rtype]inextid2eid:returnFalse# entity is ready, replace all relation's extid by eidsforrtypeinentity_dict:rschema=schema.rschema(rtype)ifrschema.inlined:entity_dict[rtype]=extid2eid[entity_dict[rtype]]returnTrueclassExtEntitiesImporter(object):"""This class is responsible for importing externals entities, that is instances of :class:`ExtEntity`, into CubicWeb entities. :param schema: the CubicWeb's instance schema :param store: a CubicWeb `Store` :param extid2eid: optional {extid: eid} dictionary giving information on existing entities. It will be completed during import. You may want to use :func:`cwuri2eid` to build it. :param existing_relation: optional {rtype: set((subj eid, obj eid))} mapping giving information on existing relations of a given type. You may want to use :class:`RelationMapping` to build it. :param etypes_order_hint: optional ordered iterable on entity types, giving an hint on the order in which they should be attempted to be imported :param import_log: optional object implementing the :class:`SimpleImportLog` interface to record events occuring during the import :param raise_on_error: optional boolean flag - default to false, indicating whether errors should be raised or logged. You usually want them to be raised during test but to be logged in production. Instances of this class are meant to import external entities through :meth:`import_entities` which handles a stream of :class:`ExtEntity`. One may then plug arbitrary filters into the external entities stream. .. automethod:: import_entities """def__init__(self,schema,store,extid2eid=None,existing_relations=None,etypes_order_hint=(),import_log=None,raise_on_error=False):self.schema=schemaself.store=storeself.extid2eid=extid2eidifextid2eidisnotNoneelse{}self.existing_relations=(existing_relationsifexisting_relationsisnotNoneelsedefaultdict(set))self.etypes_order_hint=etypes_order_hintifimport_logisNone:import_log=SimpleImportLog('<unspecified>')self.import_log=import_logself.raise_on_error=raise_on_error# set of created/updated eidsself.created=set()self.updated=set()defimport_entities(self,ext_entities):"""Import given external entities (:class:`ExtEntity`) stream (usually a generator)."""# {etype: [etype dict]} of entities that are in the import queuequeue={}# order entity dictionaries then create/update themdeferred=self._import_entities(ext_entities,queue)# create deferred relations that don't exist alreadymissing_relations=self.prepare_insert_deferred_relations(deferred)self._warn_about_missing_work(queue,missing_relations)def_import_entities(self,ext_entities,queue):extid2eid=self.extid2eiddeferred={}# non inlined relations that may be deferredself.import_log.record_debug('importing entities')forext_entityinself.iter_ext_entities(ext_entities,deferred,queue):try:eid=extid2eid[ext_entity.extid]exceptKeyError:self.prepare_insert_entity(ext_entity)else:ifext_entity.values:self.prepare_update_entity(ext_entity,eid)returndeferreddefiter_ext_entities(self,ext_entities,deferred,queue):"""Yield external entities in an order which attempts to satisfy schema constraints (inlined / cardinality) and to optimize the import. """schema=self.schemaextid2eid=self.extid2eidforext_entityinext_entities:# check data in the transitional representation and prepare it for# later insertion in the databaseforsubject_uri,rtype,object_uriinext_entity.prepare(schema):deferred.setdefault(rtype,set()).add((subject_uri,object_uri))ifnotext_entity.is_ready(extid2eid):queue.setdefault(ext_entity.etype,[]).append(ext_entity)continueyieldext_entity# check for some entities in the queue that may now be ready. We'll have to restart# search for ready entities until no one is generatednew=Truewhilenew:new=Falseforetypeinself.etypes_order_hint:ifetypeinqueue:new_queue=[]forext_entityinqueue[etype]:ifext_entity.is_ready(extid2eid):yieldext_entity# may unlock entity previously handled within this loopnew=Trueelse:new_queue.append(ext_entity)ifnew_queue:queue[etype][:]=new_queueelse:delqueue[etype]defprepare_insert_entity(self,ext_entity):"""Call the store to prepare insertion of the given external entity"""eid=self.store.prepare_insert_entity(ext_entity.etype,**ext_entity.values)self.extid2eid[ext_entity.extid]=eidself.created.add(eid)returneiddefprepare_update_entity(self,ext_entity,eid):"""Call the store to prepare update of the given external entity"""self.store.prepare_update_entity(ext_entity.etype,eid,**ext_entity.values)self.updated.add(eid)defprepare_insert_deferred_relations(self,deferred):"""Call the store to insert deferred relations (not handled during insertion/update for entities). Return a list of relations `[(subj ext id, obj ext id)]` that may not be inserted because the target entities don't exists yet. """prepare_insert_relation=self.store.prepare_insert_relationrschema=self.schema.rschemaextid2eid=self.extid2eidmissing_relations=[]forrtype,relationsindeferred.items():self.import_log.record_debug('importing %s%s relations'%(len(relations),rtype))symmetric=rschema(rtype).symmetricexisting=self.existing_relations[rtype]forsubject_uri,object_uriinrelations:try:subject_eid=extid2eid[subject_uri]object_eid=extid2eid[object_uri]exceptKeyError:missing_relations.append((subject_uri,rtype,object_uri))continueif(subject_eid,object_eid)notinexisting:prepare_insert_relation(subject_eid,rtype,object_eid)existing.add((subject_eid,object_eid))ifsymmetric:existing.add((object_eid,subject_eid))returnmissing_relationsdef_warn_about_missing_work(self,queue,missing_relations):error=self.import_log.record_errorifqueue:msgs=["can't create some entities, is there some cycle or ""missing data?"]forext_entitiesinqueue.values():forext_entityinext_entities:msgs.append(str(ext_entity))map(error,msgs)ifself.raise_on_error:raiseException('\n'.join(msgs))ifmissing_relations:msgs=["can't create some relations, is there missing data?"]forsubject_uri,rtype,object_uriinmissing_relations:msgs.append("%s%s%s"%(subject_uri,rtype,object_uri))map(error,msgs)ifself.raise_on_error:raiseException('\n'.join(msgs))classSimpleImportLog(object):"""Fake CWDataImport log using a simple text format. Useful to display logs in the UI instead of storing them to the database. """def__init__(self,filename):self.logs=[]self.filename=filenamedefrecord_debug(self,msg,path=None,line=None):self._log(logging.DEBUG,msg,path,line)defrecord_info(self,msg,path=None,line=None):self._log(logging.INFO,msg,path,line)defrecord_warning(self,msg,path=None,line=None):self._log(logging.WARNING,msg,path,line)defrecord_error(self,msg,path=None,line=None):self._log(logging.ERROR,msg,path,line)defrecord_fatal(self,msg,path=None,line=None):self._log(logging.FATAL,msg,path,line)def_log(self,severity,msg,path,line):encodedmsg=u'%s\t%s\t%s\t%s'%(severity,self.filename,lineoru'',msg)self.logs.append(encodedmsg)classHTMLImportLog(SimpleImportLog):"""Fake CWDataImport log using a simple HTML format."""def__init__(self,filename):super(HTMLImportLog,self).__init__(xml_escape(filename))def_log(self,severity,msg,path,line):encodedmsg=u'%s\t%s\t%s\t%s<br/>'%(severity,self.filename,lineoru'',xml_escape(msg))self.logs.append(encodedmsg)