├── LICENSE.md ├── README.rst ├── examples ├── cleanup.py ├── test0.py ├── test1.py ├── test2.py ├── test3.py ├── test4.py ├── test5.py └── test6.py ├── lib └── hydra │ ├── __init__.py │ └── tpf.py ├── requirements.txt └── setup.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 BruJu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Hydra library for Python 2 | ======================== 3 | 4 | The primary goal is to provide a lib for easily writing Hydra-enabled clients [1]_. 5 | 6 | A secondary goal is to provide a client for Triple Patterns Fragments [2]_, 7 | and an RDFlib [3]_ Store backed on any TPF service. 8 | 9 | Installation 10 | ++++++++++++ 11 | 12 | To install this library, from the projet directory, type:: 13 | 14 | pip install . 15 | 16 | NB: developers might want to add the ``-e`` option to the command line above, 17 | so that modifications to the source are automatically taken into account. 18 | 19 | Quick start 20 | +++++++++++ 21 | 22 | To create a Hydra-enabled resource, use: 23 | 24 | .. code:: python 25 | 26 | from hydra import Resource, SCHEMA 27 | res = Resource.from_iri(the_iri_of_the_resource) 28 | 29 | If the resource has an API documentation associated with it, 30 | it will be available as an attribute. 31 | The API documentation provides access to the supported class, 32 | their supported properties and operations. 33 | 34 | .. code:: python 35 | 36 | print("Api documentation:") 37 | for supcls in res.api_documentation.supported_classes: 38 | print(" %s" % supcls.identifier) 39 | for supop in supcls.supported_operations: 40 | print(" %s" % supop.identifier) 41 | 42 | Alternatively, 43 | you can query the resource directly for available operations. 44 | For example, the following searches for a suitable operation for creating a new event, 45 | and performs it. 46 | 47 | .. code:: python 48 | 49 | create_event = res.find_suitable_operation(SCHEMA.AddAction, SCHEMA.Event) 50 | resp, body = create_event({ 51 | "@context": "http://schema.org/", 52 | "@type": "http://schema.org/Event", 53 | "name": "Halloween", 54 | "description": "This is halloween, this is halloween", 55 | "startDate": "2015-10-31T00:00:00Z", 56 | "endDate": "2015-10-31T23:59:59Z", 57 | }) 58 | assert resp.status == 201, "%s %s" % (resp.status, resp.reason) 59 | new_event = Resource.from_iri(resp['location']) 60 | 61 | And you can go on with the new event you just created... 62 | 63 | Triple Pattern Fragments 64 | ++++++++++++++++++++++++ 65 | 66 | The ``hydra.tpf`` module implements of Triple Pattern Fragments specification [2]_. 67 | In particular, it provides an implementation of Store, 68 | so that TPF services can be used transparently: 69 | 70 | .. code:: python 71 | 72 | import hydra.tpf # ensures that the TPFStore plugin is registered 73 | from rdflib import Graph 74 | 75 | g = Graph('TPFStore') 76 | g.open('http://data.linkeddatafragments.org/dbpedia2014') 77 | 78 | results = g.query("SELECT DISTINCT ?cls { [ a ?cls ] } LIMIT 10") 79 | 80 | Note however that this is experimental at the moment... 81 | 82 | References 83 | ++++++++++ 84 | 85 | .. [1] http://www.hydra-cg.com/ 86 | .. [2] http://www.hydra-cg.com/spec/latest/triple-pattern-fragments/ 87 | .. [3] https://rdflib.readthedocs.org/ 88 | 89 | -------------------------------------------------------------------------------- /examples/cleanup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example from the README 3 | """ 4 | 5 | import logging 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | 9 | def main(): 10 | 11 | from hydra import Collection, Resource, SCHEMA 12 | res = Collection.from_iri("http://www.markus-lanthaler.com/hydra/event-api/events/") 13 | print res 14 | 15 | for i in res.members: 16 | # below, we must force to load each member, 17 | # because the collection contains *some* info about its members, 18 | # causing the code to assume that *all* info is present in the collection graph 19 | i = Resource.from_iri(i.identifier) 20 | name = i.value(SCHEMA.name) 21 | if "hydra-py" in name or "py-hydra" in name or "Halloween" in name: 22 | resp, _ = i.find_suitable_operation(SCHEMA.DeleteAction)() 23 | if resp.status // 100 != 2: 24 | print("error deleting <%s>" % i.identifier) 25 | else: 26 | print("deleted <%s>" % i.identifier) 27 | 28 | main() -------------------------------------------------------------------------------- /examples/test0.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example from the README 3 | """ 4 | def main(): 5 | the_iri_of_the_resource = 'http://www.markus-lanthaler.com/hydra/event-api/' 6 | 7 | from hydra import Resource, SCHEMA 8 | res = Resource.from_iri(the_iri_of_the_resource) 9 | 10 | print(res) 11 | 12 | print("\nApi documentation:") 13 | for supcls in res.api_documentation.supported_classes: 14 | print(" %s" % supcls.identifier) 15 | for supop in supcls.supported_operations: 16 | print(" %s" % supop.identifier) 17 | print("") 18 | 19 | create_event = res.find_suitable_operation(SCHEMA.AddAction, SCHEMA.Event) 20 | resp, body = create_event({ 21 | "@context": "http://schema.org/", 22 | "@type": "http://schema.org/Event", 23 | "name": "Halloween", 24 | "description": "This is halloween, this is halloween", 25 | "startDate": "2015-10-31T00:00:00Z", 26 | "endDate": "2015-10-31T23:59:59Z", 27 | }) 28 | assert resp.status == 201, "%s %s" % (resp.status, resp.reason) 29 | new_event = Resource.from_iri(resp['location']) 30 | 31 | print(new_event) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() -------------------------------------------------------------------------------- /examples/test1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dumps the content of an Hydra API documentation. 3 | """ 4 | 5 | import logging 6 | import sys 7 | logging.basicConfig(level=logging.WARNING) 8 | 9 | import hydra 10 | 11 | URL = 'http://www.markus-lanthaler.com/hydra/event-api/' 12 | if len(sys.argv) > 1: 13 | URL = sys.argv[1] 14 | 15 | service = hydra.Resource.from_iri(URL) 16 | #print service.graph.serialize(format="n3") + "\n--------\n" 17 | 18 | apidoc = service.api_documentation 19 | assert apidoc is not None 20 | 21 | print "SERVICE:", URL 22 | print "API DOC:", apidoc.identifier.n3() 23 | if apidoc.title: 24 | print " title:", apidoc.title 25 | print 26 | 27 | for cls in apidoc.supported_classes: 28 | print "SUPPORTED CLASS: ", cls.identifier 29 | for pr in cls.supported_properties: 30 | print " SUPPORTED PROPERTY:", pr.identifier 31 | print " property:", pr.property.identifier 32 | if pr.title: 33 | print " title:", pr.title 34 | print " required:", pr.required 35 | print " readable:", pr.readable 36 | print " writeable:", pr.writeable 37 | print " readonly:", pr.readonly 38 | print " writeonly:", pr.writeonly 39 | for op in pr.property.supported_operations: 40 | print " SUPPORTED OPERATION:", op.identifier.n3() 41 | if op.title: 42 | print " title:", op.title 43 | print " method:", op.method 44 | if op.expected_class: 45 | print " expects:", op.expected_class.identifier 46 | if op.returned_class: 47 | print " returns:", op.returned_class.identifier 48 | for op in cls.supported_operations: 49 | print " SUPPORTED OPERATION: ", op.identifier.n3() 50 | if op.title: 51 | print " title:", op.title 52 | print " types: ", ",".join(i.n3() for i in op.types) 53 | print " method:", op.method 54 | if op.expected_class: 55 | print " expects:", op.expected_class.identifier 56 | if op.returned_class: 57 | print " returns:", op.returned_class.identifier 58 | print 59 | 60 | -------------------------------------------------------------------------------- /examples/test2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Examine a given collection for a hydra:search property, 3 | and dumps the corresponding IRI template. 4 | """ 5 | 6 | import sys 7 | import logging 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | import hydra 11 | 12 | URL = 'http://data.linkeddatafragments.org/dbpedia2014#dataset' 13 | if len(sys.argv) > 1: 14 | URL = sys.argv[1] 15 | 16 | collec = hydra.Collection.from_iri(URL) 17 | #print collec.graph.serialize(format="nquads") + "\n--------\n" 18 | 19 | print "COLLECTION: " + collec.identifier 20 | print list(collec[hydra.HYDRA.search]) 21 | for st in collec.iri_templates: 22 | print " SEARCH TEMPLATE: " + st.identifier.n3() 23 | print " template: ", st.template 24 | print " tpl_type: ", st.template_type 25 | print " vat_repr: ", st.variable_representation 26 | example = {} 27 | for m in st.mappings: 28 | print " VARIABLE: ", m.variable 29 | print " property: ", m.property 30 | print " required: ", m.required 31 | example[m.property] = m.variable[0] + " " + m.variable[-1] 32 | print " EXAMPLE: ", st.generate_iri(example) 33 | 34 | -------------------------------------------------------------------------------- /examples/test3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing find_suitable_template with background knowledge 3 | """ 4 | 5 | import sys 6 | from rdflib import Namespace, RDF, RDFS 7 | import logging 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | import hydra 11 | 12 | 13 | EX = Namespace("http://example.org/") 14 | hydra.BACKGROUND_KNOWLEDGE.add((EX.p, RDFS.subPropertyOf, RDF.predicate)) 15 | hydra.BACKGROUND_KNOWLEDGE.add((EX.o, RDFS.subPropertyOf, EX.o2)) 16 | hydra.BACKGROUND_KNOWLEDGE.add((EX.o2, RDFS.subPropertyOf, RDF.object)) 17 | 18 | URL = 'http://data.linkeddatafragments.org/dbpedia2014#dataset' 19 | if len(sys.argv) > 1: 20 | URL = sys.argv[1] 21 | 22 | collec = hydra.Collection.from_iri(URL) 23 | #print collec.graph.serialize(format="nquads") + "\n--------\n" 24 | 25 | template = collec.find_suitable_template([RDF.subject, EX.p, EX.o]) 26 | print template.generate_iri({ RDF.subject: "X", EX.p: "Y", EX.o: "Z" }) 27 | -------------------------------------------------------------------------------- /examples/test4.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use the tpf module to query a TPF-enabled dataset. 3 | """ 4 | import logging 5 | import sys 6 | from rdflib import Graph, Namespace, RDF, RDFS 7 | logging.basicConfig(level=logging.INFO) 8 | 9 | from hydra.tpf import TPFAwareCollection 10 | 11 | 12 | 13 | URL = 'http://data.linkeddatafragments.org/dbpedia2014#dataset' 14 | if len(sys.argv) > 1: 15 | URL = sys.argv[1] 16 | 17 | collec = TPFAwareCollection.from_iri(URL) 18 | #print collec.graph.serialize(format="nquads") + "\n--------\n" 19 | 20 | g = Graph() 21 | for i, t in enumerate(collec.iter_triples(None, RDF.type, None)): 22 | #print i, t 23 | if i == 350: 24 | break 25 | assert t[1] == RDF.type 26 | g.add(t) 27 | print len(g) -------------------------------------------------------------------------------- /examples/test5.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use TPFStore and performs a SPARQL query on top of it. 3 | Note that this is very (very!) slow as soon as the query becomes slightly complex... :-/ 4 | """ 5 | import logging 6 | from rdflib import Graph 7 | import sys 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | import hydra.tpf # required to register TPFStore plugin 11 | 12 | URL = 'http://data.linkeddatafragments.org/dbpedia2014' 13 | if len(sys.argv) > 1: 14 | URL = sys.argv[1] 15 | 16 | g = Graph("TPFStore") 17 | g.open(URL) 18 | 19 | QUERY = """ 20 | PREFIX rdfs: 21 | PREFIX dbr: 22 | PREFIX dbo: 23 | 24 | SELECT * { 25 | ?p a dbo:Person; dbo:birthPlace ?bp . 26 | } 27 | LIMIT 10 28 | """ 29 | 30 | print len(g) 31 | 32 | results = g.query(QUERY) 33 | for i in results: 34 | print i 35 | -------------------------------------------------------------------------------- /examples/test6.py: -------------------------------------------------------------------------------- 1 | """ 2 | A test with Markus' demo. 3 | """ 4 | from datetime import datetime, timedelta 5 | import logging 6 | from rdflib import Graph 7 | import sys 8 | logging.basicConfig(level=logging.WARNING) 9 | 10 | from hydra import ApiDocumentation, HYDRA, Resource, SCHEMA 11 | 12 | 13 | URL = 'http://www.markus-lanthaler.com/hydra/event-api/' 14 | if len(sys.argv) > 1: 15 | URL = sys.argv[1] 16 | 17 | entrypoint = ApiDocumentation.from_iri(URL) 18 | 19 | print "All operations:" 20 | 21 | for op in entrypoint.all_operations: 22 | print op.identifier, op.method, op.target_iri 23 | 24 | print "\nCreating new event" 25 | add_event = entrypoint.find_suitable_operation(SCHEMA.AddAction, SCHEMA.Event) 26 | start = datetime.utcnow() 27 | end = (start + timedelta(0,1)) 28 | resp, body = add_event({ 29 | "@context": "http://www.markus-lanthaler.com/hydra/event-api/contexts/Event.jsonld", 30 | "@type": "Event", 31 | "name": "Testing hydra-py", 32 | "description": "In the process of testing hyda-py", 33 | "start_date": start.isoformat()[:19] + 'Z', 34 | "end_date": end.isoformat()[:19] + 'Z', 35 | }) 36 | if resp.status != 201: 37 | print "failed... (%s %s)" % (resp.status, resp.reason) 38 | evt = Resource.from_iri(resp['location']) 39 | print evt.identifier 40 | 41 | 42 | print "\nUpdating event" 43 | evt.set(SCHEMA.description, evt.value(SCHEMA.description)+" (updated)") 44 | update_event = evt.find_suitable_operation(SCHEMA.UpdateAction, SCHEMA.Event) 45 | resp, body = update_event(evt.graph.default_context) 46 | if resp.status // 100 != 2: 47 | print "failed... (%s %s)" % (resp.status, resp.reason) 48 | else: 49 | print "succeeded" 50 | exit() 51 | print "trying again with ad-hoc JSON" 52 | resp, body = update_event({ 53 | "@context": "http://www.markus-lanthaler.com/hydra/event-api/contexts/Event.jsonld", 54 | "@type": "Event", 55 | "@id": unicode(evt.identifier), 56 | "name": unicode(evt.value(SCHEMA.name)), 57 | "description": unicode(evt.value(SCHEMA.description)), 58 | "start_date": unicode(evt.value(SCHEMA.startDate)), 59 | "end_date": unicode(evt.value(SCHEMA.endDate)), 60 | }) 61 | if resp.status // 100 != 2: 62 | print "failed... (%s %s)" % (resp.status, resp.reason) 63 | else: 64 | print "succeeded" 65 | -------------------------------------------------------------------------------- /lib/hydra/__init__.py: -------------------------------------------------------------------------------- 1 | from httplib2 import Http, HttpLib2ErrorWithResponse 2 | import json 3 | import logging 4 | from rdflib import BNode, ConjunctiveGraph, Graph, Literal, Namespace, RDF, RDFS, URIRef, XSD 5 | from rdflib.resource import Resource as RdflibResource 6 | from rdflib.parser import StringInputSource 7 | from rdflib.plugin import register, Parser, Serializer 8 | from re import compile as regex 9 | from uritemplate import expand 10 | from warnings import warn 11 | 12 | register('application/ld+json', Parser, 'rdflib_jsonld.parser', 'JsonLDParser') 13 | register('application/ld+json', Serializer, 'rdflib_jsonld.serializer', 'JsonLDSerializer') 14 | LOG = logging.getLogger(__name__) 15 | 16 | 17 | 18 | __version__ = "0.1" 19 | 20 | 21 | 22 | HYDRA = Namespace('http://www.w3.org/ns/hydra/core#') 23 | SCHEMA = Namespace('http://schema.org/') 24 | 25 | class NullLiteral(object): 26 | def toPython(self): 27 | return None 28 | NULL = NullLiteral() 29 | 30 | # the following is useful in classes that define an attribute named 'property' 31 | _py_property = property 32 | 33 | 34 | class Resource(RdflibResource): 35 | 36 | @classmethod 37 | def from_iri(cls, iri, headers=None, http=None): 38 | ret = cls(None, URIRef(iri)) 39 | del ret._graph # graph will be lazily loaded (see property _graph below) 40 | ret._headers = headers 41 | ret._http = http 42 | return ret 43 | 44 | @classmethod 45 | def from_peer(cls, identifier, resource, headers=None, http=None): 46 | """ 47 | I build an instance of this class for one of its peer resources. 48 | 49 | If identifier is a BNode or has the same base IRI as resource, 50 | then I reuse resource's graph instead of downloading it. 51 | 52 | I also reuse resource's graph if it contains at least one out-goind triple with identifier as subject. 53 | This is a (somewhat daring) heuristic, 54 | but is required in many cases where a class or a property is described directly in the API Documentation. 55 | """ 56 | if type(identifier) is BNode \ 57 | or resource.identifier.split('#', 1)[0] == identifier.split('#', 1)[0] \ 58 | or (identifier, None, None) in resource.graph: 59 | return cls(resource.graph, identifier) 60 | else: 61 | return cls.from_iri(identifier, headers, http) 62 | 63 | 64 | @property 65 | def _graph(self): 66 | """Lazy loading of the _graph attribute 67 | 68 | This property getter will be called only when the instance attribute self._graph has been deleted. 69 | In that case, it will load the graph from self.identifier. 70 | 71 | This is used by the `from_iri`:meth: class method, 72 | to ensure that graphs are only loaded when required... 73 | """ 74 | if '_graph' in self.__dict__: 75 | return self.__dict__['_graph'] 76 | 77 | headers = self.__dict__.pop('_headers') 78 | http = self.__dict__.pop('_http') 79 | base_iri = self._identifier.split('#', 1)[0] 80 | effective_headers = dict(DEFAULT_REQUEST_HEADERS) 81 | if headers: 82 | effective_headers.update(headers) 83 | http = http or DEFAULT_HTTP_CLIENT 84 | 85 | LOG.info('downloading <%s>', base_iri) 86 | response, content = http.request(base_iri, "GET", headers=effective_headers) 87 | LOG.debug('got %s %s %s', response.status, response['content-type'], response.fromcache) 88 | if response.status // 100 != 2: 89 | raise HttpLib2ErrorWithResponse(response.reason, response, content) 90 | 91 | source = StringInputSource(content) 92 | ctype = response['content-type'].split(';',1)[0] 93 | g = ConjunctiveGraph(identifier=base_iri) 94 | g.addN(BACKGROUND_KNOWLEDGE.quads()) 95 | g.parse(source, base_iri, ctype) 96 | _fix_default_graph(g) 97 | 98 | # if available, load API Documentation in a separate graph 99 | links = response.get('link') 100 | if links: 101 | if type(links) != list: 102 | links = [links] 103 | for link in links: 104 | match = APIDOC_RE.match(link) 105 | if match: 106 | self._api_doc = apidoc_iri = URIRef(match.groups()[0]) 107 | if apidoc_iri != self.identifier: 108 | apidoc = ApiDocumentation.from_iri(apidoc_iri, headers, http) 109 | g.addN(apidoc.graph.quads()) 110 | break 111 | 112 | self.__dict__['_graph'] = g 113 | return g 114 | @_graph.setter 115 | def _graph(self, value): 116 | """Ensures that the instance attribute self._graph can still be set.""" 117 | self.__dict__['_graph'] = value 118 | @_graph.deleter 119 | def _graph(self): 120 | """Ensures that the instance attribute self._graph can still be deleted.""" 121 | del self.__dict__['_graph'] 122 | 123 | 124 | 125 | def get_api_documentation(self): 126 | graph = self._graph # ensures that graph is loaded 127 | if self._api_doc: 128 | return ApiDocumentation(graph, self._api_doc) 129 | else: 130 | return None 131 | api_documentation = property(get_api_documentation) 132 | _api_doc = None 133 | 134 | def get_title(self): 135 | return self.graph \ 136 | .value(self.identifier, HYDRA.title, default=NULL).toPython() 137 | title = property(get_title) 138 | 139 | def get_description(self): 140 | return self.graph \ 141 | .value(self.identifier, HYDRA.description, default=NULL).toPython() 142 | description = property(get_description) 143 | 144 | def iter_types(self): 145 | return self.graph.objects(self.identifier, RDF.type) 146 | types = property(iter_types) 147 | 148 | def iter_operations(self, headers=None, http=None): 149 | self_identifier = self.identifier 150 | for obj in self.graph.objects(self.identifier, HYDRA.operation): 151 | yield Operation.from_peer(obj, self, headers, http).bound(self_identifier) 152 | operations = property(iter_operations) 153 | 154 | def iter_all_operations(self, headers=None, http=None): 155 | for op in self.iter_operations(headers, http): 156 | yield op 157 | 158 | graph = self.graph 159 | identifier = self.identifier 160 | 161 | for op in graph.objects(identifier, TYPE_OP): 162 | yield Operation.from_peer(op, self).bound(identifier) 163 | 164 | for prop, target in graph.predicate_objects (identifier): 165 | for op in graph.objects(prop, LINK_OP): 166 | yield Operation.from_peer(op, self).bound(target) 167 | for op in graph.objects(prop, RANGE_OP): 168 | yield Operation.from_peer(op, self).bound(target) 169 | 170 | all_operations = property(iter_all_operations) 171 | 172 | def iter_suitable_operations(self, operation_type=None, 173 | input_type=None, output_type=None, 174 | headers=None, http=None): 175 | for op in self.iter_all_operations(headers, http): 176 | if op.is_suitable_for(operation_type, input_type, output_type): 177 | yield op 178 | 179 | def find_suitable_operation(self, operation_type=None, 180 | input_type=None, output_type=None, 181 | headers=None, http=None): 182 | for op in self.iter_suitable_operations(operation_type, input_type, output_type, headers, http): 183 | return op 184 | return None 185 | 186 | def iter_iri_templates(self, templated_link=HYDRA.search, headers=None, http=None): 187 | for obj in self.graph.objects(self.identifier, templated_link): 188 | yield IriTemplate.from_peer(obj, self, headers, http) 189 | iri_templates = property(iter_iri_templates) 190 | 191 | def iter_suitable_template(self, properties, templated_link=HYDRA.search, headers=None, http=None): 192 | graph = self.graph 193 | for iri_template in self.iter_iri_templates(templated_link, headers, http): 194 | if iri_template.is_suitable_for(properties): 195 | yield iri_template 196 | 197 | def find_suitable_template(self, properties, templated_link=HYDRA.search, headers=None, http=None): 198 | for iri_template in self.iter_suitable_template(properties, templated_link, headers, http): 199 | return iri_template 200 | return None 201 | 202 | def freetext_query(self, query, templated_link=HYDRA.search, headers=None, http=None): 203 | fulltext_search_template = self.find_suitable_template([HYDRA.freetextQuery], templated_link, headers, http) 204 | result_iri = fulltext_search_template.generate_iri({HYDRA.freetextQuery: query}) 205 | return Collection.from_iri(result_iri, headers, http) 206 | 207 | 208 | 209 | class ApiDocumentation(Resource): 210 | 211 | def iter_supported_classes(self, headers=None, http=None): 212 | for obj in self.graph.objects(self.identifier, HYDRA.supportedClass): 213 | yield Class.from_peer(obj, self, headers, http) 214 | supported_classes = property(iter_supported_classes) 215 | 216 | def iter_possible_status(self, headers=None, http=None): 217 | for obj in self.graph.objects(self.identifier, HYDRA.possibleStatus): 218 | yield Status.from_peer(obj, self, headers, http) 219 | possible_status = property(iter_possible_status) 220 | 221 | def get_entrypoint(self, headers=None, http=None): 222 | uri = self.graph.value(self.identifier, HYDRA.entrypoint) 223 | if uri is None: 224 | return None 225 | else: 226 | return Resource.from_peer(uri, self, headers, None) 227 | entrypoint = property(get_entrypoint) 228 | 229 | 230 | 231 | class Class(Resource): 232 | 233 | def iter_supported_properties(self, headers=None, http=None): 234 | for obj in self.graph.objects(self.identifier, HYDRA.supportedProperty): 235 | yield SupportedProperty.from_peer(obj, self, headers, http) 236 | supported_properties = property(iter_supported_properties) 237 | 238 | def iter_supported_operations(self, headers=None, http=None): 239 | for obj in self.graph.objects(self.identifier, HYDRA.supportedOperation): 240 | yield Operation.from_peer(obj, self, headers, http) 241 | supported_operations = property(iter_supported_operations) 242 | 243 | 244 | class Status(Resource): 245 | 246 | def get_status_code(self): 247 | return self.graph \ 248 | .value(self.identifier, HYDRA.statusCode, NULL).toPython() 249 | status_code = property(get_status_code) 250 | 251 | 252 | class Operation(Resource): 253 | 254 | def get_method(self): 255 | return self.graph \ 256 | .value(self.identifier, HYDRA.method, default=NULL).toPython() 257 | method = property(get_method) 258 | 259 | def get_expected_class(self, headers=None, http=None): 260 | uri = self.graph.value(self.identifier, HYDRA.expects) 261 | if uri is None: 262 | return None 263 | else: 264 | return Class.from_peer(uri, self, headers, http) 265 | expected_class = property(get_expected_class) 266 | 267 | def get_returned_class(self, headers=None, http=None): 268 | uri = self.graph.value(self.identifier, HYDRA.returns) 269 | if uri is None: 270 | return None 271 | else: 272 | return Class.from_peer(uri, self, headers, http) 273 | returned_class = property(get_returned_class) 274 | 275 | def iter_possible_status(self, headers=None, http=None): 276 | for obj in self.graph.objects(self.identifier, HYDRA.possibleStatus): 277 | yield Status.from_peer(obj, self, headers, http) 278 | possible_status = property(iter_possible_status) 279 | 280 | def is_suitable_for(self, operation_type=None, input_type=None, output_type=None): 281 | if operation_type is not None: 282 | if (self.identifier, TYPE, operation_type) not in self.graph: 283 | return False 284 | if input_type is not None: 285 | if self.expected_class is None \ 286 | or (input_type, SUBCLASS, self.expected_class.identifier) not in self.graph: 287 | return False 288 | if output_type is not None: 289 | if self.returned_class is None \ 290 | or (self.returned_class.identifier, SUBCLASS, output_type) not in self.graph: 291 | return False 292 | return True 293 | 294 | def bound(self, target_iri): 295 | return BoundOperation(self, target_iri) 296 | 297 | 298 | class BoundOperation(Operation): 299 | 300 | def __init__(self, unbound, target_iri): 301 | Operation.__init__(self, unbound.graph, unbound.identifier) 302 | self.target_iri = target_iri 303 | 304 | def _new(self, identifier): 305 | """Required as __init__ breaks compatibility with superclass""" 306 | return Resource(self._graph, identifier) 307 | 308 | def perform(self, body=None, headers=None, http=None): 309 | LOG.debug("perform: %s <%s>", self.method, self.target_iri) 310 | effective_headers = dict(DEFAULT_REQUEST_HEADERS) 311 | if headers: 312 | effective_headers.update(headers) 313 | http = http or DEFAULT_HTTP_CLIENT 314 | 315 | if type(body) is dict: 316 | body = json.dumps(body) 317 | effective_headers['content-type'] = 'application/ld+json' 318 | elif isinstance(body, Graph): 319 | ctype = effective_headers.setdefault('content-type', 'application/ld+json') 320 | body = body.serialize(format=ctype) 321 | 322 | return http.request(unicode(self.target_iri), 323 | self.method, 324 | body, 325 | effective_headers) 326 | # TODO should we provide a higher abstraction level for perform() 327 | 328 | def __call__(self, *args, **kw): 329 | return self.perform(*args, **kw) 330 | 331 | 332 | class SupportedProperty(Resource): 333 | 334 | def get_property(self, headers=None, http=None): 335 | prop = self.graph.value(self.identifier, HYDRA.property) 336 | if prop is not None: 337 | return Property.from_peer(prop, self, headers, http) 338 | else: 339 | return None 340 | property = _py_property(get_property) 341 | 342 | def get_required(self): 343 | return self.graph \ 344 | .value(self.identifier, HYDRA.required, default=NULL).toPython() 345 | required = _py_property(get_required) 346 | 347 | def get_readable(self): 348 | return self.graph \ 349 | .value(self.identifier, HYDRA.readable, default=NULL).toPython() 350 | readable = _py_property(get_readable) 351 | 352 | def get_writeable(self): 353 | return self.graph \ 354 | .value(self.identifier, HYDRA.writeable, default=NULL).toPython() 355 | writeable = _py_property(get_writeable) 356 | 357 | # those seem to be deprecated from the spec, but are still used in the spec 358 | 359 | def get_readonly(self): 360 | return self.graph \ 361 | .value(self.identifier, HYDRA.readonly, default=NULL).toPython() 362 | readonly = _py_property(get_readonly) 363 | 364 | def get_writeonly(self): 365 | return self.graph \ 366 | .value(self.identifier, HYDRA.writeonly, default=NULL).toPython() 367 | writeonly = _py_property(get_writeonly) 368 | 369 | 370 | class Property(Resource): 371 | 372 | def is_link(self): 373 | the_triple = (self.identifier, RDF.type, HYDRA.Link) 374 | return (the_triple in self.graph) 375 | link = property(is_link) 376 | 377 | def iter_supported_operations(self, headers=None, http=None): 378 | for obj in self.graph.objects(self.identifier, HYDRA.supportedOperation): 379 | yield Operation.from_peer(obj, self, headers, http) 380 | supported_operations = property(iter_supported_operations) 381 | 382 | 383 | class Collection(Resource): 384 | 385 | def get_total_items(self): 386 | return self.graph \ 387 | .value(self.identifier, HYDRA.totalItems, default=NULL).toPython() 388 | total_items = _py_property(get_total_items) 389 | 390 | def iter_members(self, member_class=Resource, headers=None, http=None): 391 | for obj in self.graph.objects(self.identifier, HYDRA.member): 392 | yield member_class.from_peer(obj, self, headers, http) 393 | members = property(iter_members) 394 | 395 | def is_paged(self): 396 | the_triple = (self.identifier, RDF.type, HYDRA.PagedCollection) 397 | return (the_triple in self.graph) 398 | paged = property(is_paged) 399 | 400 | def get_items_per_page(self): 401 | return self.graph \ 402 | .value(self.identifier, HYDRA.itemsPerPage, default=NULL).toPython() 403 | items_per_page = _py_property(get_items_per_page) 404 | 405 | def get_first_page(self, headers=None, http=None): 406 | warn("get_first_page/first_page is deprecated; " 407 | "use get_first/first instead", stacklevel=2) 408 | obj = self.graph.value(self.identifier, HYDRA.firstPage) 409 | if obj: 410 | return Collection.from_iri(obj, headers, http) 411 | else: 412 | return None 413 | first_page = _py_property(get_first_page) 414 | 415 | def get_first(self, headers=None, http=None): 416 | obj = self.graph.value(self.identifier, HYDRA.first) 417 | if obj: 418 | return Collection.from_iri(obj, headers, http) 419 | else: 420 | return None 421 | first = _py_property(get_first) 422 | 423 | def get_last_page(self, headers=None, http=None): 424 | warn("get_last_page/last_page is deprecated; " 425 | "use get_last/last instead", stacklevel=2) 426 | obj = self.graph.value(self.identifier, HYDRA.lastPage) 427 | if obj: 428 | return Collection.from_iri(obj, headers, http) 429 | else: 430 | return None 431 | last_page = _py_property(get_last_page) 432 | 433 | def get_last(self, headers=None, http=None): 434 | obj = self.graph.value(self.identifier, HYDRA.last) 435 | if obj: 436 | return Collection.from_iri(obj, headers, http) 437 | else: 438 | return None 439 | last = _py_property(get_last) 440 | 441 | def get_next_page(self, headers=None, http=None): 442 | warn("get_next_page/next_page is deprecated; " 443 | "use get_next/next instead", stacklevel=2) 444 | obj = self.graph.value(self.identifier, HYDRA.nextPage) 445 | if obj: 446 | return Collection.from_iri(obj, headers, http) 447 | else: 448 | return None 449 | next_page = _py_property(get_next_page) 450 | 451 | def get_next(self, headers=None, http=None): 452 | obj = self.graph.value(self.identifier, HYDRA.next) 453 | if obj: 454 | return Collection.from_iri(obj, headers, http) 455 | else: 456 | return None 457 | next = _py_property(get_next) 458 | 459 | def get_previous_page(self, headers=None, http=None): 460 | warn("get_previous_page/previous_page is deprecated; " 461 | "use get_previous/previous instead", stacklevel=2) 462 | obj = self.graph.value(self.identifier, HYDRA.previousPage) 463 | if obj: 464 | return Collection.from_iri(obj, headers, http) 465 | else: 466 | return None 467 | previous_page = _py_property(get_previous_page) 468 | 469 | def get_previous(self, headers=None, http=None): 470 | obj = self.graph.value(self.identifier, HYDRA.previous) 471 | if obj: 472 | return Collection.from_iri(obj, headers, http) 473 | else: 474 | return None 475 | previous = _py_property(get_previous) 476 | 477 | def iter_pages(self): 478 | i = self 479 | while i is not None: 480 | yield i 481 | i = i.next 482 | pages = property(iter_pages) 483 | 484 | 485 | 486 | class IriTemplate(Resource): 487 | 488 | def get_template(self): 489 | return self.graph \ 490 | .value(self.identifier, HYDRA.template, default=NULL).toPython() 491 | template = property(get_template) 492 | 493 | def get_template_type(self): 494 | lit = self.graph.value(self.identifier, HYDRA.template) 495 | if lit is None or lit.datatype == XSD.String: 496 | return HYDRA.Rfc6570Template 497 | else: 498 | return lit.datatype 499 | template_type = property(get_template_type) 500 | 501 | def iter_mappings(self, headers=None, http=None): 502 | for obj in self.graph.objects(self.identifier, HYDRA.mapping): 503 | yield IriTemplateMapping.from_peer(obj, self, headers, http) 504 | mappings = property(iter_mappings) 505 | 506 | def get_variable_representation(self, default=HYDRA.BasicRepresentation): 507 | return self.graph \ 508 | .value(self.identifier, HYDRA.variableRepresentation, default=HYDRA.BasicRepresentation) 509 | variable_representation = property(get_variable_representation) 510 | 511 | def is_suitable_for(self, properties): 512 | graph = self.graph 513 | values = { URIRef(prop): 1 for prop in properties } 514 | map, _ = self._map_properties(values) 515 | return (map is not None) 516 | 517 | def generate_iri(self, values, default_representation=HYDRA.BasicRepresentation): 518 | """ 519 | Generate an IRI according to this template. 520 | 521 | This method takes care of formatting the provided values according to the template's variableRepresentation. 522 | 523 | :param values: a dict whose keys are properties, and values are RDF terms 524 | :return: the IRI as a string 525 | 526 | NB: if required properties are missing from `values`, a ValueError is raised. 527 | """ 528 | # ensure that keys are URIRefs in values 529 | graph = self.graph 530 | values = { URIRef(key):value for key, value in values.items() } 531 | representation = self.get_variable_representation(default=default_representation) 532 | mode = 0 if representation is HYDRA.BasicRepresentation else 1 533 | data, msg = self._map_properties(values) 534 | if data is None: 535 | raise ValueError(msg) 536 | data = { key: _format_variable(value, mode) 537 | for key, value in data.items() } 538 | return expand(self.template, data) 539 | 540 | def _map_properties(self, values): 541 | graph = self.graph 542 | data = {} 543 | for mapping in self.mappings: 544 | for prop, value in values.iteritems(): 545 | if (prop, SUBPROP, mapping.property) in graph: 546 | values.pop(prop) 547 | if value is not None: 548 | data[mapping.variable] = value 549 | break 550 | else: # for-else: we didn't break out of the loop 551 | if mapping.required: 552 | return None, "Required property <%s> is missing" % property 553 | if values: 554 | return None, "Service does not support properties %s" % values.keys() 555 | return data, None 556 | 557 | 558 | 559 | def _format_variable(term, mode): 560 | if mode == 0: # Basic 561 | return unicode(term) 562 | else: # explicit 563 | if type(term) is Literal: 564 | ret = u'"%s"' % term 565 | if term.datatype and term.datatype != XSD.String: 566 | ret = u'%s^^%s' % (ret, term.datatype) 567 | elif term.language: 568 | ret = u'%s@%s' % (ret, term.language) 569 | return ret.encode('utf-8') 570 | else: 571 | return unicode(term).encode('utf-8') 572 | 573 | 574 | class IriTemplateMapping(Resource): 575 | 576 | def get_variable(self): 577 | return self.graph \ 578 | .value(self.identifier, HYDRA.variable, default=NULL).toPython() 579 | variable = _py_property(get_variable) 580 | 581 | def get_property(self): 582 | return self.graph \ 583 | .value(self.identifier, HYDRA.property) 584 | property = _py_property(get_property) 585 | 586 | def get_required(self): 587 | return self.graph \ 588 | .value(self.identifier, HYDRA.required, default=NULL).toPython() 589 | required = _py_property(get_required) 590 | 591 | 592 | 593 | 594 | def _fix_default_graph(cg): 595 | """ 596 | Find the *real* default graph of a ConjunctiveGraph. 597 | 598 | This is a workaround required by a bug in the JSON-LD parser. 599 | """ 600 | if len(cg.default_context) > 0: 601 | if len(list(cg.contexts())) > 1: 602 | LOG.warn("It seems that _fix_default_graph is not needed anymore...") 603 | return cg.default_context 604 | for g in cg.contexts(): 605 | if type(g.identifier) is BNode: 606 | cg.default_context = g 607 | 608 | class _MemCache(dict): 609 | 610 | def __nonzero__(self): 611 | # even if empty, a _MemCache is True 612 | return True 613 | 614 | def set(self, key, value): 615 | self[key] = value 616 | 617 | def delete(self, key): 618 | if key in self: 619 | del self[key] 620 | 621 | DEFAULT_HTTP_CLIENT = Http(_MemCache()) 622 | DEFAULT_REQUEST_HEADERS = { 623 | # NB: the spaces and line-breaks in 'accept' below are a hack 624 | # to work around a problem in httplib2: 625 | # the cache does not work with arbibtrary long lines 626 | "accept": "application/ld+json, application/n-quads;q=0.9,\r\n application/turtle;q=0.8, application/n-triples;q=0.7,\r\n application/rdf+xml;q=0.6, text/html;q=0.5, */*;q=0.1", 627 | "user-agent": "hydra-py-v" + __version__, 628 | } 629 | 630 | BACKGROUND_KNOWLEDGE = ConjunctiveGraph(identifier=URIRef("urn:x-hydra-py:background-knowledge")) 631 | 632 | SUBCLASS = RDFS.subClassOf * "*" 633 | SUBPROP = RDFS.subPropertyOf * "*" 634 | LINK_OP = SUBPROP / HYDRA.supportedOperation 635 | RANGE = RDFS.range / SUBCLASS 636 | RANGE_OP = RANGE / HYDRA.supportedOperation 637 | TYPE = RDF.type / SUBCLASS 638 | TYPE_OP = TYPE / HYDRA.supportedOperation 639 | 640 | APIDOC_RE = regex(r'^<([^>]*)>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"$') 641 | -------------------------------------------------------------------------------- /lib/hydra/tpf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Triple Pattern Fragments client 3 | 4 | http://www.hydra-cg.com/spec/latest/triple-pattern-fragments/ 5 | """ 6 | import logging 7 | from rdflib import Namespace, RDF 8 | from rdflib.plugin import register, Store 9 | 10 | from hydra import Collection, HYDRA, NULL 11 | 12 | LOG = logging.getLogger(__name__) 13 | 14 | VOID = Namespace('http://rdfs.org/ns/void#') 15 | 16 | class TriplePatternFragment(Collection): 17 | 18 | def iter_triples(self): 19 | for p in self.pages: 20 | for triple in p.graph.default_context: 21 | yield triple 22 | triples = property(iter_triples) 23 | 24 | def get_triple_count(self): 25 | return self.graph \ 26 | .value(self.identifier, VOID.triples, default=NULL).toPython() 27 | triple_count = property(get_triple_count) 28 | 29 | def get_dataset(self, headers=None, http=None): 30 | graph = self.graph 31 | for candidate in graph.subjects(VOID.subset, self.identifier): 32 | if (candidate, HYDRA.search, None) in graph: 33 | return TPFAwareCollection(graph, candidate) 34 | return None 35 | dataset = property(get_dataset) 36 | 37 | 38 | class TPFAwareCollection(Collection): 39 | 40 | triple_search = None 41 | 42 | def get_tpf(self, subject=None, predicate=None, object=None, headers=None, http=None): 43 | if self.triple_search is None: 44 | self.triple_search = self.find_suitable_template([ 45 | RDF.subject, RDF.predicate, RDF.object]) 46 | if self.triple_search is None: 47 | raise ValueError("Couldn't find any TPF search template") 48 | tpf_iri = self.triple_search.generate_iri({ 49 | RDF.subject: subject, 50 | RDF.predicate: predicate, 51 | RDF.object: object, 52 | }, default_representation=HYDRA.ExplicitRepresentation) 53 | return TriplePatternFragment.from_iri(tpf_iri, headers, http) 54 | 55 | def iter_triples(self, subject=None, predicate=None, object=None): 56 | return self.get_tpf(subject, predicate, object).iter_triples() 57 | 58 | 59 | class TPFStore(Store): 60 | 61 | def open(self, start_iri, create=False): 62 | self.collec = TriplePatternFragment.from_iri(start_iri).dataset 63 | 64 | def add(self, (subject, predicate, object), context, quoted=False): 65 | raise NotImplemented("TPFStore is readonly") 66 | 67 | def remove(self, (subject, predicate, object), context=None): 68 | raise NotImplemented("TPFStore is readonly") 69 | 70 | def triples(self, triple_pattern, context=None): 71 | for triple in self.collec.iter_triples(*triple_pattern): 72 | if _triple_match(triple_pattern, triple): 73 | yield triple, context 74 | 75 | def __len__(self, context=None): 76 | return self.collec.get_tpf().triple_count 77 | 78 | def _node_match(template_node, node): 79 | return 1 if (template_node is None or template_node == node) else 0 80 | 81 | def _triple_match(template_triple, triple): 82 | return sum(map(_node_match, template_triple, triple)) == 3 83 | 84 | register("TPFStore", Store, "hydra.tpf", "TPFStore") 85 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rdflib 2 | rdflib-jsonld 3 | uritemplate 4 | httplib2 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Setup script for the Python implementation of LD Patch 5 | """ 6 | 7 | from setuptools import setup 8 | 9 | from ast import literal_eval 10 | 11 | def get_version(source='lib/hydra/__init__.py'): 12 | """ 13 | Retrieve version number without importing the script. 14 | """ 15 | with open(source) as pyfile: 16 | for line in pyfile: 17 | if line.startswith('__version__'): 18 | return literal_eval(line.partition('=')[2].lstrip()) 19 | raise ValueError("VERSION not found") 20 | 21 | README = '' 22 | with open('README.rst', 'r') as f: 23 | README = f.read() 24 | 25 | INSTALL_REQ = [] 26 | with open('requirements.txt', 'r') as f: 27 | # Get requirements depencies as written in the file 28 | INSTALL_REQ = [ i[:-1] for i in f if i[0] != "#" ] 29 | 30 | setup(name = 'hydra', 31 | version = get_version(), 32 | package_dir = {'': 'lib'}, 33 | packages = ['hydra'], 34 | description = 'A Hydra implementation for Python', 35 | long_description = README, 36 | author='Pierre-Antoine Champin', 37 | author_email='pchampin@liris.cnrs.fr', 38 | license='LGPL v3', 39 | platforms='OS Independant', 40 | url='http://github.com/pchampin/hydra-py', 41 | include_package_data=True, 42 | install_requires=INSTALL_REQ, 43 | scripts=[], 44 | ) 45 | --------------------------------------------------------------------------------