--- a/cubicweb/entities/adapters.py Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/entities/adapters.py Wed Mar 11 11:18:40 2020 +0100
@@ -23,56 +23,117 @@
from hashlib import sha1
from itertools import chain
-from rdflib import URIRef, Literal
-
+from rdflib import URIRef, Literal, namespace as rdflib_namespace
from logilab.mtconverter import TransformError
from logilab.common.decorators import cached, cachedproperty
-from cubicweb.entity import EntityAdapter
+from cubicweb import rdf
from cubicweb import (Unauthorized, ValidationError, ViolatedConstraint,
UniqueTogetherError)
-from cubicweb.schema import constraint_name_for
+from cubicweb.entity import EntityAdapter
+from cubicweb.schema import constraint_name_for, VIRTUAL_RTYPES
from cubicweb.predicates import is_instance, relation_possible, match_exception
-from cubicweb.rdf import NAMESPACES
-
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
+ __regid__ = "rdf"
+
+ SKIP_RTYPES = VIRTUAL_RTYPES | set(['cwuri', 'is', 'is_instance_of'])
def __init__(self, _cw, **kwargs):
super().__init__(_cw, **kwargs)
self.entity.complete()
+ self.used_namespaces = {}
+
+ def _use_namespace(self, prefix, base_url=None):
+ if prefix not in self.used_namespaces:
+ if base_url is not None:
+ if prefix in rdf.NAMESPACES:
+ raise KeyError('prefix redefinition not allowed: '
+ f'"{prefix}" already exists as "{rdf.NAMESPACES[prefix]}"')
+ ns = rdflib_namespace.Namespace(base_url)
+ else:
+ ns = rdf.NAMESPACES[prefix]
+ self.used_namespaces[prefix] = ns
+ elif base_url is not None:
+ if self.used_namespaces[prefix] != rdflib_namespace.Namespace(base_url):
+ raise ValueError('prefix redefinition not allowed: '
+ f'"{prefix}" already exists as "{self.used_namespaces[prefix]}"')
+ return self.used_namespaces[prefix]
@cachedproperty
def uri(self):
- return self.entity.cwuri
+ return URIRef(self.entity.cwuri)
def triples(self):
"""return sequence of 3-tuple of rdflib identifiers"""
- raise NotImplementedError()
+ yield from self.cw_triples()
+ yield from self.dc_triples()
+
+ def cw_triples(self):
+ RDF = self._use_namespace('rdf')
+ CW = self._use_namespace('cubicweb')
+
+ yield (self.uri, RDF.type, CW[self.entity.e_schema.type])
+
+ for rschema, targettypes, role in self.entity.e_schema.relation_definitions('relation'):
+ rtype = rschema.type
+ if rtype in self.SKIP_RTYPES or rtype.endswith('_permission'):
+ continue
+ for targetype in targettypes:
+ # if rschema is an attribute
+ if targetype.final:
+ try:
+ value = self.entity.cw_attr_cache[rtype]
+ except KeyError:
+ continue
+ if value is not None:
+ yield (self.uri, CW[rtype], Literal(value))
+ # else if rschema is a relation
+ else:
+ for related in self.entity.related(rtype, role, entities=True, safe=True):
+ if role == 'subject':
+ yield (self.uri, CW[rtype], URIRef(related.cwuri))
+ else:
+ yield (URIRef(related.cwuri), CW[rtype], self.uri)
+
+ def dc_triples(self):
+ dc_entity = self.entity.cw_adapt_to('IDublinCore')
+ DC = self._use_namespace('dc')
+ yield (self.uri, DC.title, Literal(dc_entity.long_title())) # or title() ?
+ desc = dc_entity.description()
+ if desc:
+ yield (self.uri, DC.description, Literal(desc))
+ creator = dc_entity.creator() # use URI instead of Literal ?
+ if creator:
+ yield (self.uri, DC.creator, Literal(creator))
+ yield (self.uri, DC.date, Literal(dc_entity.date()))
+ yield (self.uri, DC.type, Literal(dc_entity.type()))
+ yield (self.uri, DC.language, Literal(dc_entity.language()))
-class CWUserFoafAdapter(EntityRDFAdapter):
- __regid__ = "rdf.foaf"
+class CWUserRDFAdapter(EntityRDFAdapter):
__select__ = is_instance("CWUser")
def triples(self):
- RDF = NAMESPACES["rdf"]
- FOAF = NAMESPACES["foaf"]
- uri = URIRef(self.uri)
- yield (uri, RDF.type, FOAF.Person)
+ yield from super().triples()
+ yield from self.foaf_triples()
+
+ def foaf_triples(self):
+ RDF = self._use_namespace('rdf')
+ FOAF = self._use_namespace('foaf')
+ yield (self.uri, RDF.type, FOAF.Person)
if self.entity.surname:
- yield (uri, FOAF.familyName, Literal(self.entity.surname))
+ yield (self.uri, FOAF.familyName, Literal(self.entity.surname))
if self.entity.firstname:
- yield (uri, FOAF.givenName, Literal(self.entity.firstname))
+ yield (self.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))
+ yield (self.uri, FOAF.mbox_sha1sum, Literal(email_digest))
class IDublinCoreAdapter(EntityAdapter):
@@ -104,7 +165,7 @@
return self.entity.printable_value('description', format=format)
return u''
- def authors(self):
+ def authors(self): # XXX is this part of DC ?
"""Return a suitable description for the author(s) of the entity"""
try:
return u', '.join(u.name() for u in self.entity.owned_by)
@@ -132,7 +193,7 @@
# check if entities has internationalizable attributes
# XXX one is enough or check if all String attributes are internationalizable?
for rschema, attrschema in eschema.attribute_definitions():
- if rschema.rdef(eschema, attrschema).internationalizable:
+ if getattr(rschema.rdef(eschema, attrschema), 'internationalizable', False):
return self._cw._(self._cw.user.property_value('ui.language'))
return self._cw._(self._cw.vreg.property_value('ui.language'))
--- a/cubicweb/pyramid/__init__.py Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/__init__.py Wed Mar 11 11:18:40 2020 +0100
@@ -231,6 +231,8 @@
cwconfig = config.registry.get('cubicweb.config')
repo = config.registry.get('cubicweb.repository')
+ config.include('cubicweb.pyramid.rest_api')
+
if repo is not None:
if cwconfig is None:
config.registry['cubicweb.config'] = cwconfig = repo.config
--- a/cubicweb/pyramid/predicates.py Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/predicates.py Wed Mar 11 11:18:40 2020 +0100
@@ -24,22 +24,6 @@
from cubicweb._exceptions import UnknownEid
-class MatchIsETypePredicate(object):
- """A predicate that match if a given etype exist in schema.
- """
- def __init__(self, matchname, config):
- self.matchname = matchname
-
- def text(self):
- return 'match_is_etype = %s' % self.matchname
-
- phash = text
-
- def __call__(self, info, request):
- return info['match'][self.matchname].lower() in \
- request.registry['cubicweb.registry'].case_insensitive_etypes
-
-
class MatchIsEIDPredicate(object):
"""A predicate that match if a given eid exist in the database.
"""
@@ -64,6 +48,33 @@
return True
+class MatchIsETypeAndEIDPredicate(object):
+ """A predicate that match if a given eid exist in the database and if the
+ etype of the entity same as the one given in the URL
+ """
+ def __init__(self, matchnames, config):
+ self.match_etype, self.match_eid = matchnames
+
+ def text(self):
+ return f"match_is_etype_and_eid = {self.match_etype}/{self.match_eid}"
+
+ phash = text
+
+ def __call__(self, info, request):
+ try:
+ eid = int(info['match'][self.match_eid])
+ except ValueError:
+ return False
+
+ try:
+ entity = request.cw_cnx.entity_from_eid(eid)
+ except UnknownEid:
+ return False
+
+ etype = info['match'][self.match_etype]
+ return entity.__regid__.lower() == etype.lower()
+
+
def includeme(config):
- config.add_route_predicate('match_is_etype', MatchIsETypePredicate)
config.add_route_predicate('match_is_eid', MatchIsEIDPredicate)
+ config.add_route_predicate('match_is_etype_and_eid', MatchIsETypeAndEIDPredicate)
--- a/cubicweb/pyramid/resources.py Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/resources.py Wed Mar 11 11:18:40 2020 +0100
@@ -18,84 +18,33 @@
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Pyramid resource definitions for CubicWeb."""
+from pyramid.httpexceptions import HTTPNotFound
-from rql import TypeResolverException
-
-from pyramid.decorator import reify
-from pyramid.httpexceptions import HTTPNotFound
+from cubicweb import rdf
-class EntityResource(object):
-
- """A resource class for an entity. It provide method to retrieve an entity
- by eid.
- """
-
- @classmethod
- def from_eid(cls):
- def factory(request):
- return cls(request, None, None, request.matchdict['eid'])
- return factory
-
- def __init__(self, request, cls, attrname, value):
- self.request = request
- self.cls = cls
- self.attrname = attrname
- self.value = value
-
- @reify
- def rset(self):
- req = self.request.cw_request
- if self.cls is None:
- return req.execute('Any X WHERE X eid %(x)s',
- {'x': int(self.value)})
- st = self.cls.fetch_rqlst(self.request.cw_cnx.user, ordermethod=None)
- st.add_constant_restriction(st.get_variable('X'), self.attrname,
- 'x', 'Substitute')
- if self.attrname == 'eid':
- try:
- rset = req.execute(st.as_string(), {'x': int(self.value)})
- except (ValueError, TypeResolverException):
- # conflicting eid/type
- raise HTTPNotFound()
- else:
- rset = req.execute(st.as_string(), {'x': self.value})
- return rset
+def negociate_mime_type(request, possible_mimetypes):
+ accepted_headers_by_weight = sorted(
+ request.accept.parsed or [], key=lambda h: h[1], reverse=True
+ )
+ mime_type_negociated = None
+ for parsed_header in accepted_headers_by_weight:
+ accepted_mime_type = parsed_header[0]
+ if accepted_mime_type in possible_mimetypes:
+ mime_type_negociated = accepted_mime_type
+ break
+ return mime_type_negociated
-class ETypeResource(object):
-
- """A resource for etype.
- """
- @classmethod
- def from_match(cls, matchname):
- def factory(request):
- return cls(request, request.matchdict[matchname])
- return factory
-
- def __init__(self, request, etype):
- vreg = request.registry['cubicweb.registry']
-
- self.request = request
- self.etype = vreg.case_insensitive_etypes[etype.lower()]
- self.cls = vreg['etypes'].etype_class(self.etype)
+def rdf_context_from_eid(request):
+ mime_type = negociate_mime_type(request, rdf.RDF_MIMETYPE_TO_FORMAT)
+ if mime_type is None:
+ raise HTTPNotFound()
+ entity = request.cw_request.entity_from_eid(request.matchdict['eid'])
+ return RDFResource(entity, mime_type)
- def __getitem__(self, value):
- # Try eid first, then rest attribute as for URL path evaluation
- # mecanism in cubicweb.web.views.urlpublishing.
- for attrname in ('eid', self.cls.cw_rest_attr_info()[0]):
- resource = EntityResource(self.request, self.cls, attrname, value)
- try:
- rset = resource.rset
- except HTTPNotFound:
- continue
- if rset.rowcount:
- return resource
- raise KeyError(value)
- @reify
- def rset(self):
- rql = self.cls.fetch_rql(self.request.cw_cnx.user)
- rset = self.request.cw_request.execute(rql)
- return rset
+class RDFResource:
+ def __init__(self, entity, mime_type):
+ self.entity = entity
+ self.mime_type = mime_type
--- a/cubicweb/pyramid/rest_api.py Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/rest_api.py Wed Mar 11 11:18:40 2020 +0100
@@ -20,26 +20,44 @@
"""Experimental REST API for CubicWeb using Pyramid."""
-from __future__ import absolute_import
-
+import rdflib
from pyramid.view import view_config
-from cubicweb.pyramid.resources import EntityResource, ETypeResource
+from pyramid.response import Response
+
+from cubicweb import rdf
+from cubicweb.pyramid.resources import rdf_context_from_eid, RDFResource
@view_config(
- route_name='cwentities',
- context=EntityResource,
- request_method='DELETE')
-def delete_entity(context, request):
- context.rset.one().cw_delete()
- request.response.status_int = 204
- return request.response
+ route_name='one_entity',
+ context=RDFResource,
+)
+@view_config(
+ route_name='one_entity_eid',
+ context=RDFResource,
+)
+def view_entity_as_rdf(context, request):
+ graph = rdflib.ConjunctiveGraph()
+ rdf.add_entity_to_graph(graph, context.entity)
+ rdf_format = rdf.RDF_MIMETYPE_TO_FORMAT[context.mime_type]
+ response = Response(graph.serialize(format=rdf_format))
+ response.content_type = context.mime_type
+ return response
def includeme(config):
config.include('.predicates')
config.add_route(
- 'cwentities', '/{etype}/*traverse',
- factory=ETypeResource.from_match('etype'), match_is_etype='etype')
+ 'one_entity',
+ '/{etype}/{eid}',
+ factory=rdf_context_from_eid,
+ match_is_etype_and_eid=('etype', 'eid'),
+ )
+ config.add_route(
+ 'one_entity_eid',
+ '/{eid}',
+ factory=rdf_context_from_eid,
+ match_is_eid='eid'
+ )
config.scan(__name__)
--- a/cubicweb/rdf.py Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/rdf.py Wed Mar 11 11:18:40 2020 +0100
@@ -15,8 +15,7 @@
# 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
+from rdflib import plugin, namespace
import rdflib_jsonld # noqa
plugin.register("jsonld", plugin.Serializer, "rdflib_jsonld.serializer", "JsonLDSerializer")
@@ -32,35 +31,25 @@
}
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",),
+ "rdf": namespace.RDF,
+ "rdfs": namespace.RDFS,
+ "owl": namespace.OWL,
+ "xsd": namespace.XSD,
+ "skos": namespace.SKOS,
+ "void": namespace.VOID,
+ "dc": namespace.DC,
+ "dcterms": namespace.DCTERMS,
+ "foaf": namespace.FOAF,
+ "doap": namespace.DOAP,
+ "schema": namespace.Namespace("http://schema.org/"),
+ "cubicweb": namespace.Namespace("http://ns.cubicweb.org/cubicweb/0.0/")
}
-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):
+ adapter = entity.cw_adapt_to("rdf")
+ if adapter:
for triple in adapter.triples():
graph.add(triple)
+ for prefix, rdfns in adapter.used_namespaces.items():
+ graph.bind(prefix, rdfns)
--- a/cubicweb/web/views/basecontrollers.py Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/web/views/basecontrollers.py Wed Mar 11 11:18:40 2020 +0100
@@ -23,7 +23,7 @@
from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
AuthenticationError, UndoTransactionException,
- Forbidden)
+ Forbidden, rdf)
from cubicweb.utils import json_dumps
from cubicweb.predicates import (authenticated_user, anonymous_user,
match_form_params)
@@ -101,6 +101,10 @@
else:
rset = None
vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
+ if rset and len(rset) == 1:
+ for mimetype in rdf.RDF_MIMETYPE_TO_FORMAT:
+ req.headers_out.addRawHeader(
+ 'Link', "<%s>;rel=alternate;type=%s" % (rset.one().cwuri, mimetype))
try:
view = self._cw.vreg['views'].select(vid, req, rset=rset)
except ObjectNotFound: