# HG changeset patch # User Fabien Amarger # Date 1583921920 -3600 # Node ID ebf4806e4ab7ae4e121ad8ba094ba71819ee22be # Parent 2cc3f481ecd02a092162ef2c34d1dd205fb6bb82# Parent 3966f09d5f5c8702efa08fb2d3a8b29fad544189 merge public heads diff -r 2cc3f481ecd0 -r ebf4806e4ab7 cubicweb/entities/adapters.py --- 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')) diff -r 2cc3f481ecd0 -r ebf4806e4ab7 cubicweb/pyramid/__init__.py --- 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 diff -r 2cc3f481ecd0 -r ebf4806e4ab7 cubicweb/pyramid/predicates.py --- 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) diff -r 2cc3f481ecd0 -r ebf4806e4ab7 cubicweb/pyramid/resources.py --- 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 . -"""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 diff -r 2cc3f481ecd0 -r ebf4806e4ab7 cubicweb/pyramid/rest_api.py --- 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__) diff -r 2cc3f481ecd0 -r ebf4806e4ab7 cubicweb/rdf.py --- 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 . -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) diff -r 2cc3f481ecd0 -r ebf4806e4ab7 cubicweb/web/views/basecontrollers.py --- 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: