mustrd 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
src/mustrdAnzo.py ADDED
@@ -0,0 +1,236 @@
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2023 Semantic Partners Ltd
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+ import requests
26
+ from pyanzo import AnzoClient
27
+ from rdflib import Graph, ConjunctiveGraph, Literal, URIRef
28
+ from requests import ConnectTimeout, Response, HTTPError, RequestException, ConnectionError
29
+ from bs4 import BeautifulSoup
30
+ import logging
31
+ from namespace import MUST
32
+
33
+
34
+ # https://github.com/Semantic-partners/mustrd/issues/73
35
+ def manage_anzo_response(response: Response) -> str:
36
+ content_string = response.content.decode("utf-8")
37
+ if response.status_code == 200:
38
+ return content_string
39
+ elif response.status_code == 403:
40
+ html = BeautifulSoup(content_string, 'html.parser')
41
+ title_tag = html.title.string
42
+ raise HTTPError(f"Anzo authentication error, status code: {response.status_code}, content: {title_tag}")
43
+ else:
44
+ raise RequestException(f"Anzo error, status code: {response.status_code}, content: {content_string}")
45
+
46
+
47
+ def query_with_bindings(bindings: dict, when: str) -> str:
48
+ values = ""
49
+ for key, value in bindings.items():
50
+ values += f"VALUES ?{key} {{{value.n3()}}} "
51
+ split_query = when.lower().split("where {", 1)
52
+ return f"{split_query[0].strip()} WHERE {{ {values} {split_query[1].strip()}"
53
+
54
+ def execute_select (triple_store: dict, when: str, bindings: dict = None) -> str:
55
+ try:
56
+ if bindings:
57
+ when = query_with_bindings(bindings, when)
58
+ when = when.replace("${fromSources}", f"FROM <{triple_store['input_graph']}>\nFROM <{triple_store['output_graph']}>").replace(
59
+ "${targetGraph}", f"<{triple_store['output_graph']}>")
60
+ data = {'datasourceURI': triple_store['gqe_uri'], 'query': when,
61
+ 'skipCache': 'true'}
62
+ url = f"https://{triple_store['url']}:{triple_store['port']}/sparql?format=application/sparql-results+json"
63
+ return manage_anzo_response(requests.post(url=url,
64
+ auth=(triple_store['username'], triple_store['password']),
65
+ data=data,
66
+ verify=False))
67
+ except (ConnectionError, TimeoutError, HTTPError, ConnectTimeout):
68
+ raise
69
+
70
+ def execute_update(triple_store: dict, when: str, bindings: dict = None) -> Graph:
71
+ logging.debug(f"updating in anzo! {triple_store=} {when=}")
72
+ input_graph = triple_store['input_graph']
73
+ output_graph = triple_store['output_graph']
74
+
75
+ substituted_query = when.replace("${usingSources}", f"USING <{triple_store['input_graph']}> \nUSING <{triple_store['output_graph']}>").replace(
76
+ "${targetGraph}", f"<{output_graph}>")
77
+
78
+ data = {'datasourceURI': triple_store['gqe_uri'], 'update': substituted_query,
79
+ 'default-graph-uri': input_graph, 'skipCache': 'true'}
80
+ url = f"https://{triple_store['url']}:{triple_store['port']}/sparql?format=ttl"
81
+ response = manage_anzo_response(requests.post(url=url,
82
+ auth=(triple_store['username'],
83
+ triple_store['password']),
84
+ data=data,
85
+ verify=False))
86
+ logging.debug(f'response {response}')
87
+ check_data = {'datasourceURI': triple_store['gqe_uri'], 'query': "construct {?s ?p ?o} { ?s ?p ?o }",
88
+ 'default-graph-uri': output_graph, 'skipCache': 'true'}
89
+ everything_response = manage_anzo_response(requests.post(url=url,
90
+ auth=(triple_store['username'],
91
+ triple_store['password']),
92
+ data=check_data,
93
+ verify=False))
94
+ # todo deal with error responses
95
+ new_graph = Graph().parse(data=everything_response)
96
+ logging.debug(f"new_graph={new_graph.serialize(format='ttl')}")
97
+ return new_graph
98
+
99
+
100
+ def execute_construct(triple_store: dict, when: str, bindings: dict = None) -> Graph:
101
+ try:
102
+ if bindings:
103
+ when = query_with_bindings(bindings, when)
104
+ data = {'datasourceURI': triple_store['gqe_uri'], 'query': when,
105
+ 'default-graph-uri': triple_store['input_graph'], 'skipCache': 'true'}
106
+ url = f"https://{triple_store['url']}:{triple_store['port']}/sparql?format=ttl"
107
+ response = requests.post(url=url,
108
+ auth=(triple_store['username'],
109
+ triple_store['password']),
110
+ data=data,
111
+ verify=False)
112
+ logging.debug(f'response {response}')
113
+ return Graph().parse(data=manage_anzo_response(response))
114
+ except (ConnectionError, TimeoutError, HTTPError, ConnectTimeout) as e:
115
+ logging.error(f'response {e}')
116
+ raise
117
+
118
+
119
+ # Get Given or then from the content of a graphmart
120
+ def get_spec_component_from_graphmart(triple_store: dict, graphmart: URIRef, layer: URIRef = None) -> ConjunctiveGraph:
121
+ try:
122
+ anzo_client = AnzoClient(triple_store['url'], triple_store['port'],
123
+ username=triple_store['username'],
124
+ password=triple_store['password'])
125
+ return anzo_client.query_graphmart(graphmart=graphmart,
126
+ data_layers=layer,
127
+ query_string="CONSTRUCT {?s ?p ?o} WHERE {?s ?p ?o}",
128
+ skip_cache=True).as_quad_store().as_rdflib_graph()
129
+ except RuntimeError as e:
130
+ raise ConnectionError(f"Anzo connection error, {e}")
131
+
132
+
133
+ def get_query_from_querybuilder(triple_store: dict, folder_name: Literal, query_name: Literal) -> str:
134
+ query = f"""SELECT ?query WHERE {{
135
+ graph ?queryFolder {{
136
+ ?bookmark a <http://www.cambridgesemantics.com/ontologies/QueryPlayground#QueryBookmark>;
137
+ <http://openanzo.org/ontologies/2008/07/System#query> ?query;
138
+ <http://purl.org/dc/elements/1.1/title> "{query_name}"
139
+ }}
140
+ ?queryFolder a <http://www.cambridgesemantics.com/ontologies/QueryPlayground#QueryFolder>;
141
+ <http://purl.org/dc/elements/1.1/title> "{folder_name}"
142
+ }}"""
143
+ anzo_client = AnzoClient(triple_store['url'], triple_store['port'],
144
+ username=triple_store['username'],
145
+ password=triple_store['password'])
146
+
147
+ result = anzo_client.query_journal(query_string=query).as_table_results().as_record_dictionaries()
148
+ if len(result) == 0:
149
+ raise FileNotFoundError(f"Query {query_name} not found in folder {folder_name}")
150
+ return result[0].get("query")
151
+
152
+
153
+ # https://github.com/Semantic-partners/mustrd/issues/102
154
+ def get_query_from_step(triple_store: dict, query_step_uri: URIRef) -> str:
155
+ query = f"""SELECT ?stepUri ?query WHERE {{
156
+ BIND(<{query_step_uri}> as ?stepUri)
157
+ ?stepUri a <http://cambridgesemantics.com/ontologies/Graphmarts#Step>;
158
+ <http://cambridgesemantics.com/ontologies/Graphmarts#transformQuery> ?query
159
+ }}
160
+ # """
161
+ anzo_client = AnzoClient(triple_store['url'], triple_store['port'],
162
+ username=triple_store['username'],
163
+ password=triple_store['password'])
164
+ record_dictionaries = anzo_client.query_journal(query_string=query).as_table_results().as_record_dictionaries()
165
+
166
+ return record_dictionaries[0].get(
167
+ "query")
168
+
169
+ def get_queries_from_templated_step(triple_store: dict, query_step_uri: URIRef) -> dict:
170
+
171
+ query = f"""SELECT ?stepUri ?param_query ?query_template WHERE {{
172
+ BIND(<{query_step_uri}> as ?stepUri)
173
+ ?stepUri a <http://cambridgesemantics.com/ontologies/Graphmarts#Step> ;
174
+ <http://cambridgesemantics.com/ontologies/Graphmarts#parametersTemplate> ?param_query ;
175
+ <http://cambridgesemantics.com/ontologies/Graphmarts#template> ?query_template .
176
+ }}
177
+ """
178
+ anzo_client = AnzoClient(triple_store['url'], triple_store['port'],
179
+ username=triple_store['username'],
180
+ password=triple_store['password'])
181
+ record_dictionaries = anzo_client.query_journal(query_string=query).as_table_results().as_record_dictionaries()
182
+ return record_dictionaries[0]
183
+
184
+
185
+ def get_queries_for_layer(triple_store: dict, graphmart_layer_uri: URIRef):
186
+ query = f"""PREFIX graphmarts: <http://cambridgesemantics.com/ontologies/Graphmarts#>
187
+ PREFIX anzo: <http://openanzo.org/ontologies/2008/07/Anzo#>
188
+ SELECT ?query ?param_query ?query_template
189
+ {{ <{graphmart_layer_uri}> graphmarts:step ?step .
190
+ ?step anzo:index ?index ;
191
+ anzo:orderedValue ?query_step .
192
+ ?query_step graphmarts:enabled true ;
193
+ OPTIONAL {{ ?query_step
194
+ graphmarts:parametersTemplate ?param_query ;
195
+ graphmarts:template ?query_template ;
196
+ . }}
197
+ OPTIONAL {{ ?query_step
198
+ graphmarts:transformQuery ?query ;
199
+ . }}
200
+ }}
201
+ ORDER BY ?index"""
202
+ anzo_client = AnzoClient(triple_store['url'], triple_store['port'],
203
+ username=triple_store['username'],
204
+ password=triple_store['password'])
205
+ return anzo_client.query_journal(query_string=query).as_table_results().as_record_dictionaries()
206
+
207
+
208
+ def upload_given(triple_store: dict, given: Graph):
209
+ logging.debug(f"upload_given {triple_store} {given}")
210
+
211
+ try:
212
+ input_graph = triple_store['input_graph']
213
+ output_graph = triple_store['output_graph']
214
+ clear_graph(triple_store, input_graph)
215
+ clear_graph(triple_store, output_graph)
216
+ serialized_given = given.serialize(format="nt")
217
+ insert_query = f"INSERT DATA {{graph <{triple_store['input_graph']}>{{{serialized_given}}}}}"
218
+ data = {'datasourceURI': triple_store['gqe_uri'], 'update': insert_query}
219
+ response = requests.post(url=f"https://{triple_store['url']}:{triple_store['port']}/sparql",
220
+ auth=(triple_store['username'], triple_store['password']), data=data, verify=False)
221
+ manage_anzo_response(response)
222
+ except (ConnectionError, TimeoutError, HTTPError, ConnectTimeout):
223
+ raise
224
+
225
+
226
+ def clear_graph(triple_store: dict, graph_uri: str):
227
+ try:
228
+ clear_query = f"CLEAR GRAPH <{graph_uri}>"
229
+ data = {'datasourceURI': triple_store['gqe_uri'], 'update': clear_query}
230
+ url = f"https://{triple_store['url']}:{triple_store['port']}/sparql"
231
+ response = requests.post(url=url,
232
+ auth=(triple_store['username'], triple_store['password']), data=data, verify=False)
233
+ manage_anzo_response(response)
234
+ except (ConnectionError, TimeoutError, HTTPError, ConnectTimeout):
235
+ raise
236
+
src/mustrdGraphDb.py ADDED
@@ -0,0 +1,125 @@
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2023 Semantic Partners Ltd
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+ import urllib.parse
26
+ import requests
27
+ from rdflib import Graph, Literal
28
+ from requests import ConnectionError, HTTPError, RequestException, Response
29
+
30
+
31
+ # https://github.com/Semantic-partners/mustrd/issues/72
32
+ def manage_graphdb_response(response: Response) -> str:
33
+ content_string = response.content.decode("utf-8")
34
+ if response.status_code == 200:
35
+ return content_string
36
+ elif response.status_code == 204:
37
+ pass
38
+ elif response.status_code == 401:
39
+ raise HTTPError(f"GraphDB authentication error, status code: {response.status_code}, content: {content_string}")
40
+ elif response.status_code == 406:
41
+ raise HTTPError(f"GraphDB error, status code: {response.status_code}, content: {content_string}")
42
+ else:
43
+ raise RequestException(f"GraphDb error, status code: {response.status_code}, content: {content_string}")
44
+
45
+
46
+ def upload_given(triple_store: dict, given: Graph):
47
+ if given:
48
+ try:
49
+ graph = "default"
50
+ if triple_store['input_graph']:
51
+ graph = urllib.parse.urlencode({'graph': triple_store['input_graph']})
52
+ url = f"{triple_store['url']}:{triple_store['port']}/repositories/{triple_store['repository']}" \
53
+ f"/rdf-graphs/service?{graph}"
54
+ # graph store PUT drop silently the graph or default and upload the payload
55
+ # https://www.w3.org/TR/sparql11-http-rdf-update/#http-put
56
+ manage_graphdb_response(requests.put(url=url,
57
+ auth=(triple_store['username'], triple_store['password']),
58
+ data=given.serialize(format="ttl"),
59
+ headers={'Content-Type': 'text/turtle'}))
60
+ except ConnectionError:
61
+ raise
62
+
63
+
64
+ def parse_bindings(bindings: dict = None) -> dict:
65
+ return None if not bindings else {f"${k}": str(v.n3()) for k, v in bindings.items()}
66
+
67
+
68
+ def execute_select(triple_store: dict, when: str, bindings: dict = None) -> str:
69
+ return post_query(triple_store, when, "application/sparql-results+json", parse_bindings(bindings))
70
+
71
+
72
+ def execute_construct(triple_store: dict, when: str, bindings: dict = None) -> Graph:
73
+ return Graph().parse(data=post_query(triple_store, when, "text/turtle", parse_bindings(bindings)))
74
+
75
+
76
+ def execute_update(triple_store: dict, when: str, bindings: dict = None) -> Graph:
77
+ post_update_query(triple_store, when, parse_bindings(bindings))
78
+ return Graph().parse(data=post_query(triple_store, "CONSTRUCT {?s ?p ?o} where { ?s ?p ?o }", 'text/turtle'))
79
+
80
+
81
+ def post_update_query(triple_store: dict, query: str, params: dict = None) -> str:
82
+ params = add_graph_to_params(params, triple_store["input_graph"])
83
+ try:
84
+ return manage_graphdb_response(requests.post(
85
+ url=f"{triple_store['url']}:{triple_store['port']}/repositories/{triple_store['repository']}/statements",
86
+ data=query,
87
+ params=params,
88
+ auth=(triple_store['username'], triple_store['password']),
89
+ headers={'Content-Type': 'application/sparql-update'}))
90
+ except (ConnectionError, OSError):
91
+ raise
92
+
93
+
94
+ def post_query(triple_store: dict, query: str, accept: str, params: dict = None) -> str:
95
+ headers = {
96
+ 'Content-Type': 'application/sparql-query',
97
+ 'Accept': accept
98
+ }
99
+ params = add_graph_to_params(params, triple_store["input_graph"])
100
+ try:
101
+ return manage_graphdb_response(
102
+ requests.post(url=f"{triple_store['url']}:{triple_store['port']}/repositories/{triple_store['repository']}",
103
+ data=query,
104
+ params=params,
105
+ auth=(triple_store['username'], triple_store['password']),
106
+ headers=headers))
107
+ except (ConnectionError, OSError):
108
+ raise
109
+
110
+
111
+ def add_graph_to_params(params: dict, graph: Literal) -> dict:
112
+ graph = graph or "http://rdf4j.org/schema/rdf4j#nil"
113
+ if params:
114
+ params['default-graph-uri'] = graph
115
+ params['using-graph-uri'] = graph
116
+ params['remove-graph-uri'] = graph
117
+ params['insert-graph-uri'] = graph
118
+ else:
119
+ params = {
120
+ 'default-graph-uri': graph,
121
+ 'using-graph-uri': graph,
122
+ 'remove-graph-uri': graph,
123
+ 'insert-graph-uri': graph
124
+ }
125
+ return params
src/mustrdRdfLib.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2023 Semantic Partners Ltd
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+ from pyparsing import ParseException
26
+ from rdflib import Graph
27
+ from requests import RequestException
28
+
29
+
30
+ def execute_select(triple_store: dict, given: Graph, when: str, bindings: dict = None) -> str:
31
+ try:
32
+ return given.query(when, initBindings=bindings).serialize(format="json").decode("utf-8")
33
+ except ParseException:
34
+ raise
35
+ except Exception as e:
36
+ raise RequestException(e)
37
+
38
+
39
+ def execute_construct(triple_store: dict, given: Graph, when: str, bindings: dict = None) -> Graph:
40
+ try:
41
+ return given.query(when, initBindings=bindings).graph
42
+ except ParseException:
43
+ raise
44
+ except Exception as e:
45
+ raise RequestException(e)
46
+
47
+
48
+ def execute_update(triple_store: dict, given: Graph, when: str, bindings: dict = None) -> Graph:
49
+ try:
50
+ result = given
51
+ result.update(when, initBindings=bindings)
52
+ return result
53
+ except ParseException:
54
+ raise
55
+ except Exception as e:
56
+ raise RequestException(e)
@@ -0,0 +1,175 @@
1
+ from dataclasses import dataclass
2
+ from TestResult import ResultList, TestResult, get_result_list
3
+ import pytest
4
+ import os
5
+ from pathlib import Path
6
+ from rdflib.namespace import Namespace
7
+ from rdflib import Graph
8
+
9
+ from utils import get_project_root
10
+ from mustrd import get_triple_store_graph, get_triple_stores, SpecSkipped, validate_specs, get_specs, SpecPassed, run_spec
11
+ from namespace import MUST
12
+ from pytest import Session
13
+ from typing import Dict
14
+
15
+ spnamespace = Namespace("https://semanticpartners.com/data/test/")
16
+
17
+ project_root = get_project_root()
18
+
19
+
20
+ @dataclass
21
+ class TestConfig:
22
+ test_function: str
23
+ spec_path: str
24
+ data_path: str
25
+ triplestore_spec_path: str
26
+ filter_on_tripleStore: str
27
+
28
+ def __init__(self, test_function: str, spec_path: str, data_path: str, triplestore_spec_path: str,
29
+ filter_on_tripleStore: str = None):
30
+ self.test_function = test_function
31
+ self.spec_path = spec_path
32
+ self.data_path = data_path
33
+ self.triplestore_spec_path = triplestore_spec_path
34
+ self.filter_on_tripleStore = filter_on_tripleStore
35
+
36
+
37
+ class MustrdTestPlugin:
38
+ md_path: str
39
+ test_configs: Dict[str, TestConfig]
40
+
41
+ def __init__(self, md_path, test_configs):
42
+ self.md_path = md_path
43
+ self.test_configs = test_configs
44
+
45
+ # Hook called at collection time: reads the configuration of the tests, and generate pytests from it
46
+ def pytest_generate_tests(self, metafunc):
47
+
48
+ if len(metafunc.fixturenames) > 0:
49
+ if metafunc.function.__name__ in self.test_configs:
50
+ one_test_config = self.test_configs[metafunc.function.__name__]
51
+
52
+ triple_stores = self.get_triple_stores_from_file(one_test_config)
53
+
54
+ unit_tests = []
55
+ if one_test_config.filter_on_tripleStore and not triple_stores:
56
+ unit_tests = list(map(lambda triple_store:
57
+ SpecSkipped(MUST.TestSpec, triple_store, "No triplestore found"),
58
+ one_test_config.filter_on_tripleStore))
59
+ else:
60
+ unit_tests = self.generate_tests_for_config({"spec_path": project_root / one_test_config.spec_path,
61
+ "data_path": project_root / one_test_config.data_path},
62
+ triple_stores)
63
+
64
+ # Create the test in itself
65
+ if unit_tests:
66
+ metafunc.parametrize(metafunc.fixturenames[0], unit_tests, ids=self.get_test_name)
67
+ else:
68
+ metafunc.parametrize(metafunc.fixturenames[0],
69
+ [SpecSkipped(MUST.TestSpec, None, "No triplestore found")],
70
+ ids=lambda x: "No configuration found for this test")
71
+
72
+ # Generate test for each triple store available
73
+ def generate_tests_for_config(self, config, triple_stores):
74
+
75
+ shacl_graph = Graph().parse(Path(os.path.join(project_root, "model/mustrdShapes.ttl")))
76
+ ont_graph = Graph().parse(Path(os.path.join(project_root, "model/ontology.ttl")))
77
+ valid_spec_uris, spec_graph, invalid_spec_results = validate_specs(config, triple_stores,
78
+ shacl_graph, ont_graph)
79
+
80
+ specs, skipped_spec_results = \
81
+ get_specs(valid_spec_uris, spec_graph, triple_stores, config)
82
+
83
+ # Return normal specs + skipped results
84
+ return specs + skipped_spec_results + invalid_spec_results
85
+
86
+ # Function called to generate the name of the test
87
+ def get_test_name(self, spec):
88
+ # FIXME: SpecSkipped should have the same structure?
89
+ if isinstance(spec, SpecSkipped):
90
+ triple_store = spec.triple_store
91
+ else:
92
+ triple_store = spec.triple_store['type']
93
+ triple_store_name = triple_store.replace("https://mustrd.com/model/", "")
94
+ test_name = spec.spec_uri.replace(spnamespace, "").replace("_", " ")
95
+ return triple_store_name + ": " + test_name
96
+
97
+ # Get triple store configuration or default
98
+ def get_triple_stores_from_file(self, test_config):
99
+ if test_config.triplestore_spec_path:
100
+ try:
101
+ triple_stores = get_triple_stores(get_triple_store_graph(project_root / test_config.triplestore_spec_path))
102
+ except Exception:
103
+ print(f"""No triple store configuration found at {project_root / test_config.triplestore_spec_path}.
104
+ Fall back: only embedded rdflib will be executed""")
105
+ triple_stores = [{'type': MUST.RdfLib}]
106
+ else:
107
+ print("No triple store configuration required: using embedded rdflib")
108
+ triple_stores = [{'type': MUST.RdfLib}]
109
+
110
+ if test_config.filter_on_tripleStore:
111
+ triple_stores = list(filter(lambda triple_store: (triple_store["type"] in test_config.filter_on_tripleStore),
112
+ triple_stores))
113
+ return triple_stores
114
+
115
+ # Hook function. Initialize the list of result in session
116
+ def pytest_sessionstart(self, session):
117
+ session.results = dict()
118
+
119
+ # Hook function called each time a report is generated by a test
120
+ # The report is added to a list in the session
121
+ # so it can be used later in pytest_sessionfinish to generate the global report md file
122
+ @pytest.hookimpl(tryfirst=True, hookwrapper=True)
123
+ def pytest_runtest_makereport(self, item, call):
124
+ outcome = yield
125
+ result = outcome.get_result()
126
+
127
+ if result.when == 'call':
128
+ # Add the result of the test to the session
129
+ item.session.results[item] = result
130
+
131
+ # Take all the test results in session, parse them, split them in mustrd and standard pytest and generate md file
132
+ def pytest_sessionfinish(self, session: Session, exitstatus):
133
+ # if md path has not been defined in argument, then do not generate md file
134
+ if not self.md_path:
135
+ return
136
+
137
+ test_results = []
138
+ for test_conf, result in session.results.items():
139
+ # Case auto generated tests
140
+ if test_conf.originalname != test_conf.name:
141
+ module_name = test_conf.parent.name
142
+ class_name = test_conf.originalname
143
+ test_name = test_conf.name.replace(class_name, "").replace("[", "").replace("]", "")
144
+ is_mustrd = True
145
+ # Case normal unit tests
146
+ else:
147
+ module_name = test_conf.parent.parent.name
148
+ class_name = test_conf.parent.name
149
+ test_name = test_conf.originalname
150
+ is_mustrd = False
151
+
152
+ test_results.append(TestResult(test_name, class_name, module_name, result.outcome, is_mustrd))
153
+
154
+ result_list = ResultList(None, get_result_list(test_results,
155
+ lambda result: result.type,
156
+ lambda result: result.module_name,
157
+ lambda result: result.class_name),
158
+ False)
159
+
160
+ md = result_list.render()
161
+ with open(self.md_path, 'w') as file:
162
+ file.write(md)
163
+
164
+
165
+ # Function called in the test to actually run it
166
+ def run_test_spec(test_spec):
167
+ if isinstance(test_spec, SpecSkipped):
168
+ pytest.skip(f"Invalid configuration, error : {test_spec.message}")
169
+ result = run_spec(test_spec)
170
+
171
+ result_type = type(result)
172
+ if result_type == SpecSkipped:
173
+ # FIXME: Better exception management
174
+ pytest.skip("Unsupported configuration")
175
+ return result_type == SpecPassed