diff options
author | Nicholas Car <nicholas.car@surroundaustralia.com> | 2020-05-05 22:02:22 +1000 |
---|---|---|
committer | Nicholas Car <nicholas.car@surroundaustralia.com> | 2020-05-05 22:02:22 +1000 |
commit | 802678ce0aba5ac15c27be4dab7519e79c34d2b9 (patch) | |
tree | f08b5cdede584912fc23f4e65d10bff38241b1bf | |
parent | 88338195dc18de91c7122c025cc619951c876043 (diff) | |
parent | f6fde7ed2bbd75793083631c04ae69bdc2496383 (diff) | |
download | rdflib-namespaces_all.tar.gz |
Merge branch 'master' into namespaces_allnamespaces_all
-rw-r--r-- | .travis.yml | 9 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | docs/_static/RDFlib-500.png | bin | 0 -> 26062 bytes | |||
-rw-r--r-- | docs/_static/RDFlib.ico (renamed from docs/_static/logo-rdflib.ico) | bin | 3262 -> 3262 bytes | |||
-rw-r--r-- | docs/_static/RDFlib.png | bin | 0 -> 21014 bytes | |||
-rw-r--r-- | docs/_static/RDFlib.svg | 47 | ||||
-rw-r--r-- | docs/conf.py | 2 | ||||
-rw-r--r-- | docs/sphinx-requirements.txt | 2 | ||||
-rw-r--r-- | rdflib/extras/external_graph_libs.py | 2 | ||||
-rw-r--r-- | rdflib/graph.py | 68 | ||||
-rw-r--r-- | rdflib/plugins/stores/sparqlconnector.py | 6 | ||||
-rw-r--r-- | rdflib/plugins/stores/sparqlstore.py | 8 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | test/test_batch_add.py | 89 | ||||
-rw-r--r-- | test/test_sparqlstore.py | 78 | ||||
-rw-r--r-- | tox.ini | 4 |
16 files changed, 302 insertions, 24 deletions
diff --git a/.travis.yml b/.travis.yml index f37f0750..2d2ada1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ # http://travis-ci.org/#!/RDFLib/rdflib -sudo: false language: python branches: only: @@ -10,16 +9,14 @@ git: depth: 3 python: - - 2.7 - - 3.4 - 3.5 - 3.6 + - 3.7 -matrix: +jobs: include: - - python: 3.7 + - python: 3.8 dist: xenial - sudo: true before_install: - pip install -U setuptools pip # seems travis comes with a too old setuptools for html5lib @@ -1,4 +1,4 @@ -![](docs/_static/logo-rdflib.png) +![](docs/_static/RDFlib.png) RDFLib ====== @@ -8,7 +8,7 @@ RDFLib [![PyPI](https://img.shields.io/pypi/v/rdflib.svg)](https://pypi.python.org/pypi/rdflib) [![PyPI](https://img.shields.io/pypi/pyversions/rdflib.svg)](https://pypi.python.org/pypi/rdflib) -RDFLib is a pure Python package work working with [RDF](http://www.w3.org/RDF/). RDFLib contains most things you need to work with RDF, including: +RDFLib is a pure Python package for working with [RDF](http://www.w3.org/RDF/). RDFLib contains most things you need to work with RDF, including: * parsers and serializers for RDF/XML, N3, NTriples, N-Quads, Turtle, TriX, Trig and JSON-LD (via a plugin). * a Graph interface which can be backed by any one of a number of Store implementations diff --git a/docs/_static/RDFlib-500.png b/docs/_static/RDFlib-500.png Binary files differnew file mode 100644 index 00000000..8312a071 --- /dev/null +++ b/docs/_static/RDFlib-500.png diff --git a/docs/_static/logo-rdflib.ico b/docs/_static/RDFlib.ico Binary files differindex b667f6b6..b667f6b6 100644 --- a/docs/_static/logo-rdflib.ico +++ b/docs/_static/RDFlib.ico diff --git a/docs/_static/RDFlib.png b/docs/_static/RDFlib.png Binary files differnew file mode 100644 index 00000000..435f07e4 --- /dev/null +++ b/docs/_static/RDFlib.png diff --git a/docs/_static/RDFlib.svg b/docs/_static/RDFlib.svg new file mode 100644 index 00000000..8e8d3fc1 --- /dev/null +++ b/docs/_static/RDFlib.svg @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#329545;} + .st1{fill:#FDD43B;} + .st2{fill:#144A99;} + .st3{fill:#FFFFFF;} +</style> +<g id="RDFlib"> + <path id="path1948" class="st0" d="M247.4,33.3c-17.7,0.1-34.5,1.6-49.4,4.2c-43.7,7.7-51.7,23.9-51.7,53.7v39.4h103.4v13.1H146.4 + h-38.8c-30,0-56.4,18.1-64.6,52.4c-9.5,39.4-9.9,63.9,0,105.1c7.3,30.6,24.9,52.4,54.9,52.4h35.5v-47.2c0-34.1,29.5-64.2,64.6-64.2 + h103.3c28.7,0,51.7-23.7,51.7-52.5V91.3c0-28-23.6-49.1-51.7-53.7C283.5,34.6,265.1,33.3,247.4,33.3z M191.5,65 + c10.7,0,19.4,8.9,19.4,19.8c0,10.9-8.7,19.6-19.4,19.6c-10.7,0-19.4-8.8-19.4-19.6C172.1,73.9,180.8,65,191.5,65z"/> + <path id="path1950" class="st1" d="M365.9,143.8v45.9c0,35.6-30.2,65.5-64.6,65.5H198c-28.3,0-51.7,24.2-51.7,52.5v98.4 + c0,28,24.4,44.5,51.7,52.5c32.7,9.6,64.1,11.4,103.3,0c26-7.5,51.7-22.7,51.7-52.5v-39.4H249.7v-13.1H353h51.7 + c30,0,41.2-21,51.7-52.4c10.8-32.4,10.3-63.5,0-105.1c-7.4-29.9-21.6-52.4-51.7-52.4H365.9z M307.8,393.1 + c10.7,0,19.4,8.8,19.4,19.6c0,10.9-8.7,19.8-19.4,19.8c-10.7,0-19.4-8.9-19.4-19.8C288.4,401.9,297.1,393.1,307.8,393.1z"/> + <path id="XMLID_41_" class="st2" d="M342.1,345.7c5.2-17,2.3-36.4-11.5-51c-3.9-4.2-8.6-7.7-13.6-10.4c-1.8-1-3.6-1.8-5.5-2.5 + c0,0-10.5-5.3-11.5-43.2c-0.3-12.7-0.5-28.2,5.9-39.7c0.9-1.6,1.9-3.2,3.3-4.4c7.3-6.2,15.4-10.5,20.7-18.8 + c5.4-8.5,8.4-18.4,8.4-28.4c0-43.9-53.7-68.6-87.2-40.3c-5.1,4.3-9.4,9.6-12.5,15.6c-5.6,10.6-7.2,22.2-5.5,33.2 + c0,0,2.5,12.8-29.8,32.9c-28.6,17.8-42.5,13-45.3,11.7c-8.2-5.2-17.8-8.2-28.2-8.2c-44,0-68.6,53.8-40.2,87.3 + c4.3,5.1,9.6,9.3,15.5,12.4c19.2,10.2,41.9,7.4,57.9-5.4c0,0,11.4-9,45.2,9.2c26.7,14.3,30.7,28.4,31.2,33.7 + c1.4,14.3,3.7,26.6,13.8,37.7C280.8,397,330.5,384,342.1,345.7z M215.1,280.6c-27.8-14.9-32-27.4-32.6-31.2 + c0.4-4.5,0.1-9.1-0.6-13.5l0.2,0.3c0,0-2.3-12.1,29.6-31.9c28.5-17.7,41.5-14.2,43.9-13.3c1.6,1.1,3.2,2,4.8,2.9 + c3.2,1.7,6.5,3,9.9,4c3.9,3.7,11.1,14.3,11.9,42.2c0.8,28.1-7.5,38.9-12,42.7c-4.6,2.1-9,4.9-12.9,8.3 + C253,292.7,240.8,294.4,215.1,280.6z"/> + <g id="XMLID_32_"> + <path id="XMLID_33_" class="st3" d="M253.8,117.4c-15.4,16.8-15.7,41.6-0.9,55.6c-7.3-7.1-7.2-21.7,0.2-35.8 + c1-1.3,3.7-4.2,7.7-2.9c0.4,0.1,0.7,0.2,0.8,0.2c0.9,0.2,1.8,0.3,2.8,0.3c6.1-0.3,10.9-5.5,10.6-11.6c-0.1-2.7-1.3-5.2-3-7 + c14.2-9.3,30.5-10.4,37.2-4.3l0.3,0C294.3,97.9,269.3,100.4,253.8,117.4z"/> + </g> + <g id="XMLID_29_"> + <path id="XMLID_31_" class="st3" d="M98.6,272.9c-0.1-0.1-0.3-0.3-0.4-0.4c0.1,0.1,0.2,0.2,0.3,0.3L98.6,272.9z"/> + <path id="XMLID_30_" class="st3" d="M99.1,216.9c-15.4,16.8-15.7,41.6-0.9,55.6c-7.3-7.1-7.2-21.7,0.2-35.8c1-1.3,3.7-4.2,7.7-2.9 + c0.4,0.1,0.7,0.2,0.8,0.2c0.9,0.2,1.8,0.3,2.8,0.3c6.1-0.3,10.9-5.5,10.6-11.6c-0.1-2.7-1.3-5.2-3-7c14.2-9.3,30.5-10.4,37.2-4.3 + l0.3,0C139.6,197.4,114.7,199.9,99.1,216.9z"/> + </g> + <g id="XMLID_26_"> + <path id="XMLID_28_" class="st3" d="M262.4,357.2c-0.1-0.1-0.3-0.3-0.4-0.4c0.1,0.1,0.2,0.2,0.3,0.3L262.4,357.2z"/> + <path id="XMLID_27_" class="st3" d="M262.9,301.2c-15.4,16.8-15.7,41.6-0.9,55.6c-7.3-7.1-7.2-21.7,0.2-35.8 + c1-1.3,3.7-4.2,7.7-2.9c0.4,0.1,0.7,0.2,0.8,0.2c0.9,0.2,1.8,0.3,2.8,0.3c6.1-0.3,10.9-5.5,10.6-11.6c-0.1-2.7-1.3-5.2-3-7 + c14.2-9.3,30.5-10.4,37.2-4.3l0.3,0C303.4,281.7,278.4,284.2,262.9,301.2z"/> + </g> +</g> +</svg> diff --git a/docs/conf.py b/docs/conf.py index 76f893fb..dbecc10b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -149,7 +149,7 @@ html_theme_path = [ # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None -html_logo = "_static/logo-rdflib.png" +html_logo = "_static/RDFlib.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 diff --git a/docs/sphinx-requirements.txt b/docs/sphinx-requirements.txt index 55809050..45583540 100644 --- a/docs/sphinx-requirements.txt +++ b/docs/sphinx-requirements.txt @@ -1,3 +1,3 @@ -sphinx==3.0.2 +sphinx==3.0.3 sphinxcontrib-apidoc git+https://github.com/gniezen/n3pygments.git diff --git a/rdflib/extras/external_graph_libs.py b/rdflib/extras/external_graph_libs.py index 8617b370..873805b4 100644 --- a/rdflib/extras/external_graph_libs.py +++ b/rdflib/extras/external_graph_libs.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python # encoding: utf-8 from __future__ import absolute_import from __future__ import division diff --git a/rdflib/graph.py b/rdflib/graph.py index 80dc02f8..a92f3bc5 100644 --- a/rdflib/graph.py +++ b/rdflib/graph.py @@ -271,6 +271,7 @@ __all__ = [ "Dataset", "UnSupportedAggregateOperation", "ReadOnlyGraphAggregate", + "BatchAddGraph", ] @@ -2013,6 +2014,73 @@ def _assertnode(*terms): return True +class BatchAddGraph(object): + ''' + Wrapper around graph that turns calls to :meth:`add` (and optionally, :meth:`addN`) + into calls to :meth:`~rdflib.graph.Graph.addN`. + + :Parameters: + + - `graph`: The graph to wrap + - `batch_size`: The maximum number of triples to buffer before passing to + `graph`'s `addN` + - `batch_addn`: If True, then even calls to `addN` will be batched according to + `batch_size` + + :ivar graph: The wrapped graph + :ivar count: The number of triples buffered since initaialization or the last call + to :meth:`reset` + :ivar batch: The current buffer of triples + + ''' + + def __init__(self, graph, batch_size=1000, batch_addn=False): + if not batch_size or batch_size < 2: + raise ValueError("batch_size must be a positive number") + self.graph = graph + self.__graph_tuple = (graph,) + self.__batch_size = batch_size + self.__batch_addn = batch_addn + self.reset() + + def reset(self): + ''' + Manually clear the buffered triples and reset the count to zero + ''' + self.batch = [] + self.count = 0 + + def add(self, triple_or_quad): + ''' + Add a triple to the buffer + + :param triple: The triple to add + ''' + if len(self.batch) >= self.__batch_size: + self.graph.addN(self.batch) + self.batch = [] + self.count += 1 + if len(triple_or_quad) == 3: + self.batch.append(triple_or_quad + self.__graph_tuple) + else: + self.batch.append(triple_or_quad) + + def addN(self, quads): + if self.__batch_addn: + for q in quads: + self.add(q) + else: + self.graph.addN(quads) + + def __enter__(self): + self.reset() + return self + + def __exit__(self, *exc): + if exc[0] is None: + self.graph.addN(self.batch) + + def test(): import doctest diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py index ee981419..abb69a55 100644 --- a/rdflib/plugins/stores/sparqlconnector.py +++ b/rdflib/plugins/stores/sparqlconnector.py @@ -87,6 +87,7 @@ class SPARQLConnector(object): if self.method == 'GET': args['params'].update(params) elif self.method == 'POST': + args['headers'].update({'Content-Type': 'application/sparql-query'}) args['data'] = params else: raise SPARQLConnectorException("Unknown method %s" % self.method) @@ -106,7 +107,10 @@ class SPARQLConnector(object): if default_graph: params["using-graph-uri"] = default_graph - headers = {'Accept': _response_mime_types[self.returnFormat]} + headers = { + 'Accept': _response_mime_types[self.returnFormat], + 'Content-Type': 'application/sparql-update', + } args = dict(self.kwargs) diff --git a/rdflib/plugins/stores/sparqlstore.py b/rdflib/plugins/stores/sparqlstore.py index 5f7446ce..4c93e213 100644 --- a/rdflib/plugins/stores/sparqlstore.py +++ b/rdflib/plugins/stores/sparqlstore.py @@ -493,8 +493,8 @@ class SPARQLUpdateStore(SPARQLStore): def open(self, configuration, create=False): """ sets the endpoint URLs for this SPARQLStore - :param configuration: either a tuple of (queryEndpoint, update_endpoint), - or a string with the query endpoint + :param configuration: either a tuple of (query_endpoint, update_endpoint), + or a string with the endpoint which is configured as query and update endpoint :param create: if True an exception is thrown. """ @@ -507,9 +507,7 @@ class SPARQLUpdateStore(SPARQLStore): self.update_endpoint = configuration[1] else: self.query_endpoint = configuration - - if not self.update_endpoint: - self.update_endpoint = self.endpoint + self.update_endpoint = configuration def _transaction(self): if self._edits is None: @@ -6,7 +6,7 @@ from setuptools import setup, find_packages kwargs = {} kwargs['install_requires'] = [ 'six', 'isodate', 'pyparsing'] -kwargs['tests_require'] = ['html5lib', 'networkx', 'nose', 'doctest-ignore-unicode'] +kwargs['tests_require'] = ['html5lib', 'networkx', 'nose', 'doctest-ignore-unicode', 'requests'] kwargs['test_suite'] = "nose.collector" kwargs['extras_require'] = { 'html': ['html5lib'], @@ -43,15 +43,14 @@ setup( url="https://github.com/RDFLib/rdflib", license="BSD-3-Clause", platforms=["any"], + python_requires='>=3.5', classifiers=[ "Programming Language :: Python", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "License :: OSI Approved :: BSD License", "Topic :: Software Development :: Libraries :: Python Modules", "Operating System :: OS Independent", diff --git a/test/test_batch_add.py b/test/test_batch_add.py new file mode 100644 index 00000000..1747100c --- /dev/null +++ b/test/test_batch_add.py @@ -0,0 +1,89 @@ +import unittest +from rdflib.graph import Graph, BatchAddGraph +from rdflib.term import URIRef + + +class TestBatchAddGraph(unittest.TestCase): + def test_batch_size_zero_denied(self): + with self.assertRaises(ValueError): + BatchAddGraph(Graph(), batch_size=0) + + def test_batch_size_none_denied(self): + with self.assertRaises(ValueError): + BatchAddGraph(Graph(), batch_size=None) + + def test_batch_size_one_denied(self): + with self.assertRaises(ValueError): + BatchAddGraph(Graph(), batch_size=1) + + def test_batch_size_negative_denied(self): + with self.assertRaises(ValueError): + BatchAddGraph(Graph(), batch_size=-12) + + def test_exit_submits_partial_batch(self): + trip = (URIRef('a'), URIRef('b'), URIRef('c')) + g = Graph() + with BatchAddGraph(g, batch_size=10) as cut: + cut.add(trip) + self.assertIn(trip, g) + + def test_add_more_than_batch_size(self): + trips = [(URIRef('a'), URIRef('b%d' % i), URIRef('c%d' % i)) + for i in range(12)] + g = Graph() + with BatchAddGraph(g, batch_size=10) as cut: + for trip in trips: + cut.add(trip) + self.assertEqual(12, len(g)) + + def test_add_quad_for_non_conjunctive_empty(self): + ''' + Graph drops quads that don't match our graph. Make sure we do the same + ''' + g = Graph(identifier='http://example.org/g') + badg = Graph(identifier='http://example.org/badness') + with BatchAddGraph(g) as cut: + cut.add((URIRef('a'), URIRef('b'), URIRef('c'), badg)) + self.assertEqual(0, len(g)) + + def test_add_quad_for_non_conjunctive_pass_on_context_matches(self): + g = Graph() + with BatchAddGraph(g) as cut: + cut.add((URIRef('a'), URIRef('b'), URIRef('c'), g)) + self.assertEqual(1, len(g)) + + def test_no_addN_on_exception(self): + ''' + Even if we've added triples so far, it may be that attempting to add the last + batch is the cause of our exception, so we don't want to attempt again + ''' + g = Graph() + trips = [(URIRef('a'), URIRef('b%d' % i), URIRef('c%d' % i)) + for i in range(12)] + + try: + with BatchAddGraph(g, batch_size=10) as cut: + for i, trip in enumerate(trips): + cut.add(trip) + if i == 11: + raise Exception('myexc') + except Exception as e: + if str(e) != 'myexc': + pass + self.assertEqual(10, len(g)) + + def test_addN_batching_addN(self): + class MockGraph(object): + def __init__(self): + self.counts = [] + + def addN(self, quads): + self.counts.append(sum(1 for _ in quads)) + + g = MockGraph() + quads = [(URIRef('a'), URIRef('b%d' % i), URIRef('c%d' % i), g) + for i in range(12)] + + with BatchAddGraph(g, batch_size=10, batch_addn=True) as cut: + cut.addN(quads) + self.assertEqual(g.counts, [10, 2]) diff --git a/test/test_sparqlstore.py b/test/test_sparqlstore.py index 26a69460..a0a93b57 100644 --- a/test/test_sparqlstore.py +++ b/test/test_sparqlstore.py @@ -4,7 +4,10 @@ import os import unittest from nose import SkipTest from requests import HTTPError - +from http.server import BaseHTTPRequestHandler, HTTPServer +import socket +from threading import Thread +import requests try: assert len(urlopen("http://dbpedia.org/sparql").read()) > 0 @@ -67,5 +70,78 @@ class SPARQLStoreDBPediaTestCase(unittest.TestCase): assert type(i[0]) == Literal, i[0].n3() +class SPARQLStoreUpdateTestCase(unittest.TestCase): + def setUp(self): + port = self.setup_mocked_endpoint() + self.graph = Graph(store="SPARQLUpdateStore", identifier=URIRef("urn:ex")) + self.graph.open(("http://localhost:{port}/query".format(port=port), + "http://localhost:{port}/update".format(port=port)), create=False) + ns = list(self.graph.namespaces()) + assert len(ns) > 0, ns + + def setup_mocked_endpoint(self): + # Configure mock server. + s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) + s.bind(('localhost', 0)) + address, port = s.getsockname() + s.close() + mock_server = HTTPServer(('localhost', port), SPARQL11ProtocolStoreMock) + + # Start running mock server in a separate thread. + # Daemon threads automatically shut down when the main process exits. + mock_server_thread = Thread(target=mock_server.serve_forever) + mock_server_thread.setDaemon(True) + mock_server_thread.start() + print("Started mocked sparql endpoint on http://localhost:{port}/".format(port=port)) + return port + + def tearDown(self): + self.graph.close() + + def test_Query(self): + query = "insert data {<urn:s> <urn:p> <urn:o>}" + res = self.graph.update(query) + print(res) + + +class SPARQL11ProtocolStoreMock(BaseHTTPRequestHandler): + def do_POST(self): + """ + If the body should be analysed as well, just use: + ``` + body = self.rfile.read(int(self.headers['Content-Length'])).decode() + print(body) + ``` + """ + contenttype = self.headers.get("Content-Type") + if self.path == "/query": + if self.headers.get("Content-Type") == "application/sparql-query": + pass + elif self.headers.get("Content-Type") == "application/x-www-form-urlencoded": + pass + else: + self.send_response(requests.codes.not_acceptable) + self.end_headers() + elif self.path == "/update": + if self.headers.get("Content-Type") == "application/sparql-update": + pass + elif self.headers.get("Content-Type") == "application/x-www-form-urlencoded": + pass + else: + self.send_response(requests.codes.not_acceptable) + self.end_headers() + else: + self.send_response(requests.codes.not_found) + self.end_headers() + self.send_response(requests.codes.ok) + self.end_headers() + return + + def do_GET(self): + # Process an HTTP GET request and return a response with an HTTP 200 status. + self.send_response(requests.codes.ok) + self.end_headers() + return + if __name__ == '__main__': unittest.main() @@ -1,6 +1,6 @@ [tox] envlist = - py27,py34,py35,py36 + py35,py36,py37,py38 [testenv] setenv = @@ -20,7 +20,7 @@ deps = [testenv:cover] basepython = - python2.7 + python3.7 commands = {envpython} run_tests.py --where=./ \ --with-coverage --cover-html --cover-html-dir=./coverage \ |