Merge 3.27
authorPhilippe Pepiot <philippe.pepiot@logilab.fr>
Thu, 05 Mar 2020 10:15:38 +0100
changeset 12900 2cc3f481ecd0
parent 12892 0df0db725f07 (diff)
parent 12899 af59ca8b23d0 (current diff)
child 12913 ebf4806e4ab7
Merge 3.27
cubicweb/__pkginfo__.py
cubicweb/server/hook.py
cubicweb/web/action.py
doc/tutorials/advanced/part02_security.rst
--- a/cubicweb/__pkginfo__.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/__pkginfo__.py	Thu Mar 05 10:15:38 2020 +0100
@@ -22,8 +22,8 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 27, 2)
-version = '.'.join(str(num) for num in numversion)
+numversion = (3, 28, 0)
+version = '.'.join(str(num) for num in numversion) + '.dev0'
 
 description = "a repository of entities / relations for knowledge management"
 author = "Logilab"
--- a/cubicweb/cwvreg.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/cwvreg.py	Thu Mar 05 10:15:38 2020 +0100
@@ -550,7 +550,7 @@
         return RQLHelper(self.schema,
                          special_relations={'eid': 'uid', 'has_text': 'fti'})
 
-    def solutions(self, req, rqlst, args):
+    def compute_var_types(self, req, rqlst, args):
         def type_from_eid(eid, req=req):
             return req.entity_type(eid)
         return self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
@@ -558,7 +558,7 @@
     def parse(self, req, rql, args=None):
         rqlst = self.rqlhelper.parse(rql)
         try:
-            self.solutions(req, rqlst, args)
+            self.compute_var_types(req, rqlst, args)
         except UnknownEid:
             for select in rqlst.children:
                 select.solutions = []
--- a/cubicweb/devtools/__init__.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/devtools/__init__.py	Thu Mar 05 10:15:38 2020 +0100
@@ -624,14 +624,14 @@
     def init_test_database(self):
         """initialize a fresh postgresql database used for testing purpose"""
         from cubicweb.server import init_repository
-        from cubicweb.server.serverctl import system_source_cnx, createdb
+        from cubicweb.server.serverctl import source_cnx, createdb
         # connect on the dbms system base to create our base
         try:
             self._drop(self.system_source['db-name'])
             createdb(self.helper, self.system_source, self.dbcnx, self.cursor)
             self.dbcnx.commit()
-            cnx = system_source_cnx(self.system_source, special_privs='LANGUAGE C',
-                                    interactive=False)
+            cnx = source_cnx(self.system_source, special_privs='LANGUAGE C',
+                             interactive=False)
             templcursor = cnx.cursor()
             try:
                 # XXX factorize with db-create code
--- a/cubicweb/devtools/repotest.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/devtools/repotest.py	Thu Mar 05 10:15:38 2020 +0100
@@ -209,7 +209,7 @@
 
     def _prepare_plan(self, cnx, rql, kwargs=None):
         rqlst = self.repo.vreg.rqlhelper.parse(rql, annotate=True)
-        self.repo.vreg.solutions(cnx, rqlst, kwargs)
+        self.repo.vreg.compute_var_types(cnx, rqlst, kwargs)
         if rqlst.TYPE == 'select':
             self.repo.vreg.rqlhelper.annotate(rqlst)
             for select in rqlst.children:
--- a/cubicweb/entities/adapters.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/entities/adapters.py	Thu Mar 05 10:15:38 2020 +0100
@@ -20,18 +20,62 @@
 """
 from cubicweb import _
 
+from hashlib import sha1
 from itertools import chain
 
+from rdflib import URIRef, Literal
+
 from logilab.mtconverter import TransformError
-from logilab.common.decorators import cached
+from logilab.common.decorators import cached, cachedproperty
 
-from cubicweb import (Unauthorized, ValidationError, view, ViolatedConstraint,
+from cubicweb.entity import EntityAdapter
+from cubicweb import (Unauthorized, ValidationError, ViolatedConstraint,
                       UniqueTogetherError)
 from cubicweb.schema import constraint_name_for
 from cubicweb.predicates import is_instance, relation_possible, match_exception
 
+from cubicweb.rdf import NAMESPACES
 
-class IDublinCoreAdapter(view.EntityAdapter):
+
+class EntityRDFAdapter(EntityAdapter):
+    """EntityRDFAdapter is to be specialized for each entity that wants to
+    be converted to RDF using the mechanism from cubicweb.rdf
+    """
+    __abstract__ = True
+
+    def __init__(self, _cw, **kwargs):
+        super().__init__(_cw, **kwargs)
+        self.entity.complete()
+
+    @cachedproperty
+    def uri(self):
+        return self.entity.cwuri
+
+    def triples(self):
+        """return sequence of 3-tuple of rdflib identifiers"""
+        raise NotImplementedError()
+
+
+class CWUserFoafAdapter(EntityRDFAdapter):
+    __regid__ = "rdf.foaf"
+    __select__ = is_instance("CWUser")
+
+    def triples(self):
+        RDF = NAMESPACES["rdf"]
+        FOAF = NAMESPACES["foaf"]
+        uri = URIRef(self.uri)
+        yield (uri, RDF.type, FOAF.Person)
+        if self.entity.surname:
+            yield (uri, FOAF.familyName, Literal(self.entity.surname))
+        if self.entity.firstname:
+            yield (uri, FOAF.givenName, Literal(self.entity.firstname))
+        emailaddr = self.entity.cw_adapt_to("IEmailable").get_email()
+        if emailaddr:
+            email_digest = sha1(emailaddr.encode("utf-8")).hexdigest()
+            yield (uri, FOAF.mbox_sha1sum, Literal(email_digest))
+
+
+class IDublinCoreAdapter(EntityAdapter):
     __regid__ = 'IDublinCore'
     __select__ = is_instance('Any')
 
@@ -93,7 +137,7 @@
         return self._cw._(self._cw.vreg.property_value('ui.language'))
 
 
-class IEmailableAdapter(view.EntityAdapter):
+class IEmailableAdapter(EntityAdapter):
     __regid__ = 'IEmailable'
     __select__ = relation_possible('primary_email') | relation_possible('use_email')
 
@@ -126,7 +170,7 @@
                     for attr in self.allowed_massmail_keys())
 
 
-class INotifiableAdapter(view.EntityAdapter):
+class INotifiableAdapter(EntityAdapter):
     __regid__ = 'INotifiable'
     __select__ = is_instance('Any')
 
@@ -145,7 +189,7 @@
         return ()
 
 
-class IFTIndexableAdapter(view.EntityAdapter):
+class IFTIndexableAdapter(EntityAdapter):
     """standard adapter to handle fulltext indexing
 
     .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers
@@ -226,7 +270,7 @@
         maindict.setdefault(weight, []).extend(words)
 
 
-class IDownloadableAdapter(view.EntityAdapter):
+class IDownloadableAdapter(EntityAdapter):
     """interface for downloadable entities"""
     __regid__ = 'IDownloadable'
     __abstract__ = True
@@ -256,7 +300,7 @@
 
 
 # XXX should propose to use two different relations for children/parent
-class ITreeAdapter(view.EntityAdapter):
+class ITreeAdapter(EntityAdapter):
     """This adapter provides a tree interface.
 
     It has to be overriden to be configured using the tree_relation,
@@ -412,7 +456,7 @@
         return path
 
 
-class ISerializableAdapter(view.EntityAdapter):
+class ISerializableAdapter(EntityAdapter):
     """Adapter to serialize an entity to a bare python structure that may be
     directly serialized to e.g. JSON.
     """
@@ -441,7 +485,7 @@
 # error handling adapters ######################################################
 
 
-class IUserFriendlyError(view.EntityAdapter):
+class IUserFriendlyError(EntityAdapter):
     __regid__ = 'IUserFriendlyError'
     __abstract__ = True
 
--- a/cubicweb/entities/wfobjs.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/entities/wfobjs.py	Thu Mar 05 10:15:38 2020 +0100
@@ -24,7 +24,7 @@
 from logilab.common.decorators import cached, clear_cache
 
 from cubicweb.entities import AnyEntity, fetch_config
-from cubicweb.view import EntityAdapter
+from cubicweb.entity import EntityAdapter
 from cubicweb.predicates import relation_possible
 
 
--- a/cubicweb/entity.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/entity.py	Thu Mar 05 10:15:38 2020 +0100
@@ -1118,7 +1118,7 @@
         etypecls.fetch_rqlst(self._cw.user, select, searchedvar,
                              ordermethod=ordermethod)
         # from now on, we need variable type resolving
-        self._cw.vreg.solutions(self._cw, select, args)
+        self._cw.vreg.compute_var_types(self._cw, select, args)
         # insert RQL expressions for schema constraints into the rql syntax tree
         if vocabconstraints:
             cstrcls = (RQLVocabularyConstraint, RQLConstraint)
@@ -1314,6 +1314,28 @@
     def __set__(self, eobj, value):
         raise NotImplementedError
 
+# entity adapters #############################################################
+
+class Adapter(AppObject):
+    """base class for adapters"""
+    __registry__ = 'adapters'
+
+
+class EntityAdapter(Adapter):
+    """base class for entity adapters (eg adapt an entity to an interface)
+
+    An example would be:
+
+    >>> some_entity.cw_adapt_to('IDownloadable')
+    """
+    def __init__(self, _cw, **kwargs):
+        try:
+            self.entity = kwargs.pop('entity')
+        except KeyError:
+            self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
+                                                    kwargs.get('col') or 0)
+        Adapter.__init__(self, _cw, **kwargs)
+
 
 from logging import getLogger
 from cubicweb import set_log_methods
--- a/cubicweb/pyramid/predicates.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/pyramid/predicates.py	Thu Mar 05 10:15:38 2020 +0100
@@ -21,6 +21,8 @@
 """Contains predicates used in Pyramid views.
 """
 
+from cubicweb._exceptions import UnknownEid
+
 
 class MatchIsETypePredicate(object):
     """A predicate that match if a given etype exist in schema.
@@ -38,5 +40,30 @@
             request.registry['cubicweb.registry'].case_insensitive_etypes
 
 
+class MatchIsEIDPredicate(object):
+    """A predicate that match if a given eid exist in the database.
+    """
+    def __init__(self, matchname, config):
+        self.matchname = matchname
+
+    def text(self):
+        return 'match_is_eid = %s' % self.matchname
+
+    phash = text
+
+    def __call__(self, info, request):
+        try:
+            eid = int(info['match'][self.matchname])
+        except ValueError:
+            return False
+
+        try:
+            request.cw_cnx.entity_from_eid(eid)
+        except UnknownEid:
+            return False
+        return True
+
+
 def includeme(config):
     config.add_route_predicate('match_is_etype', MatchIsETypePredicate)
+    config.add_route_predicate('match_is_eid', MatchIsEIDPredicate)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/rdf.py	Thu Mar 05 10:15:38 2020 +0100
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# copyright 2019 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/>.
+
+from rdflib import ConjunctiveGraph, plugin
+from rdflib.namespace import Namespace, RDF, FOAF
+import rdflib_jsonld  # noqa
+
+plugin.register("jsonld", plugin.Serializer, "rdflib_jsonld.serializer", "JsonLDSerializer")
+
+RDF_MIMETYPE_TO_FORMAT = {
+    'application/rdf+xml': 'xml',
+    'text/turtle': 'turtle',
+    'text/n3': 'n3',
+    'application/n-quads': 'nquads',
+    'application/n-triples': 'nt',
+    'application/trig': 'trig',
+    'application/ld+json': 'json-ld',
+}
+
+NAMESPACES = {
+    "rdf": RDF,
+    "schema": Namespace("http://schema.org/"),
+    "foaf": FOAF,
+}
+
+
+# dict: name of CWEType -> list of regid of adapters derived from EntityRDFAdapter
+ETYPES_ADAPTERS = {
+    "CWUser": ("rdf.foaf",),
+}
+
+
+def conjunctive_graph():
+    """factory to build a ``ConjunctiveGraph`` and bind all namespaces
+    """
+    graph = ConjunctiveGraph()
+    for prefix, rdfns in NAMESPACES.items():
+        graph.bind(prefix, rdfns)
+    return graph
+
+
+def iter_rdf_adapters(entity):
+    for adapter_id in ETYPES_ADAPTERS.get(entity.__regid__, ()):
+        adapter = entity.cw_adapt_to(adapter_id)
+        if adapter:
+            yield adapter
+
+
+def add_entity_to_graph(graph, entity):
+    for adapter in iter_rdf_adapters(entity):
+        for triple in adapter.triples():
+            graph.add(triple)
--- a/cubicweb/rqlrewrite.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/rqlrewrite.py	Thu Mar 05 10:15:38 2020 +0100
@@ -245,7 +245,7 @@
         vreg = session.vreg
         self.schema = vreg.schema
         self.annotate = vreg.rqlhelper.annotate
-        self._compute_solutions = vreg.solutions
+        self._compute_solutions = vreg.compute_var_types
 
     def compute_solutions(self):
         self.annotate(self.select)
--- a/cubicweb/server/hook.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/hook.py	Thu Mar 05 10:15:38 2020 +0100
@@ -248,6 +248,7 @@
 
 from logging import getLogger
 from itertools import chain
+from typing import Union, Tuple
 
 from logilab.common.decorators import classproperty, cached
 from logilab.common.logging_ext import set_log_methods
@@ -521,8 +522,8 @@
     """
     __select__ = enabled_category()
     # set this in derivated classes
-    events = None
-    category = None
+    events: Union[None, Tuple[str], Tuple[str, str]] = None
+    category: Union[None, str] = None
     order = 0
     # stop pylint from complaining about missing attributes in Hooks classes
     eidfrom = eidto = entity = rtype = repo = None
--- a/cubicweb/server/querier.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/querier.py	Thu Mar 05 10:15:38 2020 +0100
@@ -36,9 +36,9 @@
 
 from cubicweb.utils import QueryCache, RepeatList
 from cubicweb.misc.source_highlight import highlight_terminal
-from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
+from cubicweb.server.rqlannotation import RQLAnnotator, set_qdata
 from cubicweb.server.ssplanner import (READ_ONLY_RTYPES, add_types_restriction,
-                                       SSPlanner)
+                                       prepare_plan)
 from cubicweb.server.edition import EditedEntity
 from cubicweb.statsd_logger import statsd_timeit, statsd_c
 
@@ -154,20 +154,15 @@
 class ExecutionPlan(object):
     """the execution model of a rql query, composed of querier steps"""
 
-    def __init__(self, querier, rqlst, args, cnx):
+    def __init__(self, schema, rqlst, args, cnx):
+        self.schema = schema
         # original rql syntax tree
         self.rqlst = rqlst
         self.args = args or {}
         # cnx executing the query
         self.cnx = cnx
-        # quick reference to the system source
-        self.syssource = cnx.repo.system_source
         # execution steps
         self.steps = []
-        # various resource accesors
-        self.querier = querier
-        self.schema = querier.schema
-        self.rqlhelper = cnx.vreg.rqlhelper
         # tracing token for debugging
         self.rql_query_tracing_token = None
 
@@ -176,7 +171,7 @@
         self.steps.append(step)
 
     def sqlexec(self, sql, args=None):
-        return self.syssource.sqlexec(self.cnx, sql, args)
+        return self.cnx.repo.system_source.sqlexec(self.cnx, sql, args)
 
     def execute(self):
         """execute a plan and return resulting rows"""
@@ -215,8 +210,8 @@
         else:
             noinvariant = ()
         if cached is None:
-            self.rqlhelper.simplify(union)
-            self.querier.sqlgen_annotate(union)
+            self.cnx.vreg.rqlhelper.simplify(union)
+            RQLAnnotator(self.schema).annotate(union)
             set_qdata(self.schema.rschema, union, noinvariant)
         if union.has_text_query:
             self.cache_key = None
@@ -314,7 +309,7 @@
                 sol[newvarname] = nvartype
         select.clean_solutions(solutions)
         add_types_restriction(self.schema, select)
-        self.rqlhelper.annotate(rqlst)
+        self.cnx.vreg.rqlhelper.annotate(rqlst)
         self.preprocess(rqlst, security=False)
         return rqlst
 
@@ -323,8 +318,8 @@
     """an execution model specific to the INSERT rql query
     """
 
-    def __init__(self, querier, rqlst, args, cnx):
-        ExecutionPlan.__init__(self, querier, rqlst, args, cnx)
+    def __init__(self, schema, rqlst, args, cnx):
+        ExecutionPlan.__init__(self, schema, rqlst, args, cnx)
         # save originally selected variable, we may modify this
         # dictionary for substitution (query parameters)
         self.selected = rqlst.selection
@@ -478,10 +473,6 @@
     def set_schema(self, schema):
         self.schema = schema
         self.clear_caches()
-        # rql planner
-        self._planner = SSPlanner(schema, self._repo.vreg.rqlhelper)
-        # sql generation annotator
-        self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
 
     def clear_caches(self, eids=None, etypes=None):
         if eids is None:
@@ -496,8 +487,8 @@
     def plan_factory(self, rqlst, args, cnx):
         """create an execution plan for an INSERT RQL query"""
         if rqlst.TYPE == 'insert':
-            return InsertPlan(self, rqlst, args, cnx)
-        return ExecutionPlan(self, rqlst, args, cnx)
+            return InsertPlan(self.schema, rqlst, args, cnx)
+        return ExecutionPlan(self.schema, rqlst, args, cnx)
 
     @statsd_timeit
     def execute(self, cnx, rql, args=None, build_descr=True):
@@ -555,7 +546,7 @@
         plan = self.plan_factory(rqlst, args, cnx)
         plan.cache_key = cachekey
         plan.rql_query_tracing_token = str(uuid.uuid4())
-        self._planner.build_plan(plan)
+        prepare_plan(plan, self.schema, self._repo.vreg.rqlhelper)
 
         query_debug_informations = {
             "rql": rql,
@@ -567,7 +558,6 @@
         }
 
         start = time.time()
-
         # execute the plan
         try:
             results = plan.execute()
@@ -634,7 +624,7 @@
         # some cache usage stats
         self.cache_hit, self.cache_miss = 0, 0
         # rql parsing / analysing helper
-        self.solutions = repo.vreg.solutions
+        self.compute_var_types = repo.vreg.compute_var_types
         rqlhelper = repo.vreg.rqlhelper
         # set backend on the rql helper, will be used for function checking
         rqlhelper.backend = repo.config.system_source_config['db-driver']
@@ -677,7 +667,7 @@
             # which are eids. Notice that if you may not need `eidkeys`, we
             # have to compute solutions anyway (kept as annotation on the
             # tree)
-            eidkeys = self.solutions(cnx, rqlst, args)
+            eidkeys = self.compute_var_types(cnx, rqlst, args)
             if args and rql not in self._ck_cache:
                 self._ck_cache[rql] = eidkeys
                 if eidkeys:
--- a/cubicweb/server/rqlannotation.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/rqlannotation.py	Thu Mar 05 10:15:38 2020 +0100
@@ -117,7 +117,8 @@
                 var._q_invariant = False
 
 
-class SQLGenAnnotator(object):
+class RQLAnnotator(object):
+    """Annotate the RQL abstract syntax tree to inform the SQL generation"""
 
     def __init__(self, schema):
         self.schema = schema
--- a/cubicweb/server/serverctl.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/serverctl.py	Thu Mar 05 10:15:38 2020 +0100
@@ -26,6 +26,7 @@
 
 from logilab.common.configuration import Configuration, merge_options
 from logilab.common.shellutils import ASK, generate_password
+from logilab.common.deprecation import deprecated
 
 from logilab.database import get_db_helper, get_connection
 
@@ -96,6 +97,9 @@
     return cnx
 
 
+@deprecated('[3.28] instead of system_source_cnx(source, True, **args) use '
+            'source_cnx(source, get_db_helper(source[\'db-driver\']).system_database(), '
+            '**args)')
 def system_source_cnx(source, dbms_system_base=False,
                       special_privs='CREATE/DROP DATABASE', interactive=True):
     """shortcut to get a connextion to the instance system database
@@ -118,8 +122,9 @@
     import logilab.common as lgp
     lgp.USE_MX_DATETIME = False
     # connect on the dbms system base to create our base
-    cnx = system_source_cnx(source, True, special_privs=special_privs,
-                            interactive=interactive)
+    system_db = get_db_helper(source['db-driver']).system_database()
+    cnx = source_cnx(source, system_db, special_privs=special_privs,
+                     interactive=interactive)
     # disable autocommit (isolation_level(1)) because DROP and
     # CREATE DATABASE can't be executed in a transaction
     set_isolation_level = getattr(cnx, 'set_isolation_level', None)
@@ -201,7 +206,7 @@
 @contextmanager
 def db_transaction(source, privilege):
     """Open a transaction to the instance database"""
-    cnx = system_source_cnx(source, special_privs=privilege)
+    cnx = source_cnx(source, special_privs=privilege)
     cursor = cnx.cursor()
     try:
         yield cursor
@@ -379,8 +384,8 @@
             except BaseException:
                 dbcnx.rollback()
                 raise
-        cnx = system_source_cnx(source, special_privs='CREATE LANGUAGE/SCHEMA',
-                                interactive=not automatic)
+        cnx = source_cnx(source, special_privs='CREATE LANGUAGE/SCHEMA',
+                         interactive=not automatic)
         cursor = cnx.cursor()
         helper.init_fti_extensions(cursor)
         namespace = source.get('db-namespace')
@@ -572,7 +577,7 @@
         config = ServerConfiguration.config_for(appid)
         source = config.system_source_config
         set_owner = self.config.set_owner
-        cnx = system_source_cnx(source, special_privs='GRANT')
+        cnx = source_cnx(source, special_privs='GRANT')
         cursor = cnx.cursor()
         schema = config.load_schema()
         try:
--- a/cubicweb/server/sources/native.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/sources/native.py	Thu Mar 05 10:15:38 2020 +0100
@@ -48,11 +48,10 @@
 from cubicweb.server import schema2sql as y2sql
 from cubicweb.server.utils import crypt_password, verify_and_update
 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
-from cubicweb.server.rqlannotation import set_qdata
+from cubicweb.server.rqlannotation import RQLAnnotator, set_qdata
 from cubicweb.server.hook import CleanupDeletedEidsCacheOp
 from cubicweb.server.edition import EditedEntity
-from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
-from cubicweb.server.sources.rql2sql import SQLGenerator
+from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results, rql2sql
 from cubicweb.misc.source_highlight import highlight_terminal
 from cubicweb.statsd_logger import statsd_timeit
 
@@ -260,7 +259,7 @@
 class NativeSQLSource(SQLAdapterMixIn, AbstractSource):
     """adapter for source using the native cubicweb schema (see below)
     """
-    sqlgen_class = SQLGenerator
+
     options = (
         ('db-driver',
          {'type': 'string',
@@ -333,8 +332,8 @@
             self.authentifiers.insert(0, EmailPasswordAuthentifier(self))
         AbstractSource.__init__(self, repo, source_config, *args, **kwargs)
         # sql generator
-        self._rql_sqlgen = self.sqlgen_class(self.schema, self.dbhelper,
-                                             ATTR_MAP.copy())
+        self._rql_sqlgen = rql2sql.SQLGenerator(self.schema, self.dbhelper,
+                                                ATTR_MAP.copy())
         # full text index helper
         self.do_fti = not repo.config['delay-full-text-indexation']
         # sql queries cache
@@ -473,7 +472,7 @@
         rqlst = self.repo.vreg.rqlhelper.parse(rql)
         rqlst.restricted_vars = ()
         rqlst.children[0].solutions = sols
-        self.repo.querier.sqlgen_annotate(rqlst)
+        RQLAnnotator(self.repo.querier.schema).annotate(rqlst)
         set_qdata(self.schema.rschema, rqlst, ())
         return rqlst
 
--- a/cubicweb/server/ssplanner.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/ssplanner.py	Thu Mar 05 10:15:38 2020 +0100
@@ -307,6 +307,11 @@
         return self.build_select_plan(plan, union)
 
 
+def prepare_plan(plan, schema, rqlhelper):
+    """Add steps to a plan to prepare it for execution"""
+    return SSPlanner(schema, rqlhelper).build_plan(plan)
+
+
 # execution steps and helper functions ########################################
 
 class Step(object):
--- a/cubicweb/server/test/unittest_rqlannotation.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/test/unittest_rqlannotation.py	Thu Mar 05 10:15:38 2020 +0100
@@ -22,14 +22,14 @@
 from cubicweb.devtools.repotest import BaseQuerierTC
 
 
-class SQLGenAnnotatorTC(BaseQuerierTC):
+class RQLAnnotatorTC(BaseQuerierTC):
 
     def setUp(self):
         handler = devtools.get_test_db_handler(devtools.TestServerConfiguration('data', __file__))
         handler.build_db_cache()
         repo, _cnx = handler.get_repo_and_cnx()
         self.__class__.repo = repo
-        super(SQLGenAnnotatorTC, self).setUp()
+        super(RQLAnnotatorTC, self).setUp()
 
     def test_0_1(self):
         with self.admin_access.cnx() as cnx:
--- a/cubicweb/server/test/unittest_server_security.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/server/test/unittest_server_security.py	Thu Mar 05 10:15:38 2020 +0100
@@ -44,7 +44,7 @@
         nom = self.repo.schema['Personne'].rdef('nom')
         with self.temporary_permissions((nom, {'read': ('users', 'managers')})):
             with self.admin_access.repo_cnx() as cnx:
-                self.repo.vreg.solutions(cnx, rqlst, None)
+                self.repo.vreg.compute_var_types(cnx, rqlst, None)
                 check_relations_read_access(cnx, rqlst, {})
             with self.new_access(u'anon').repo_cnx() as cnx:
                 self.assertRaises(Unauthorized,
@@ -57,7 +57,7 @@
         rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
         with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
             with self.admin_access.repo_cnx() as cnx:
-                self.repo.vreg.solutions(cnx, rqlst, None)
+                self.repo.vreg.compute_var_types(cnx, rqlst, None)
                 solution = rqlst.solutions[0]
                 localchecks = get_local_checks(cnx, rqlst, solution)
                 self.assertEqual({}, localchecks)
@@ -520,7 +520,7 @@
                                                            ERQLExpression('X owned_by U'))}):
             with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
                 rqlst = self.repo.vreg.rqlhelper.parse('Any X WHERE X is_instance_of Societe')
-                self.repo.vreg.solutions(cnx, rqlst, {})
+                self.repo.vreg.compute_var_types(cnx, rqlst, {})
                 self.repo.vreg.rqlhelper.annotate(rqlst)
                 plan = cnx.repo.querier.plan_factory(rqlst, {}, cnx)
                 plan.preprocess(rqlst)
--- a/cubicweb/test/unittest_predicates.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/test/unittest_predicates.py	Thu Mar 05 10:15:38 2020 +0100
@@ -29,7 +29,7 @@
                                  multi_lines_rset, score_entity, is_in_state,
                                  rql_condition, relation_possible, match_form_params,
                                  paginated_rset)
-from cubicweb.view import EntityAdapter
+from cubicweb.entity import EntityAdapter
 from cubicweb.web import action
 
 
--- a/cubicweb/test/unittest_rqlrewrite.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/test/unittest_rqlrewrite.py	Thu Mar 05 10:15:38 2020 +0100
@@ -58,7 +58,7 @@
         schema = schema
 
         @staticmethod
-        def solutions(sqlcursor, rqlst, kwargs):
+        def compute_var_types(sqlcursor, rqlst, kwargs):
             rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs)
 
         class rqlhelper:
@@ -602,7 +602,7 @@
             args = {}
         union = parse(rql)  # self.vreg.parse(rql, annotate=True)
         with self.admin_access.repo_cnx() as cnx:
-            self.vreg.solutions(cnx, union, args)
+            self.vreg.compute_var_types(cnx, union, args)
             self.vreg.rqlhelper.annotate(union)
             plan = self.repo.querier.plan_factory(union, args, cnx)
             plan.preprocess(union)
--- a/cubicweb/test/unittest_vregistry.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/test/unittest_vregistry.py	Thu Mar 05 10:15:38 2020 +0100
@@ -23,7 +23,7 @@
 from cubicweb import CW_SOFTWARE_ROOT as BASE, devtools
 from cubicweb.cwvreg import CWRegistryStore, UnknownProperty
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.view import EntityAdapter
+from cubicweb.entity import EntityAdapter
 
 
 class YesSchema:
--- a/cubicweb/view.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/view.py	Thu Mar 05 10:15:38 2020 +0100
@@ -554,18 +554,7 @@
     def domid(self):
         return '%sComponent' % domid(self.__regid__)
 
-
-class Adapter(AppObject):
-    """base class for adapters"""
-    __registry__ = 'adapters'
-
-
-class EntityAdapter(Adapter):
-    """base class for entity adapters (eg adapt an entity to an interface)"""
-    def __init__(self, _cw, **kwargs):
-        try:
-            self.entity = kwargs.pop('entity')
-        except KeyError:
-            self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
-                                                    kwargs.get('col') or 0)
-        Adapter.__init__(self, _cw, **kwargs)
+# EntityAdapter moved to cubicweb.entity ######################################
+from logilab.common.deprecation import class_moved
+from cubicweb import entity
+EntityAdapter = class_moved(entity.EntityAdapter) # cubicweb 3.28
--- a/cubicweb/web/action.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/action.py	Thu Mar 05 10:15:38 2020 +0100
@@ -32,6 +32,7 @@
 Many examples are available in :mod:`cubicweb.web.views.actions`.
 """
 
+from typing import Optional
 
 from cubicweb import _
 
@@ -48,7 +49,7 @@
     __registry__ = 'actions'
     __select__ = match_search_state('normal')
     order = 99
-    category = 'moreactions'
+    category: Optional[str] = 'moreactions'
     # actions in category 'moreactions' can specify a sub-menu in which they should be filed
     submenu = None
 
--- a/cubicweb/web/test/unittest_idownloadable.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/test/unittest_idownloadable.py	Thu Mar 05 10:15:38 2020 +0100
@@ -23,12 +23,12 @@
 from pytz import utc
 
 from cubicweb.devtools.testlib import CubicWebTC, real_error_handling
-from cubicweb import view
+from cubicweb.entity import EntityAdapter
 from cubicweb.predicates import is_instance
 from cubicweb.web import http_headers
 
 
-class IDownloadableUser(view.EntityAdapter):
+class IDownloadableUser(EntityAdapter):
     __regid__ = 'IDownloadable'
     __select__ = is_instance('CWUser')
 
--- a/cubicweb/web/views/calendar.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/views/calendar.py	Thu Mar 05 10:15:38 2020 +0100
@@ -28,7 +28,8 @@
 
 from cubicweb.utils import json_dumps, make_uid
 from cubicweb.predicates import adaptable
-from cubicweb.view import EntityView, EntityAdapter
+from cubicweb.view import EntityView
+from cubicweb.entity import EntityAdapter
 
 # useful constants & functions ################################################
 
--- a/cubicweb/web/views/editcontroller.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/views/editcontroller.py	Thu Mar 05 10:15:38 2020 +0100
@@ -27,7 +27,7 @@
 from rql.utils import rqlvar_maker
 
 from cubicweb import _, ValidationError, UnknownEid
-from cubicweb.view import EntityAdapter
+from cubicweb.entity import EntityAdapter
 from cubicweb.predicates import is_instance
 from cubicweb.web import RequestError, NothingToEdit, ProcessFormError
 from cubicweb.web.views import basecontrollers, autoform
--- a/cubicweb/web/views/ibreadcrumbs.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/views/ibreadcrumbs.py	Thu Mar 05 10:15:38 2020 +0100
@@ -25,11 +25,11 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import tags, uilib
-from cubicweb.entity import Entity
+from cubicweb.entity import Entity, EntityAdapter
 from cubicweb.predicates import (is_instance, one_line_rset, adaptable,
                                 one_etype_rset, multi_lines_rset, any_rset,
                                 match_form_params)
-from cubicweb.view import EntityView, EntityAdapter
+from cubicweb.view import EntityView
 from cubicweb.web.views import basecomponents
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
 
--- a/cubicweb/web/views/magicsearch.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/views/magicsearch.py	Thu Mar 05 10:15:38 2020 +0100
@@ -454,7 +454,7 @@
                 user_rql = variables
             select = parse(user_rql, print_errors=False).children[0]
             req.vreg.rqlhelper.annotate(select)
-            req.vreg.solutions(req, select, {})
+            req.vreg.compute_var_types(req, select, {})
             if restrictions:
                 return ['%s, %s' % (user_rql, suggestion)
                         for suggestion in self.rql_build_suggestions(select, incomplete_part)]
--- a/cubicweb/web/views/navigation.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/views/navigation.py	Thu Mar 05 10:15:38 2020 +0100
@@ -56,7 +56,7 @@
 
 from cubicweb.predicates import paginated_rset, sorted_rset, adaptable
 from cubicweb.uilib import cut
-from cubicweb.view import EntityAdapter
+from cubicweb.entity import EntityAdapter
 from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent
 
 
--- a/cubicweb/web/views/xmlrss.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/cubicweb/web/views/xmlrss.py	Thu Mar 05 10:15:38 2020 +0100
@@ -26,7 +26,8 @@
 
 from cubicweb.predicates import (is_instance, non_final_entity, one_line_rset,
                                  appobject_selectable, adaptable)
-from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
+from cubicweb.view import EntityView, AnyRsetView, Component
+from cubicweb.entity import EntityAdapter
 from cubicweb.uilib import simple_sgml_tag
 from cubicweb.web import httpcache, component
 
--- a/doc/book/devrepo/datamodel/definition.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/book/devrepo/datamodel/definition.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -466,7 +466,7 @@
     __permissions__ = {'read': ('managers', 'users'),
                        'add': ('managers', RRQLExpression('U has_update_permission S')),
                        'delete': ('managers', RRQLExpression('U has_update_permission S'))
-		       }
+                       }
 
 In the above example, user will be allowed to add/delete `my_relation` if he has
 the `update` permission on the subject of the relation.
--- a/doc/book/devrepo/repo/hooks.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/book/devrepo/repo/hooks.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -35,11 +35,11 @@
         events = ('before_add_entity', 'before_update_entity')
 
         def __call__(self):
-	    if 'age' in self.entity.cw_edited:
+            if 'age' in self.entity.cw_edited:
                 if 0 <= self.entity.age <= 120:
                    return
-		msg = self._cw._('age must be between 0 and 120')
-		raise ValidationError(self.entity.eid, {'age': msg})
+                msg = self._cw._('age must be between 0 and 120')
+                raise ValidationError(self.entity.eid, {'age': msg})
 
 In our example the base `__select__` is augmented with an `is_instance` selector
 matching the desired entity type.
--- a/doc/book/devrepo/vreg.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/book/devrepo/vreg.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -300,18 +300,18 @@
 .. sourcecode:: python
 
     class UserLink(component.Component):
-	'''if the user is the anonymous user, build a link to login else a link
-	to the connected user object with a logout link
-	'''
-	__regid__ = 'loggeduserlink'
+        '''if the user is the anonymous user, build a link to login else a link
+        to the connected user object with a logout link
+        '''
+        __regid__ = 'loggeduserlink'
 
-	def call(self):
-	    if self._cw.session.anonymous_session:
-		# display login link
-		...
-	    else:
-		# display a link to the connected user object with a loggout link
-		...
+        def call(self):
+            if self._cw.session.anonymous_session:
+                # display login link
+                ...
+            else:
+                # display a link to the connected user object with a loggout link
+                ...
 
 The proper way to implement this with |cubicweb| is two have two different
 classes sharing the same identifier but with different selectors so you'll get
@@ -320,21 +320,21 @@
 .. sourcecode:: python
 
     class UserLink(component.Component):
-	'''display a link to the connected user object with a loggout link'''
-	__regid__ = 'loggeduserlink'
-	__select__ = component.Component.__select__ & authenticated_user()
+        '''display a link to the connected user object with a loggout link'''
+        __regid__ = 'loggeduserlink'
+        __select__ = component.Component.__select__ & authenticated_user()
 
-	def call(self):
+        def call(self):
             # display useractions and siteactions
-	    ...
+            ...
 
     class AnonUserLink(component.Component):
-	'''build a link to login'''
-	__regid__ = 'loggeduserlink'
-	__select__ = component.Component.__select__ & anonymous_user()
+        '''build a link to login'''
+        __regid__ = 'loggeduserlink'
+        __select__ = component.Component.__select__ & anonymous_user()
 
-	def call(self):
-	    # display login link
+        def call(self):
+            # display login link
             ...
 
 The big advantage, aside readability once you're familiar with the
--- a/doc/book/devweb/edition/examples.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/book/devweb/edition/examples.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -56,42 +56,42 @@
 .. sourcecode:: python
 
     def sender_value(form, field):
-	return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
+        return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
 
     def recipient_choices(form, field):
-	return [(e.get_email(), e.eid)
+        return [(e.get_email(), e.eid)
                  for e in form.cw_rset.entities()
-		 if e.get_email()]
+                 if e.get_email()]
 
     def recipient_value(form, field):
-	return [e.eid for e in form.cw_rset.entities()
+        return [e.eid for e in form.cw_rset.entities()
                 if e.get_email()]
 
     class MassMailingForm(forms.FieldsForm):
-	__regid__ = 'massmailing'
+        __regid__ = 'massmailing'
 
-	needs_js = ('cubicweb.widgets.js',)
-	domid = 'sendmail'
-	action = 'sendmail'
+        needs_js = ('cubicweb.widgets.js',)
+        domid = 'sendmail'
+        action = 'sendmail'
 
-	sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
-				label=_('From:'),
-				value=sender_value)
+        sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
+                                label=_('From:'),
+                                value=sender_value)
 
-	recipient = ff.StringField(widget=CheckBox(),
-	                           label=_('Recipients:'),
-				   choices=recipient_choices,
-				   value=recipients_value)
+        recipient = ff.StringField(widget=CheckBox(),
+                                   label=_('Recipients:'),
+                                   choices=recipient_choices,
+                                   value=recipients_value)
 
-	subject = ff.StringField(label=_('Subject:'), max_length=256)
+        subject = ff.StringField(label=_('Subject:'), max_length=256)
 
-	mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
-						    inputid='mailbody'))
+        mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
+                                                    inputid='mailbody'))
 
-	form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
-				  _('send email'), 'SEND_EMAIL_ICON'),
-			ImgButton('cancelbutton', "javascript: history.back()",
-				  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
+        form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
+                                  _('send email'), 'SEND_EMAIL_ICON'),
+                        ImgButton('cancelbutton', "javascript: history.back()",
+                                  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
 
 Let's detail what's going on up there. Our form will hold four fields:
 
@@ -125,13 +125,13 @@
 .. sourcecode:: python
 
     class MassMailingFormView(form.FormViewMixIn, EntityView):
-	__regid__ = 'massmailing'
-	__select__ = is_instance(IEmailable) & authenticated_user()
+        __regid__ = 'massmailing'
+        __select__ = is_instance(IEmailable) & authenticated_user()
 
-	def call(self):
-	    form = self._cw.vreg['forms'].select('massmailing', self._cw,
-	                                         rset=self.cw_rset)
-	    form.render(w=self.w)
+        def call(self):
+            form = self._cw.vreg['forms'].select('massmailing', self._cw,
+                                                 rset=self.cw_rset)
+            form.render(w=self.w)
 
 As you see, we simply define a view with proper selector so it only apply to a
 result set containing :class:`IEmailable` entities, and so that only users in the
--- a/doc/book/devweb/js.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/book/devweb/js.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -142,7 +142,7 @@
     function removeBookmark(beid) {
         d = asyncRemoteExec('delete_bookmark', beid);
         d.addCallback(function(boxcontent) {
-	    reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
+            reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
             document.location.hash = '#header';
             updateMessage(_("bookmark has been removed"));
          });
--- a/doc/book/devweb/views/reledit.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/book/devweb/views/reledit.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -136,14 +136,14 @@
     from cubicweb.web.views import reledit
 
     class DeactivatedAutoClickAndEditFormView(reledit.AutoClickAndEditFormView):
-	def _should_edit_attribute(self, rschema):
-	    return False
+        def _should_edit_attribute(self, rschema):
+            return False
 
-	def _should_edit_attribute(self, rschema, role):
-	    return False
+        def _should_edit_attribute(self, rschema, role):
+            return False
 
     def registration_callback(vreg):
-	vreg.register_and_replace(DeactivatedAutoClickAndEditFormView,
-				  reledit.AutoClickAndEditFormView)
+        vreg.register_and_replace(DeactivatedAutoClickAndEditFormView,
+                                  reledit.AutoClickAndEditFormView)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.28.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -0,0 +1,8 @@
+3.28
+====
+
+Changes
+-------
+
+- the class cubicweb.view.EntityAdapter was moved to cubicweb.entity.EntityAdapter
+  a deprecation warning is in place, but please update your source code accordingly.
--- a/doc/changes/changelog.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/changes/changelog.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -2,6 +2,7 @@
  Changelog history
 ===================
 
+.. include:: 3.28.rst
 .. include:: 3.27.rst
 .. include:: 3.26.rst
 .. include:: 3.25.rst
--- a/doc/tutorials/advanced/part02_security.rst	Thu Mar 05 09:54:49 2020 +0100
+++ b/doc/tutorials/advanced/part02_security.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -117,25 +117,25 @@
     from cubicweb.schema import ERQLExpression
 
     VISIBILITY_PERMISSIONS = {
-	'read':   ('managers',
-		   ERQLExpression('X visibility "public"'),
-		   ERQLExpression('X may_be_read_by U')),
-	'add':    ('managers',),
-	'update': ('managers', 'owners',),
-	'delete': ('managers', 'owners'),
-	}
+        'read':   ('managers',
+                   ERQLExpression('X visibility "public"'),
+                   ERQLExpression('X may_be_read_by U')),
+        'add':    ('managers',),
+        'update': ('managers', 'owners',),
+        'delete': ('managers', 'owners'),
+        }
     AUTH_ONLY_PERMISSIONS = {
-	    'read':   ('managers', 'users'),
-	    'add':    ('managers',),
-	    'update': ('managers', 'owners',),
-	    'delete': ('managers', 'owners'),
-	    }
+            'read':   ('managers', 'users'),
+            'add':    ('managers',),
+            'update': ('managers', 'owners',),
+            'delete': ('managers', 'owners'),
+            }
     CLASSIFIERS_PERMISSIONS = {
-	    'read':   ('managers', 'users', 'guests'),
-	    'add':    ('managers',),
-	    'update': ('managers', 'owners',),
-	    'delete': ('managers', 'owners'),
-	    }
+            'read':   ('managers', 'users', 'guests'),
+            'add':    ('managers',),
+            'update': ('managers', 'owners',),
+            'delete': ('managers', 'owners'),
+            }
 
     from cubicweb_folder.schema import Folder
     from cubicweb_file.schema import File
--- a/setup.py	Thu Mar 05 09:54:49 2020 +0100
+++ b/setup.py	Thu Mar 05 10:15:38 2020 +0100
@@ -73,6 +73,8 @@
         'pytz',
         'Markdown',
         'filelock',
+        'rdflib',
+        'rdflib-jsonld',
     ],
     entry_points={
         'console_scripts': [
@@ -102,9 +104,6 @@
             'pyramid_multiauth',
             'repoze.lru',
         ],
-        'rdf': [
-            'rdflib',
-        ],
         'sparql': [
             'fyzz >= 0.1.0',
         ],