naas-abi-core 1.4.1__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.
- assets/favicon.ico +0 -0
- assets/logo.png +0 -0
- naas_abi_core/__init__.py +1 -0
- naas_abi_core/apps/api/api.py +245 -0
- naas_abi_core/apps/api/api_test.py +281 -0
- naas_abi_core/apps/api/openapi_doc.py +144 -0
- naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
- naas_abi_core/apps/mcp/mcp_server.py +243 -0
- naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
- naas_abi_core/apps/terminal_agent/main.py +555 -0
- naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
- naas_abi_core/engine/Engine.py +87 -0
- naas_abi_core/engine/EngineProxy.py +109 -0
- naas_abi_core/engine/Engine_test.py +6 -0
- naas_abi_core/engine/IEngine.py +91 -0
- naas_abi_core/engine/conftest.py +45 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
- naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
- naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
- naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
- naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
- naas_abi_core/integration/__init__.py +7 -0
- naas_abi_core/integration/integration.py +28 -0
- naas_abi_core/models/Model.py +198 -0
- naas_abi_core/models/OpenRouter.py +18 -0
- naas_abi_core/models/OpenRouter_test.py +36 -0
- naas_abi_core/module/Module.py +252 -0
- naas_abi_core/module/ModuleAgentLoader.py +50 -0
- naas_abi_core/module/ModuleUtils.py +20 -0
- naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
- naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
- naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
- naas_abi_core/pipeline/__init__.py +6 -0
- naas_abi_core/pipeline/pipeline.py +70 -0
- naas_abi_core/services/__init__.py +0 -0
- naas_abi_core/services/agent/Agent.py +1619 -0
- naas_abi_core/services/agent/AgentMemory_test.py +28 -0
- naas_abi_core/services/agent/Agent_test.py +214 -0
- naas_abi_core/services/agent/IntentAgent.py +1179 -0
- naas_abi_core/services/agent/IntentAgent_test.py +139 -0
- naas_abi_core/services/agent/beta/Embeddings.py +181 -0
- naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
- naas_abi_core/services/agent/beta/LocalModel.py +88 -0
- naas_abi_core/services/agent/beta/VectorStore.py +89 -0
- naas_abi_core/services/agent/test_agent_memory.py +278 -0
- naas_abi_core/services/agent/test_postgres_integration.py +145 -0
- naas_abi_core/services/cache/CacheFactory.py +31 -0
- naas_abi_core/services/cache/CachePort.py +63 -0
- naas_abi_core/services/cache/CacheService.py +246 -0
- naas_abi_core/services/cache/CacheService_test.py +85 -0
- naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
- naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
- naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
- naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
- naas_abi_core/services/ontology/OntologyPorts.py +36 -0
- naas_abi_core/services/ontology/OntologyService.py +17 -0
- naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
- naas_abi_core/services/secret/Secret.py +138 -0
- naas_abi_core/services/secret/SecretPorts.py +45 -0
- naas_abi_core/services/secret/Secret_test.py +65 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
- naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
- naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
- naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
- naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
- naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
- naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
- naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
- naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
- naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
- naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
- naas_abi_core/services/vector_store/__init__.py +13 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
- naas_abi_core/tests/test_services_imports.py +69 -0
- naas_abi_core/utils/Expose.py +55 -0
- naas_abi_core/utils/Graph.py +182 -0
- naas_abi_core/utils/JSON.py +49 -0
- naas_abi_core/utils/LazyLoader.py +44 -0
- naas_abi_core/utils/Logger.py +12 -0
- naas_abi_core/utils/OntologyReasoner.py +141 -0
- naas_abi_core/utils/OntologyYaml.py +681 -0
- naas_abi_core/utils/SPARQL.py +256 -0
- naas_abi_core/utils/Storage.py +33 -0
- naas_abi_core/utils/StorageUtils.py +398 -0
- naas_abi_core/utils/String.py +52 -0
- naas_abi_core/utils/Workers.py +114 -0
- naas_abi_core/utils/__init__.py +0 -0
- naas_abi_core/utils/onto2py/README.md +0 -0
- naas_abi_core/utils/onto2py/__init__.py +10 -0
- naas_abi_core/utils/onto2py/__main__.py +29 -0
- naas_abi_core/utils/onto2py/onto2py.py +611 -0
- naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
- naas_abi_core/workflow/__init__.py +5 -0
- naas_abi_core/workflow/workflow.py +48 -0
- naas_abi_core-1.4.1.dist-info/METADATA +630 -0
- naas_abi_core-1.4.1.dist-info/RECORD +124 -0
- naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
- naas_abi_core-1.4.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1474 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import requests
|
|
5
|
+
from naas_abi_core.services.triple_store.adaptors.secondary.Oxigraph import Oxigraph
|
|
6
|
+
from rdflib import RDF, BNode, Graph, Literal, URIRef
|
|
7
|
+
|
|
8
|
+
# class TestOxigraph:
|
|
9
|
+
# """Test suite for the Oxigraph triple store adapter."""
|
|
10
|
+
|
|
11
|
+
# @pytest.fixture
|
|
12
|
+
# def mock_oxigraph_url(self):
|
|
13
|
+
# """Mock Oxigraph URL for testing."""
|
|
14
|
+
# return "http://localhost:7878"
|
|
15
|
+
|
|
16
|
+
# @pytest.fixture
|
|
17
|
+
# def sample_graph(self):
|
|
18
|
+
# """Create a sample RDF graph for testing."""
|
|
19
|
+
# g = Graph()
|
|
20
|
+
# g.bind("ex", "http://example.org/")
|
|
21
|
+
|
|
22
|
+
# subject = URIRef("http://example.org/alice")
|
|
23
|
+
# g.add((subject, RDF.type, URIRef("http://example.org/Person")))
|
|
24
|
+
# g.add((subject, URIRef("http://example.org/name"), Literal("Alice")))
|
|
25
|
+
# g.add((subject, URIRef("http://example.org/age"), Literal(30)))
|
|
26
|
+
|
|
27
|
+
# return g
|
|
28
|
+
|
|
29
|
+
# @pytest.fixture
|
|
30
|
+
# def mock_successful_response(self):
|
|
31
|
+
# """Mock successful HTTP response for SELECT queries."""
|
|
32
|
+
# response = Mock()
|
|
33
|
+
# response.status_code = 200
|
|
34
|
+
# response.headers = {"Content-Type": "application/sparql-results+json"}
|
|
35
|
+
# response_text = """{
|
|
36
|
+
# "head": {"vars": ["s", "p", "o"]},
|
|
37
|
+
# "results": {
|
|
38
|
+
# "bindings": [{
|
|
39
|
+
# "s": {"type": "uri", "value": "http://example.org/alice"},
|
|
40
|
+
# "p": {"type": "uri", "value": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"},
|
|
41
|
+
# "o": {"type": "uri", "value": "http://example.org/Person"}
|
|
42
|
+
# }]
|
|
43
|
+
# }
|
|
44
|
+
# }"""
|
|
45
|
+
# response.text = response_text
|
|
46
|
+
# response.content = response_text.encode('utf-8')
|
|
47
|
+
# response.raise_for_status = Mock()
|
|
48
|
+
# return response
|
|
49
|
+
|
|
50
|
+
# @pytest.fixture
|
|
51
|
+
# def mock_turtle_response(self):
|
|
52
|
+
# """Mock Turtle response for CONSTRUCT queries."""
|
|
53
|
+
# response = Mock()
|
|
54
|
+
# response.status_code = 200
|
|
55
|
+
# response.headers = {"Content-Type": "text/turtle"}
|
|
56
|
+
# response.text = """@prefix ex: <http://example.org/> .
|
|
57
|
+
# ex:alice a ex:Person ."""
|
|
58
|
+
# response.raise_for_status = Mock()
|
|
59
|
+
# return response
|
|
60
|
+
|
|
61
|
+
# def test_init_default_parameters(self):
|
|
62
|
+
# """Test Oxigraph initialization with default parameters."""
|
|
63
|
+
# with patch('requests.get') as mock_get:
|
|
64
|
+
# mock_get.return_value.status_code = 200
|
|
65
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
66
|
+
# adapter = Oxigraph()
|
|
67
|
+
|
|
68
|
+
# assert adapter.oxigraph_url == "http://localhost:7878"
|
|
69
|
+
# assert adapter.query_endpoint == "http://localhost:7878/query"
|
|
70
|
+
# assert adapter.update_endpoint == "http://localhost:7878/update"
|
|
71
|
+
# assert adapter.store_endpoint == "http://localhost:7878/store"
|
|
72
|
+
# assert adapter.timeout == 60
|
|
73
|
+
|
|
74
|
+
# def test_init_custom_parameters(self, mock_oxigraph_url):
|
|
75
|
+
# """Test Oxigraph initialization with custom parameters."""
|
|
76
|
+
# with patch('requests.get') as mock_get:
|
|
77
|
+
# mock_get.return_value.status_code = 200
|
|
78
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
79
|
+
# custom_url = "http://oxigraph.example.com:8080"
|
|
80
|
+
# adapter = Oxigraph(
|
|
81
|
+
# oxigraph_url=custom_url,
|
|
82
|
+
# timeout=30
|
|
83
|
+
# )
|
|
84
|
+
|
|
85
|
+
# assert adapter.oxigraph_url == custom_url
|
|
86
|
+
# assert adapter.query_endpoint == f"{custom_url}/query"
|
|
87
|
+
# assert adapter.timeout == 30
|
|
88
|
+
|
|
89
|
+
# def test_connection_test_success(self, mock_oxigraph_url):
|
|
90
|
+
# """Test successful connection test during initialization."""
|
|
91
|
+
# with patch('requests.get') as mock_get:
|
|
92
|
+
# mock_get.return_value.status_code = 200
|
|
93
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
94
|
+
|
|
95
|
+
# Oxigraph(mock_oxigraph_url)
|
|
96
|
+
|
|
97
|
+
# # Verify connection test was performed
|
|
98
|
+
# mock_get.assert_called_once()
|
|
99
|
+
# call_args = mock_get.call_args
|
|
100
|
+
# assert call_args[0][0] == f"{mock_oxigraph_url}/query"
|
|
101
|
+
# assert "query" in call_args[1]["params"]
|
|
102
|
+
|
|
103
|
+
# def test_connection_test_failure(self, mock_oxigraph_url):
|
|
104
|
+
# """Test connection failure during initialization."""
|
|
105
|
+
# with patch('requests.get') as mock_get:
|
|
106
|
+
# mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed")
|
|
107
|
+
|
|
108
|
+
# with pytest.raises(requests.exceptions.ConnectionError):
|
|
109
|
+
# Oxigraph(mock_oxigraph_url)
|
|
110
|
+
|
|
111
|
+
# def test_insert_success(self, mock_oxigraph_url, sample_graph):
|
|
112
|
+
# """Test successful triple insertion."""
|
|
113
|
+
# with patch('requests.get') as mock_get, \
|
|
114
|
+
# patch('requests.post') as mock_post:
|
|
115
|
+
|
|
116
|
+
# # Setup mocks
|
|
117
|
+
# mock_get.return_value.status_code = 200
|
|
118
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
119
|
+
# mock_post.return_value.raise_for_status = Mock()
|
|
120
|
+
|
|
121
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
122
|
+
# adapter.insert(sample_graph)
|
|
123
|
+
|
|
124
|
+
# # Verify the POST call was made correctly
|
|
125
|
+
# assert mock_post.call_count == 1
|
|
126
|
+
# call_args = mock_post.call_args
|
|
127
|
+
# assert call_args[0][0] == adapter.update_endpoint
|
|
128
|
+
# assert call_args[1]["headers"]["Content-Type"] == "application/sparql-update"
|
|
129
|
+
# assert "INSERT DATA" in call_args[1]["data"]
|
|
130
|
+
# assert "alice" in call_args[1]["data"] # Check that our sample data is there
|
|
131
|
+
|
|
132
|
+
# def test_remove_success(self, mock_oxigraph_url, sample_graph):
|
|
133
|
+
# """Test successful triple removal."""
|
|
134
|
+
# with patch('requests.get') as mock_get, \
|
|
135
|
+
# patch('requests.post') as mock_post:
|
|
136
|
+
|
|
137
|
+
# # Setup mocks
|
|
138
|
+
# mock_get.return_value.status_code = 200
|
|
139
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
140
|
+
# mock_post.return_value.raise_for_status = Mock()
|
|
141
|
+
|
|
142
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
143
|
+
# adapter.remove(sample_graph)
|
|
144
|
+
|
|
145
|
+
# # Verify the POST call was made correctly
|
|
146
|
+
# assert mock_post.call_count == 1
|
|
147
|
+
# call_args = mock_post.call_args
|
|
148
|
+
# assert call_args[0][0] == adapter.update_endpoint
|
|
149
|
+
# assert call_args[1]["headers"]["Content-Type"] == "application/sparql-update"
|
|
150
|
+
# assert "DELETE DATA" in call_args[1]["data"]
|
|
151
|
+
# assert "alice" in call_args[1]["data"]
|
|
152
|
+
|
|
153
|
+
# def test_get_success(self, mock_oxigraph_url, mock_turtle_response):
|
|
154
|
+
# """Test successful retrieval of all triples."""
|
|
155
|
+
# with patch('requests.get') as mock_get:
|
|
156
|
+
|
|
157
|
+
# # First call for connection test, second for get
|
|
158
|
+
# mock_get.side_effect = [
|
|
159
|
+
# Mock(status_code=200, raise_for_status=Mock()), # Connection test
|
|
160
|
+
# mock_turtle_response # Get response
|
|
161
|
+
# ]
|
|
162
|
+
|
|
163
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
164
|
+
# result = adapter.get()
|
|
165
|
+
|
|
166
|
+
# assert isinstance(result, Graph)
|
|
167
|
+
# # The mock response contains one triple
|
|
168
|
+
# assert len(result) > 0
|
|
169
|
+
|
|
170
|
+
# def test_query_select_success(self, mock_oxigraph_url, mock_successful_response):
|
|
171
|
+
# """Test successful SELECT query execution."""
|
|
172
|
+
# with patch('requests.get') as mock_get, \
|
|
173
|
+
# patch('requests.post') as mock_post:
|
|
174
|
+
|
|
175
|
+
# # Setup mocks
|
|
176
|
+
# mock_get.return_value.status_code = 200
|
|
177
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
178
|
+
# mock_post.return_value = mock_successful_response
|
|
179
|
+
|
|
180
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
181
|
+
# query = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"
|
|
182
|
+
# result = adapter.query(query)
|
|
183
|
+
|
|
184
|
+
# # Verify the query was executed correctly
|
|
185
|
+
# call_args = mock_post.call_args
|
|
186
|
+
# assert call_args[0][0] == adapter.query_endpoint
|
|
187
|
+
# assert call_args[1]["data"] == query
|
|
188
|
+
# assert "sparql-results" in call_args[1]["headers"]["Accept"]
|
|
189
|
+
|
|
190
|
+
# # Verify we can iterate results
|
|
191
|
+
# result_list = list(result)
|
|
192
|
+
# assert len(result_list) == 1
|
|
193
|
+
|
|
194
|
+
# def test_query_construct_success(self, mock_oxigraph_url, mock_turtle_response):
|
|
195
|
+
# """Test successful CONSTRUCT query execution."""
|
|
196
|
+
# with patch('requests.get') as mock_get, \
|
|
197
|
+
# patch('requests.post') as mock_post:
|
|
198
|
+
|
|
199
|
+
# # Setup mocks
|
|
200
|
+
# mock_get.return_value.status_code = 200
|
|
201
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
202
|
+
# mock_turtle_response.headers = {"Content-Type": "application/n-triples"}
|
|
203
|
+
# mock_turtle_response.text = "<http://example.org/alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Person> .\n"
|
|
204
|
+
# mock_post.return_value = mock_turtle_response
|
|
205
|
+
|
|
206
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
207
|
+
# query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"
|
|
208
|
+
# result = adapter.query(query)
|
|
209
|
+
|
|
210
|
+
# # For CONSTRUCT queries, result should be a Graph
|
|
211
|
+
# assert isinstance(result, Graph)
|
|
212
|
+
|
|
213
|
+
# def test_query_update_success(self, mock_oxigraph_url):
|
|
214
|
+
# """Test successful UPDATE query execution."""
|
|
215
|
+
# with patch('requests.get') as mock_get, \
|
|
216
|
+
# patch('requests.post') as mock_post:
|
|
217
|
+
|
|
218
|
+
# # Setup mocks
|
|
219
|
+
# mock_get.return_value.status_code = 200
|
|
220
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
221
|
+
# update_response = Mock()
|
|
222
|
+
# update_response.status_code = 200
|
|
223
|
+
# update_response.headers = {"Content-Type": "text/plain"}
|
|
224
|
+
# update_response.raise_for_status = Mock()
|
|
225
|
+
# mock_post.return_value = update_response
|
|
226
|
+
|
|
227
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
228
|
+
# query = "INSERT DATA { <http://example.org/test> <http://example.org/prop> 'value' }"
|
|
229
|
+
# adapter.query(query)
|
|
230
|
+
|
|
231
|
+
# # Verify the update was executed correctly
|
|
232
|
+
# call_args = mock_post.call_args
|
|
233
|
+
# assert call_args[0][0] == adapter.update_endpoint
|
|
234
|
+
# assert call_args[1]["data"] == query
|
|
235
|
+
|
|
236
|
+
# def test_get_subject_graph_success(self, mock_oxigraph_url, mock_turtle_response):
|
|
237
|
+
# """Test successful subject graph retrieval."""
|
|
238
|
+
# with patch('requests.get') as mock_get, \
|
|
239
|
+
# patch('requests.post') as mock_post:
|
|
240
|
+
|
|
241
|
+
# # Setup mocks
|
|
242
|
+
# mock_get.return_value.status_code = 200
|
|
243
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
244
|
+
# mock_turtle_response.headers = {"Content-Type": "application/n-triples"}
|
|
245
|
+
# mock_turtle_response.text = "<http://example.org/alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Person> .\n"
|
|
246
|
+
# mock_post.return_value = mock_turtle_response
|
|
247
|
+
|
|
248
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
249
|
+
# subject = URIRef("http://example.org/alice")
|
|
250
|
+
# result = adapter.get_subject_graph(subject)
|
|
251
|
+
|
|
252
|
+
# assert isinstance(result, Graph)
|
|
253
|
+
# # Verify the query was constructed correctly
|
|
254
|
+
# call_args = mock_post.call_args
|
|
255
|
+
# assert str(subject) in call_args[1]["data"]
|
|
256
|
+
# assert "CONSTRUCT" in call_args[1]["data"]
|
|
257
|
+
|
|
258
|
+
# def test_query_view_delegates_to_query(self, mock_oxigraph_url):
|
|
259
|
+
# """Test that query_view delegates to query method."""
|
|
260
|
+
# with patch('requests.get') as mock_get:
|
|
261
|
+
# mock_get.return_value.status_code = 200
|
|
262
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
263
|
+
|
|
264
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
265
|
+
|
|
266
|
+
# # Mock the query method
|
|
267
|
+
# adapter.query = Mock(return_value="mocked_result")
|
|
268
|
+
|
|
269
|
+
# query = "SELECT ?s WHERE { ?s ?p ?o }"
|
|
270
|
+
# result = adapter.query_view("some_view", query)
|
|
271
|
+
|
|
272
|
+
# # Verify query was called with the same query string
|
|
273
|
+
# adapter.query.assert_called_once_with(query)
|
|
274
|
+
# assert result == "mocked_result"
|
|
275
|
+
|
|
276
|
+
# def test_handle_view_event_no_op(self, mock_oxigraph_url):
|
|
277
|
+
# """Test that handle_view_event is a no-op."""
|
|
278
|
+
# with patch('requests.get') as mock_get:
|
|
279
|
+
# mock_get.return_value.status_code = 200
|
|
280
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
281
|
+
|
|
282
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
283
|
+
|
|
284
|
+
# # This should not raise any exception
|
|
285
|
+
# adapter.handle_view_event(
|
|
286
|
+
# view=(None, None, None),
|
|
287
|
+
# event=OntologyEvent.INSERT,
|
|
288
|
+
# triple=(URIRef("http://example.org/s"), URIRef("http://example.org/p"), URIRef("http://example.org/o"))
|
|
289
|
+
# )
|
|
290
|
+
|
|
291
|
+
# def test_insert_http_error(self, mock_oxigraph_url, sample_graph):
|
|
292
|
+
# """Test that HTTP errors are properly raised during insert."""
|
|
293
|
+
# with patch('requests.get') as mock_get, \
|
|
294
|
+
# patch('requests.post') as mock_post:
|
|
295
|
+
|
|
296
|
+
# # Setup mocks
|
|
297
|
+
# mock_get.return_value.status_code = 200
|
|
298
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
299
|
+
# mock_post.return_value.raise_for_status.side_effect = requests.exceptions.HTTPError("Server Error")
|
|
300
|
+
|
|
301
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
302
|
+
|
|
303
|
+
# with pytest.raises(requests.exceptions.HTTPError):
|
|
304
|
+
# adapter.insert(sample_graph)
|
|
305
|
+
|
|
306
|
+
# def test_query_timeout_error(self, mock_oxigraph_url):
|
|
307
|
+
# """Test that timeout errors are properly handled."""
|
|
308
|
+
# with patch('requests.get') as mock_get, \
|
|
309
|
+
# patch('requests.post') as mock_post:
|
|
310
|
+
|
|
311
|
+
# # Setup mocks
|
|
312
|
+
# mock_get.return_value.status_code = 200
|
|
313
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
314
|
+
# mock_post.side_effect = requests.exceptions.Timeout("Request timed out")
|
|
315
|
+
|
|
316
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
317
|
+
|
|
318
|
+
# with pytest.raises(requests.exceptions.Timeout):
|
|
319
|
+
# adapter.query("SELECT ?s WHERE { ?s ?p ?o }")
|
|
320
|
+
|
|
321
|
+
# def test_query_unexpected_content_type(self, mock_oxigraph_url):
|
|
322
|
+
# """Test handling of unexpected content types in query responses."""
|
|
323
|
+
# with patch('requests.get') as mock_get, \
|
|
324
|
+
# patch('requests.post') as mock_post:
|
|
325
|
+
|
|
326
|
+
# # Setup mocks
|
|
327
|
+
# mock_get.return_value.status_code = 200
|
|
328
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
329
|
+
|
|
330
|
+
# unexpected_response = Mock()
|
|
331
|
+
# unexpected_response.status_code = 200
|
|
332
|
+
# unexpected_response.headers = {"Content-Type": "text/html"}
|
|
333
|
+
# unexpected_response.raise_for_status = Mock()
|
|
334
|
+
# mock_post.return_value = unexpected_response
|
|
335
|
+
|
|
336
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
337
|
+
|
|
338
|
+
# with pytest.raises(ValueError, match="Unexpected content type"):
|
|
339
|
+
# adapter.query("SELECT ?s WHERE { ?s ?p ?o }")
|
|
340
|
+
|
|
341
|
+
# def test_insert_empty_graph(self, mock_oxigraph_url):
|
|
342
|
+
# """Test that inserting an empty graph is handled gracefully."""
|
|
343
|
+
# with patch('requests.get') as mock_get, \
|
|
344
|
+
# patch('requests.post') as mock_post:
|
|
345
|
+
|
|
346
|
+
# # Setup mocks
|
|
347
|
+
# mock_get.return_value.status_code = 200
|
|
348
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
349
|
+
|
|
350
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
351
|
+
# empty_graph = Graph()
|
|
352
|
+
|
|
353
|
+
# # Insert empty graph - should not make HTTP request
|
|
354
|
+
# adapter.insert(empty_graph)
|
|
355
|
+
|
|
356
|
+
# # Verify no POST requests were made for empty graph
|
|
357
|
+
# mock_post.assert_not_called()
|
|
358
|
+
|
|
359
|
+
# def test_remove_empty_graph(self, mock_oxigraph_url):
|
|
360
|
+
# """Test that removing from an empty graph works correctly."""
|
|
361
|
+
# with patch('requests.get') as mock_get, \
|
|
362
|
+
# patch('requests.post') as mock_post:
|
|
363
|
+
|
|
364
|
+
# # Setup mocks
|
|
365
|
+
# mock_get.return_value.status_code = 200
|
|
366
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
367
|
+
# mock_post.return_value.raise_for_status = Mock()
|
|
368
|
+
|
|
369
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
370
|
+
# empty_graph = Graph()
|
|
371
|
+
|
|
372
|
+
# # Remove empty graph - should still work
|
|
373
|
+
# adapter.remove(empty_graph)
|
|
374
|
+
|
|
375
|
+
# # Verify POST was called (empty DELETE DATA query is still valid)
|
|
376
|
+
# assert mock_post.call_count == 1
|
|
377
|
+
# call_args = mock_post.call_args
|
|
378
|
+
# assert "DELETE DATA" in call_args[1]["data"]
|
|
379
|
+
|
|
380
|
+
# def test_query_ask_query(self, mock_oxigraph_url):
|
|
381
|
+
# """Test ASK query handling."""
|
|
382
|
+
# with patch('requests.get') as mock_get, \
|
|
383
|
+
# patch('requests.post') as mock_post:
|
|
384
|
+
|
|
385
|
+
# # Setup mocks
|
|
386
|
+
# mock_get.return_value.status_code = 200
|
|
387
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
388
|
+
|
|
389
|
+
# ask_response = Mock()
|
|
390
|
+
# ask_response.status_code = 200
|
|
391
|
+
# ask_response.headers = {"Content-Type": "application/sparql-results+json"}
|
|
392
|
+
# ask_response.text = """{
|
|
393
|
+
# "head": {},
|
|
394
|
+
# "boolean": true
|
|
395
|
+
# }"""
|
|
396
|
+
# ask_response.raise_for_status = Mock()
|
|
397
|
+
# mock_post.return_value = ask_response
|
|
398
|
+
|
|
399
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
400
|
+
# query = "ASK WHERE { ?s ?p ?o }"
|
|
401
|
+
# result = adapter.query(query)
|
|
402
|
+
|
|
403
|
+
# # ASK queries return results but with different structure
|
|
404
|
+
# result_list = list(result)
|
|
405
|
+
# # For ASK queries, the result structure is different but should still be iterable
|
|
406
|
+
# assert isinstance(result_list, list)
|
|
407
|
+
|
|
408
|
+
# def test_query_with_different_datatypes(self, mock_oxigraph_url):
|
|
409
|
+
# """Test query results with different RDF datatypes."""
|
|
410
|
+
# with patch('requests.get') as mock_get, \
|
|
411
|
+
# patch('requests.post') as mock_post:
|
|
412
|
+
|
|
413
|
+
# # Setup mocks
|
|
414
|
+
# mock_get.return_value.status_code = 200
|
|
415
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
416
|
+
|
|
417
|
+
# # Mock response with various datatypes
|
|
418
|
+
# response = Mock()
|
|
419
|
+
# response.status_code = 200
|
|
420
|
+
# response.headers = {"Content-Type": "application/sparql-results+json"}
|
|
421
|
+
# response.text = """{
|
|
422
|
+
# "head": {"vars": ["uri", "integer", "string", "date"]},
|
|
423
|
+
# "results": {
|
|
424
|
+
# "bindings": [{
|
|
425
|
+
# "uri": {"type": "uri", "value": "http://example.org/test"},
|
|
426
|
+
# "integer": {"type": "literal", "value": "42", "datatype": "http://www.w3.org/2001/XMLSchema#integer"},
|
|
427
|
+
# "string": {"type": "literal", "value": "test string"},
|
|
428
|
+
# "date": {"type": "literal", "value": "2023-01-01", "datatype": "http://www.w3.org/2001/XMLSchema#date"}
|
|
429
|
+
# }]
|
|
430
|
+
# }
|
|
431
|
+
# }"""
|
|
432
|
+
# response.raise_for_status = Mock()
|
|
433
|
+
# mock_post.return_value = response
|
|
434
|
+
|
|
435
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
436
|
+
# result = adapter.query("SELECT ?uri ?integer ?string ?date WHERE { ?s ?p ?o }")
|
|
437
|
+
|
|
438
|
+
# rows = list(result)
|
|
439
|
+
# assert len(rows) == 1
|
|
440
|
+
# row = rows[0]
|
|
441
|
+
|
|
442
|
+
# # Test that different datatypes are handled correctly
|
|
443
|
+
# assert str(row.uri) == "http://example.org/test"
|
|
444
|
+
# assert isinstance(row.integer, Literal)
|
|
445
|
+
# assert int(row.integer) == 42
|
|
446
|
+
# assert str(row.string) == "test string"
|
|
447
|
+
|
|
448
|
+
# def test_get_subject_graph_empty_result(self, mock_oxigraph_url):
|
|
449
|
+
# """Test get_subject_graph when no triples are found for the subject."""
|
|
450
|
+
# with patch('requests.get') as mock_get, \
|
|
451
|
+
# patch('requests.post') as mock_post:
|
|
452
|
+
|
|
453
|
+
# # Setup mocks
|
|
454
|
+
# mock_get.return_value.status_code = 200
|
|
455
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
456
|
+
|
|
457
|
+
# # Mock empty CONSTRUCT result
|
|
458
|
+
# empty_response = Mock()
|
|
459
|
+
# empty_response.status_code = 200
|
|
460
|
+
# empty_response.headers = {"Content-Type": "application/n-triples"}
|
|
461
|
+
# empty_response.text = "" # Empty N-Triples
|
|
462
|
+
# empty_response.raise_for_status = Mock()
|
|
463
|
+
# mock_post.return_value = empty_response
|
|
464
|
+
|
|
465
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
466
|
+
# subject = URIRef("http://example.org/nonexistent")
|
|
467
|
+
# result = adapter.get_subject_graph(subject)
|
|
468
|
+
|
|
469
|
+
# assert isinstance(result, Graph)
|
|
470
|
+
# assert len(result) == 0
|
|
471
|
+
|
|
472
|
+
# def test_query_with_bnode(self, mock_oxigraph_url):
|
|
473
|
+
# """Test query results containing blank nodes."""
|
|
474
|
+
# with patch('requests.get') as mock_get, \
|
|
475
|
+
# patch('requests.post') as mock_post:
|
|
476
|
+
|
|
477
|
+
# # Setup mocks
|
|
478
|
+
# mock_get.return_value.status_code = 200
|
|
479
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
480
|
+
|
|
481
|
+
# bnode_response = Mock()
|
|
482
|
+
# bnode_response.status_code = 200
|
|
483
|
+
# bnode_response.headers = {"Content-Type": "application/sparql-results+json"}
|
|
484
|
+
# bnode_response.text = """{
|
|
485
|
+
# "head": {"vars": ["s", "p", "o"]},
|
|
486
|
+
# "results": {
|
|
487
|
+
# "bindings": [{
|
|
488
|
+
# "s": {"type": "bnode", "value": "_:b1"},
|
|
489
|
+
# "p": {"type": "uri", "value": "http://example.org/property"},
|
|
490
|
+
# "o": {"type": "literal", "value": "test value"}
|
|
491
|
+
# }]
|
|
492
|
+
# }
|
|
493
|
+
# }"""
|
|
494
|
+
# bnode_response.raise_for_status = Mock()
|
|
495
|
+
# mock_post.return_value = bnode_response
|
|
496
|
+
|
|
497
|
+
# adapter = Oxigraph(mock_oxigraph_url)
|
|
498
|
+
# result = adapter.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
499
|
+
|
|
500
|
+
# rows = list(result)
|
|
501
|
+
# assert len(rows) == 1
|
|
502
|
+
# row = rows[0]
|
|
503
|
+
|
|
504
|
+
# # Verify blank node is properly handled
|
|
505
|
+
# from rdflib.term import BNode
|
|
506
|
+
# assert isinstance(row.s, BNode)
|
|
507
|
+
|
|
508
|
+
# def test_connection_test_with_custom_timeout(self):
|
|
509
|
+
# """Test connection test with custom timeout."""
|
|
510
|
+
# with patch('requests.get') as mock_get:
|
|
511
|
+
# mock_get.return_value.status_code = 200
|
|
512
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
513
|
+
|
|
514
|
+
# adapter = Oxigraph("http://localhost:7878", timeout=30)
|
|
515
|
+
|
|
516
|
+
# assert adapter.timeout == 30
|
|
517
|
+
# # Verify connection test was called with correct timeout
|
|
518
|
+
# mock_get.assert_called_once()
|
|
519
|
+
# assert mock_get.call_args[1]['timeout'] == 30
|
|
520
|
+
|
|
521
|
+
# def test_main_script_functionality(self):
|
|
522
|
+
# """Test the main script functionality for basic operations."""
|
|
523
|
+
# with patch('requests.get') as mock_get, \
|
|
524
|
+
# patch('requests.post') as mock_post:
|
|
525
|
+
|
|
526
|
+
# # Setup HTTP mocks
|
|
527
|
+
# mock_get.return_value.status_code = 200
|
|
528
|
+
# mock_get.return_value.raise_for_status = Mock()
|
|
529
|
+
# mock_post.return_value.raise_for_status = Mock()
|
|
530
|
+
|
|
531
|
+
# # Test with explicit parameters
|
|
532
|
+
# adapter = Oxigraph(
|
|
533
|
+
# oxigraph_url="http://localhost:7878"
|
|
534
|
+
# )
|
|
535
|
+
|
|
536
|
+
# # Test that the main functionality can be called without errors
|
|
537
|
+
# assert adapter.oxigraph_url == "http://localhost:7878"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class TestOxigraphIntegration:
|
|
541
|
+
"""Integration tests for Oxigraph (require running Oxigraph instance)."""
|
|
542
|
+
|
|
543
|
+
@pytest.fixture
|
|
544
|
+
def oxigraph_adapter(self):
|
|
545
|
+
"""Create an Oxigraph adapter for integration testing."""
|
|
546
|
+
return Oxigraph(oxigraph_url="http://localhost:7878")
|
|
547
|
+
|
|
548
|
+
@pytest.fixture
|
|
549
|
+
def integration_graph(self):
|
|
550
|
+
"""Create a test graph for integration testing."""
|
|
551
|
+
g = Graph()
|
|
552
|
+
g.bind("test", "http://test.example.org/")
|
|
553
|
+
|
|
554
|
+
subject = URIRef("http://test.example.org/integration_test")
|
|
555
|
+
g.add((subject, RDF.type, URIRef("http://test.example.org/TestEntity")))
|
|
556
|
+
g.add(
|
|
557
|
+
(
|
|
558
|
+
subject,
|
|
559
|
+
URIRef("http://test.example.org/testProperty"),
|
|
560
|
+
Literal("integration_value"),
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return g
|
|
565
|
+
|
|
566
|
+
def generate_graph(
|
|
567
|
+
self, num_triples: int, base_uri: str = "http://test.example.org/"
|
|
568
|
+
):
|
|
569
|
+
"""Create a large test graph for integration testing."""
|
|
570
|
+
g = Graph()
|
|
571
|
+
g.bind("test", base_uri)
|
|
572
|
+
|
|
573
|
+
for i in range(num_triples):
|
|
574
|
+
subject = URIRef(f"{base_uri}/entity{i}")
|
|
575
|
+
g.add((subject, RDF.type, URIRef(f"{base_uri}/TestEntity")))
|
|
576
|
+
|
|
577
|
+
return g
|
|
578
|
+
|
|
579
|
+
def test_large_graph_insert(self, oxigraph_adapter):
|
|
580
|
+
"""Test inserting a large graph into Oxigraph."""
|
|
581
|
+
try:
|
|
582
|
+
base_uri = f"http://test.example.org/{uuid.uuid4()}"
|
|
583
|
+
large_graph = self.generate_graph(100001, base_uri)
|
|
584
|
+
oxigraph_adapter.insert(large_graph)
|
|
585
|
+
|
|
586
|
+
# Verify the data was inserted
|
|
587
|
+
count_query = (
|
|
588
|
+
f"SELECT (COUNT(*) as ?count) WHERE {{ ?s a <{base_uri}/TestEntity> }}"
|
|
589
|
+
)
|
|
590
|
+
result = list(oxigraph_adapter.query(count_query))
|
|
591
|
+
assert len(result) == 1
|
|
592
|
+
|
|
593
|
+
print(result)
|
|
594
|
+
|
|
595
|
+
# Access count using Variable object
|
|
596
|
+
from rdflib.term import Variable
|
|
597
|
+
|
|
598
|
+
count_var = Variable("count")
|
|
599
|
+
assert int(result[0][count_var]) == 100001
|
|
600
|
+
|
|
601
|
+
# Cleanup
|
|
602
|
+
oxigraph_adapter.remove(large_graph)
|
|
603
|
+
except requests.exceptions.ConnectionError:
|
|
604
|
+
pytest.skip("Oxigraph is not running - skipping large graph test")
|
|
605
|
+
|
|
606
|
+
def test_insert_and_query_workflow(self, oxigraph_adapter, integration_graph):
|
|
607
|
+
"""Test basic insert and query workflow."""
|
|
608
|
+
try:
|
|
609
|
+
# Insert test data
|
|
610
|
+
oxigraph_adapter.insert(integration_graph)
|
|
611
|
+
|
|
612
|
+
# Query for the data
|
|
613
|
+
query = """
|
|
614
|
+
SELECT ?s ?p ?o WHERE {
|
|
615
|
+
?s <http://test.example.org/testProperty> ?o
|
|
616
|
+
}
|
|
617
|
+
"""
|
|
618
|
+
results = list(oxigraph_adapter.query(query))
|
|
619
|
+
assert len(results) == 1
|
|
620
|
+
assert str(results[0].o) == "integration_value"
|
|
621
|
+
|
|
622
|
+
# Cleanup
|
|
623
|
+
oxigraph_adapter.remove(integration_graph)
|
|
624
|
+
|
|
625
|
+
except requests.exceptions.ConnectionError:
|
|
626
|
+
pytest.skip("Oxigraph is not running - skipping integration test")
|
|
627
|
+
|
|
628
|
+
def test_construct_query(self, oxigraph_adapter, integration_graph):
|
|
629
|
+
"""Test CONSTRUCT query returns a graph."""
|
|
630
|
+
try:
|
|
631
|
+
oxigraph_adapter.insert(integration_graph)
|
|
632
|
+
|
|
633
|
+
query = """
|
|
634
|
+
CONSTRUCT { ?s ?p ?o }
|
|
635
|
+
WHERE {
|
|
636
|
+
?s <http://test.example.org/testProperty> ?o .
|
|
637
|
+
?s ?p ?o
|
|
638
|
+
}
|
|
639
|
+
"""
|
|
640
|
+
result = oxigraph_adapter.query(query)
|
|
641
|
+
|
|
642
|
+
assert isinstance(result, Graph)
|
|
643
|
+
assert len(result) > 0
|
|
644
|
+
|
|
645
|
+
# Cleanup
|
|
646
|
+
oxigraph_adapter.remove(integration_graph)
|
|
647
|
+
|
|
648
|
+
except requests.exceptions.ConnectionError:
|
|
649
|
+
pytest.skip("Oxigraph is not running - skipping construct test")
|
|
650
|
+
|
|
651
|
+
def test_ask_query(self, oxigraph_adapter, integration_graph):
|
|
652
|
+
"""Test ASK query."""
|
|
653
|
+
try:
|
|
654
|
+
oxigraph_adapter.insert(integration_graph)
|
|
655
|
+
|
|
656
|
+
# Ask if data exists
|
|
657
|
+
ask_query = """
|
|
658
|
+
ASK WHERE {
|
|
659
|
+
?s <http://test.example.org/testProperty> "integration_value"
|
|
660
|
+
}
|
|
661
|
+
"""
|
|
662
|
+
result = list(oxigraph_adapter.query(ask_query))
|
|
663
|
+
assert result is not None
|
|
664
|
+
|
|
665
|
+
# Cleanup
|
|
666
|
+
oxigraph_adapter.remove(integration_graph)
|
|
667
|
+
|
|
668
|
+
except requests.exceptions.ConnectionError:
|
|
669
|
+
pytest.skip("Oxigraph is not running - skipping ASK test")
|
|
670
|
+
|
|
671
|
+
def test_get_subject_graph(self, oxigraph_adapter, integration_graph):
|
|
672
|
+
"""Test retrieving all triples for a specific subject."""
|
|
673
|
+
try:
|
|
674
|
+
oxigraph_adapter.insert(integration_graph)
|
|
675
|
+
|
|
676
|
+
subject = URIRef("http://test.example.org/integration_test")
|
|
677
|
+
subject_graph = oxigraph_adapter.get_subject_graph(subject)
|
|
678
|
+
|
|
679
|
+
assert isinstance(subject_graph, Graph)
|
|
680
|
+
assert len(subject_graph) == 2 # type and testProperty
|
|
681
|
+
|
|
682
|
+
# Cleanup
|
|
683
|
+
oxigraph_adapter.remove(integration_graph)
|
|
684
|
+
|
|
685
|
+
except requests.exceptions.ConnectionError:
|
|
686
|
+
pytest.skip("Oxigraph is not running - skipping subject graph test")
|
|
687
|
+
|
|
688
|
+
def test_filter_query(self, oxigraph_adapter):
|
|
689
|
+
"""Test SPARQL FILTER clause."""
|
|
690
|
+
try:
|
|
691
|
+
# Create test data with numbers - use unique URI path
|
|
692
|
+
g = Graph()
|
|
693
|
+
g.bind("test", "http://test.example.org/filter/")
|
|
694
|
+
|
|
695
|
+
for i in range(10):
|
|
696
|
+
subject = URIRef(f"http://test.example.org/filter/number{i}")
|
|
697
|
+
g.add(
|
|
698
|
+
(subject, RDF.type, URIRef("http://test.example.org/filter/Number"))
|
|
699
|
+
)
|
|
700
|
+
g.add(
|
|
701
|
+
(
|
|
702
|
+
subject,
|
|
703
|
+
URIRef("http://test.example.org/filter/value"),
|
|
704
|
+
Literal(i),
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
oxigraph_adapter.insert(g)
|
|
709
|
+
|
|
710
|
+
# Query with FILTER - use specific URI
|
|
711
|
+
query = """
|
|
712
|
+
SELECT ?s ?value WHERE {
|
|
713
|
+
?s <http://test.example.org/filter/value> ?value .
|
|
714
|
+
FILTER(?value > 5)
|
|
715
|
+
}
|
|
716
|
+
ORDER BY ?value
|
|
717
|
+
"""
|
|
718
|
+
results = list(oxigraph_adapter.query(query))
|
|
719
|
+
|
|
720
|
+
assert len(results) == 4 # values 6, 7, 8, 9
|
|
721
|
+
from rdflib.term import Variable
|
|
722
|
+
|
|
723
|
+
value_var = Variable("value")
|
|
724
|
+
assert int(results[0][value_var]) == 6
|
|
725
|
+
assert int(results[-1][value_var]) == 9
|
|
726
|
+
|
|
727
|
+
# Cleanup
|
|
728
|
+
oxigraph_adapter.remove(g)
|
|
729
|
+
|
|
730
|
+
except requests.exceptions.ConnectionError:
|
|
731
|
+
pytest.skip("Oxigraph is not running - skipping FILTER test")
|
|
732
|
+
|
|
733
|
+
def test_optional_clause(self, oxigraph_adapter):
|
|
734
|
+
"""Test SPARQL OPTIONAL clause."""
|
|
735
|
+
try:
|
|
736
|
+
g = Graph()
|
|
737
|
+
g.bind("test", "http://test.example.org/")
|
|
738
|
+
|
|
739
|
+
# Create entities with and without optional properties
|
|
740
|
+
for i in range(5):
|
|
741
|
+
subject = URIRef(f"http://test.example.org/entity{i}")
|
|
742
|
+
g.add((subject, RDF.type, URIRef("http://test.example.org/Entity")))
|
|
743
|
+
g.add((subject, URIRef("http://test.example.org/id"), Literal(i)))
|
|
744
|
+
|
|
745
|
+
# Only add optional property for even numbers
|
|
746
|
+
if i % 2 == 0:
|
|
747
|
+
g.add(
|
|
748
|
+
(
|
|
749
|
+
subject,
|
|
750
|
+
URIRef("http://test.example.org/optional"),
|
|
751
|
+
Literal(f"value{i}"),
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
oxigraph_adapter.insert(g)
|
|
756
|
+
|
|
757
|
+
query = """
|
|
758
|
+
SELECT ?s ?id ?optional WHERE {
|
|
759
|
+
?s <http://test.example.org/id> ?id .
|
|
760
|
+
OPTIONAL { ?s <http://test.example.org/optional> ?optional }
|
|
761
|
+
}
|
|
762
|
+
ORDER BY ?id
|
|
763
|
+
"""
|
|
764
|
+
results = list(oxigraph_adapter.query(query))
|
|
765
|
+
|
|
766
|
+
assert len(results) == 5
|
|
767
|
+
# Check that optional values are present for even IDs
|
|
768
|
+
for row in results:
|
|
769
|
+
if int(row.id) % 2 == 0:
|
|
770
|
+
assert row.optional is not None
|
|
771
|
+
else:
|
|
772
|
+
assert row.optional is None
|
|
773
|
+
|
|
774
|
+
# Cleanup
|
|
775
|
+
oxigraph_adapter.remove(g)
|
|
776
|
+
|
|
777
|
+
except requests.exceptions.ConnectionError:
|
|
778
|
+
pytest.skip("Oxigraph is not running - skipping OPTIONAL test")
|
|
779
|
+
|
|
780
|
+
def test_union_clause(self, oxigraph_adapter):
|
|
781
|
+
"""Test SPARQL UNION clause."""
|
|
782
|
+
try:
|
|
783
|
+
g = Graph()
|
|
784
|
+
g.bind("test", "http://test.example.org/")
|
|
785
|
+
|
|
786
|
+
# Create different types of entities
|
|
787
|
+
person = URIRef("http://test.example.org/person1")
|
|
788
|
+
g.add((person, RDF.type, URIRef("http://test.example.org/Person")))
|
|
789
|
+
g.add((person, URIRef("http://test.example.org/name"), Literal("Alice")))
|
|
790
|
+
|
|
791
|
+
org = URIRef("http://test.example.org/org1")
|
|
792
|
+
g.add((org, RDF.type, URIRef("http://test.example.org/Organization")))
|
|
793
|
+
g.add((org, URIRef("http://test.example.org/title"), Literal("ACME Corp")))
|
|
794
|
+
|
|
795
|
+
oxigraph_adapter.insert(g)
|
|
796
|
+
|
|
797
|
+
query = """
|
|
798
|
+
SELECT ?entity ?label WHERE {
|
|
799
|
+
{
|
|
800
|
+
?entity a <http://test.example.org/Person> .
|
|
801
|
+
?entity <http://test.example.org/name> ?label .
|
|
802
|
+
}
|
|
803
|
+
UNION
|
|
804
|
+
{
|
|
805
|
+
?entity a <http://test.example.org/Organization> .
|
|
806
|
+
?entity <http://test.example.org/title> ?label .
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
"""
|
|
810
|
+
results = list(oxigraph_adapter.query(query))
|
|
811
|
+
|
|
812
|
+
assert len(results) == 2
|
|
813
|
+
labels = [str(row.label) for row in results]
|
|
814
|
+
assert "Alice" in labels
|
|
815
|
+
assert "ACME Corp" in labels
|
|
816
|
+
|
|
817
|
+
# Cleanup
|
|
818
|
+
oxigraph_adapter.remove(g)
|
|
819
|
+
|
|
820
|
+
except requests.exceptions.ConnectionError:
|
|
821
|
+
pytest.skip("Oxigraph is not running - skipping UNION test")
|
|
822
|
+
|
|
823
|
+
def test_aggregation_functions(self, oxigraph_adapter):
|
|
824
|
+
"""Test SPARQL aggregation functions (COUNT, SUM, AVG, MIN, MAX)."""
|
|
825
|
+
try:
|
|
826
|
+
from rdflib.term import Variable
|
|
827
|
+
|
|
828
|
+
g = Graph()
|
|
829
|
+
g.bind("test", "http://test.example.org/agg/")
|
|
830
|
+
|
|
831
|
+
values = [10, 20, 30, 40, 50]
|
|
832
|
+
for i, val in enumerate(values):
|
|
833
|
+
subject = URIRef(f"http://test.example.org/agg/item{i}")
|
|
834
|
+
g.add(
|
|
835
|
+
(subject, URIRef("http://test.example.org/agg/value"), Literal(val))
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
oxigraph_adapter.insert(g)
|
|
839
|
+
|
|
840
|
+
# Test COUNT, SUM, AVG, MIN, MAX
|
|
841
|
+
query = """
|
|
842
|
+
SELECT
|
|
843
|
+
(COUNT(?value) as ?count)
|
|
844
|
+
(SUM(?value) as ?sum)
|
|
845
|
+
(AVG(?value) as ?avg)
|
|
846
|
+
(MIN(?value) as ?min)
|
|
847
|
+
(MAX(?value) as ?max)
|
|
848
|
+
WHERE {
|
|
849
|
+
?s <http://test.example.org/agg/value> ?value .
|
|
850
|
+
}
|
|
851
|
+
"""
|
|
852
|
+
results = list(oxigraph_adapter.query(query))
|
|
853
|
+
|
|
854
|
+
assert len(results) == 1
|
|
855
|
+
row = results[0]
|
|
856
|
+
count_var = Variable("count")
|
|
857
|
+
sum_var = Variable("sum")
|
|
858
|
+
avg_var = Variable("avg")
|
|
859
|
+
min_var = Variable("min")
|
|
860
|
+
max_var = Variable("max")
|
|
861
|
+
|
|
862
|
+
assert int(row[count_var]) == 5
|
|
863
|
+
assert int(row[sum_var]) == 150
|
|
864
|
+
assert int(row[avg_var]) == 30
|
|
865
|
+
assert int(row[min_var]) == 10
|
|
866
|
+
assert int(row[max_var]) == 50
|
|
867
|
+
|
|
868
|
+
# Cleanup
|
|
869
|
+
oxigraph_adapter.remove(g)
|
|
870
|
+
|
|
871
|
+
except requests.exceptions.ConnectionError:
|
|
872
|
+
pytest.skip("Oxigraph is not running - skipping aggregation test")
|
|
873
|
+
|
|
874
|
+
def test_group_by_having(self, oxigraph_adapter):
|
|
875
|
+
"""Test SPARQL GROUP BY and HAVING clauses."""
|
|
876
|
+
try:
|
|
877
|
+
from rdflib.term import Variable
|
|
878
|
+
|
|
879
|
+
g = Graph()
|
|
880
|
+
g.bind("test", "http://test.example.org/groupby/")
|
|
881
|
+
|
|
882
|
+
# Create categories with items
|
|
883
|
+
categories = ["A", "B", "C"]
|
|
884
|
+
for cat in categories:
|
|
885
|
+
for i in range(3 if cat == "A" else 2 if cat == "B" else 1):
|
|
886
|
+
subject = URIRef(f"http://test.example.org/groupby/{cat}/item{i}")
|
|
887
|
+
g.add(
|
|
888
|
+
(
|
|
889
|
+
subject,
|
|
890
|
+
URIRef("http://test.example.org/groupby/category"),
|
|
891
|
+
Literal(cat),
|
|
892
|
+
)
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
oxigraph_adapter.insert(g)
|
|
896
|
+
|
|
897
|
+
query = """
|
|
898
|
+
SELECT ?category (COUNT(?item) as ?count)
|
|
899
|
+
WHERE {
|
|
900
|
+
?item <http://test.example.org/groupby/category> ?category .
|
|
901
|
+
}
|
|
902
|
+
GROUP BY ?category
|
|
903
|
+
HAVING (COUNT(?item) > 1)
|
|
904
|
+
ORDER BY DESC(?count)
|
|
905
|
+
"""
|
|
906
|
+
results = list(oxigraph_adapter.query(query))
|
|
907
|
+
|
|
908
|
+
assert len(results) == 2 # Only A and B have more than 1 item
|
|
909
|
+
category_var = Variable("category")
|
|
910
|
+
count_var = Variable("count")
|
|
911
|
+
|
|
912
|
+
assert str(results[0][category_var]) == "A"
|
|
913
|
+
assert int(results[0][count_var]) == 3
|
|
914
|
+
assert str(results[1][category_var]) == "B"
|
|
915
|
+
assert int(results[1][count_var]) == 2
|
|
916
|
+
|
|
917
|
+
# Cleanup
|
|
918
|
+
oxigraph_adapter.remove(g)
|
|
919
|
+
|
|
920
|
+
except requests.exceptions.ConnectionError:
|
|
921
|
+
pytest.skip("Oxigraph is not running - skipping GROUP BY test")
|
|
922
|
+
|
|
923
|
+
def test_blank_nodes_are_filtered(self, oxigraph_adapter):
|
|
924
|
+
"""Test that blank nodes are filtered out during insert."""
|
|
925
|
+
try:
|
|
926
|
+
domain = uuid.uuid4()
|
|
927
|
+
g = Graph()
|
|
928
|
+
|
|
929
|
+
# Create a structure with blank nodes
|
|
930
|
+
person = URIRef(f"http://{domain}/bnode/person1")
|
|
931
|
+
address = BNode()
|
|
932
|
+
|
|
933
|
+
g.add((person, RDF.type, URIRef(f"http://{domain}/bnode/Person")))
|
|
934
|
+
g.add((person, URIRef(f"http://{domain}/bnode/hasAddress"), address))
|
|
935
|
+
g.add(
|
|
936
|
+
(
|
|
937
|
+
address,
|
|
938
|
+
URIRef(f"http://{domain}/bnode/street"),
|
|
939
|
+
Literal("123 Main St"),
|
|
940
|
+
)
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
# Insert graph with blank nodes
|
|
944
|
+
oxigraph_adapter.insert(g)
|
|
945
|
+
|
|
946
|
+
# Query should only find the person type, not the address relationship
|
|
947
|
+
query = f"""
|
|
948
|
+
SELECT ?s ?p ?o WHERE {{
|
|
949
|
+
?s ?p ?o .
|
|
950
|
+
FILTER(STRSTARTS(STR(?s), "http://{domain}/"))
|
|
951
|
+
}}
|
|
952
|
+
"""
|
|
953
|
+
results = list(oxigraph_adapter.query(query))
|
|
954
|
+
|
|
955
|
+
# Should only have 1 triple (person rdf:type Person)
|
|
956
|
+
# The blank node triples should have been filtered out
|
|
957
|
+
assert len(results) == 1
|
|
958
|
+
|
|
959
|
+
# Cleanup
|
|
960
|
+
oxigraph_adapter.remove(g)
|
|
961
|
+
|
|
962
|
+
except requests.exceptions.ConnectionError:
|
|
963
|
+
pytest.skip("Oxigraph is not running")
|
|
964
|
+
|
|
965
|
+
def test_special_characters_in_literals(self, oxigraph_adapter):
|
|
966
|
+
"""Test handling of special characters in literals."""
|
|
967
|
+
try:
|
|
968
|
+
g = Graph()
|
|
969
|
+
g.bind("test", "http://test.example.org/")
|
|
970
|
+
|
|
971
|
+
subject = URIRef("http://test.example.org/special")
|
|
972
|
+
|
|
973
|
+
# Test various special characters
|
|
974
|
+
special_strings = [
|
|
975
|
+
'String with "quotes"',
|
|
976
|
+
"String with 'apostrophes'",
|
|
977
|
+
"String with\nnewlines",
|
|
978
|
+
"String with\ttabs",
|
|
979
|
+
"Unicode: 你好世界 🌍",
|
|
980
|
+
"Emoji: 😀 👍 🎉",
|
|
981
|
+
"Special chars: @#$%^&*()",
|
|
982
|
+
"Backslash: \\test\\path",
|
|
983
|
+
]
|
|
984
|
+
|
|
985
|
+
for i, text in enumerate(special_strings):
|
|
986
|
+
g.add(
|
|
987
|
+
(subject, URIRef(f"http://test.example.org/prop{i}"), Literal(text))
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
oxigraph_adapter.insert(g)
|
|
991
|
+
|
|
992
|
+
# Query back and verify
|
|
993
|
+
query = """
|
|
994
|
+
SELECT ?p ?o WHERE {
|
|
995
|
+
<http://test.example.org/special> ?p ?o .
|
|
996
|
+
}
|
|
997
|
+
"""
|
|
998
|
+
results = list(oxigraph_adapter.query(query))
|
|
999
|
+
|
|
1000
|
+
assert len(results) == len(special_strings)
|
|
1001
|
+
|
|
1002
|
+
retrieved_values = [str(row.o) for row in results]
|
|
1003
|
+
for original in special_strings:
|
|
1004
|
+
assert original in retrieved_values
|
|
1005
|
+
|
|
1006
|
+
# Cleanup
|
|
1007
|
+
oxigraph_adapter.remove(g)
|
|
1008
|
+
|
|
1009
|
+
except requests.exceptions.ConnectionError:
|
|
1010
|
+
pytest.skip("Oxigraph is not running - skipping special characters test")
|
|
1011
|
+
|
|
1012
|
+
def test_different_datatypes(self, oxigraph_adapter):
|
|
1013
|
+
"""Test handling of different RDF datatypes."""
|
|
1014
|
+
try:
|
|
1015
|
+
from rdflib.namespace import XSD
|
|
1016
|
+
|
|
1017
|
+
g = Graph()
|
|
1018
|
+
g.bind("test", "http://test.example.org/")
|
|
1019
|
+
|
|
1020
|
+
subject = URIRef("http://test.example.org/datatypes")
|
|
1021
|
+
|
|
1022
|
+
# Add different datatype literals
|
|
1023
|
+
g.add(
|
|
1024
|
+
(
|
|
1025
|
+
subject,
|
|
1026
|
+
URIRef("http://test.example.org/integer"),
|
|
1027
|
+
Literal(42, datatype=XSD.integer),
|
|
1028
|
+
)
|
|
1029
|
+
)
|
|
1030
|
+
g.add(
|
|
1031
|
+
(
|
|
1032
|
+
subject,
|
|
1033
|
+
URIRef("http://test.example.org/float"),
|
|
1034
|
+
Literal(3.14, datatype=XSD.float),
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
g.add(
|
|
1038
|
+
(
|
|
1039
|
+
subject,
|
|
1040
|
+
URIRef("http://test.example.org/boolean"),
|
|
1041
|
+
Literal(True, datatype=XSD.boolean),
|
|
1042
|
+
)
|
|
1043
|
+
)
|
|
1044
|
+
g.add(
|
|
1045
|
+
(
|
|
1046
|
+
subject,
|
|
1047
|
+
URIRef("http://test.example.org/date"),
|
|
1048
|
+
Literal("2024-01-01", datatype=XSD.date),
|
|
1049
|
+
)
|
|
1050
|
+
)
|
|
1051
|
+
g.add(
|
|
1052
|
+
(
|
|
1053
|
+
subject,
|
|
1054
|
+
URIRef("http://test.example.org/string"),
|
|
1055
|
+
Literal("test string", datatype=XSD.string),
|
|
1056
|
+
)
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
oxigraph_adapter.insert(g)
|
|
1060
|
+
|
|
1061
|
+
# Query and verify datatypes are preserved
|
|
1062
|
+
query = """
|
|
1063
|
+
SELECT ?p ?o WHERE {
|
|
1064
|
+
<http://test.example.org/datatypes> ?p ?o .
|
|
1065
|
+
}
|
|
1066
|
+
"""
|
|
1067
|
+
results = list(oxigraph_adapter.query(query))
|
|
1068
|
+
|
|
1069
|
+
assert len(results) == 5
|
|
1070
|
+
|
|
1071
|
+
# Cleanup
|
|
1072
|
+
oxigraph_adapter.remove(g)
|
|
1073
|
+
|
|
1074
|
+
except requests.exceptions.ConnectionError:
|
|
1075
|
+
pytest.skip("Oxigraph is not running - skipping datatypes test")
|
|
1076
|
+
|
|
1077
|
+
def test_language_tags(self, oxigraph_adapter):
|
|
1078
|
+
"""Test handling of language-tagged literals."""
|
|
1079
|
+
try:
|
|
1080
|
+
g = Graph()
|
|
1081
|
+
g.bind("test", "http://test.example.org/")
|
|
1082
|
+
|
|
1083
|
+
subject = URIRef("http://test.example.org/multilingual")
|
|
1084
|
+
|
|
1085
|
+
# Add same property with different language tags
|
|
1086
|
+
g.add(
|
|
1087
|
+
(
|
|
1088
|
+
subject,
|
|
1089
|
+
URIRef("http://test.example.org/label"),
|
|
1090
|
+
Literal("Hello", lang="en"),
|
|
1091
|
+
)
|
|
1092
|
+
)
|
|
1093
|
+
g.add(
|
|
1094
|
+
(
|
|
1095
|
+
subject,
|
|
1096
|
+
URIRef("http://test.example.org/label"),
|
|
1097
|
+
Literal("Bonjour", lang="fr"),
|
|
1098
|
+
)
|
|
1099
|
+
)
|
|
1100
|
+
g.add(
|
|
1101
|
+
(
|
|
1102
|
+
subject,
|
|
1103
|
+
URIRef("http://test.example.org/label"),
|
|
1104
|
+
Literal("Hola", lang="es"),
|
|
1105
|
+
)
|
|
1106
|
+
)
|
|
1107
|
+
g.add(
|
|
1108
|
+
(
|
|
1109
|
+
subject,
|
|
1110
|
+
URIRef("http://test.example.org/label"),
|
|
1111
|
+
Literal("你好", lang="zh"),
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
oxigraph_adapter.insert(g)
|
|
1116
|
+
|
|
1117
|
+
# Query with language filter
|
|
1118
|
+
query = """
|
|
1119
|
+
SELECT ?label WHERE {
|
|
1120
|
+
<http://test.example.org/multilingual> <http://test.example.org/label> ?label .
|
|
1121
|
+
FILTER(LANG(?label) = "fr")
|
|
1122
|
+
}
|
|
1123
|
+
"""
|
|
1124
|
+
results = list(oxigraph_adapter.query(query))
|
|
1125
|
+
|
|
1126
|
+
assert len(results) == 1
|
|
1127
|
+
assert str(results[0].label) == "Bonjour"
|
|
1128
|
+
|
|
1129
|
+
# Cleanup
|
|
1130
|
+
oxigraph_adapter.remove(g)
|
|
1131
|
+
|
|
1132
|
+
except requests.exceptions.ConnectionError:
|
|
1133
|
+
pytest.skip("Oxigraph is not running - skipping language tags test")
|
|
1134
|
+
|
|
1135
|
+
def test_property_paths(self, oxigraph_adapter):
|
|
1136
|
+
"""Test SPARQL property paths."""
|
|
1137
|
+
try:
|
|
1138
|
+
g = Graph()
|
|
1139
|
+
g.bind("test", "http://test.example.org/")
|
|
1140
|
+
|
|
1141
|
+
# Create a chain: A -> B -> C -> D
|
|
1142
|
+
for i in range(3):
|
|
1143
|
+
subject = URIRef(f"http://test.example.org/node{i}")
|
|
1144
|
+
obj = URIRef(f"http://test.example.org/node{i + 1}")
|
|
1145
|
+
g.add((subject, URIRef("http://test.example.org/next"), obj))
|
|
1146
|
+
|
|
1147
|
+
oxigraph_adapter.insert(g)
|
|
1148
|
+
|
|
1149
|
+
# Test property path with +
|
|
1150
|
+
query = """
|
|
1151
|
+
SELECT ?end WHERE {
|
|
1152
|
+
<http://test.example.org/node0> <http://test.example.org/next>+ ?end .
|
|
1153
|
+
}
|
|
1154
|
+
"""
|
|
1155
|
+
results = list(oxigraph_adapter.query(query))
|
|
1156
|
+
|
|
1157
|
+
assert len(results) == 3 # node1, node2, node3
|
|
1158
|
+
|
|
1159
|
+
# Cleanup
|
|
1160
|
+
oxigraph_adapter.remove(g)
|
|
1161
|
+
|
|
1162
|
+
except requests.exceptions.ConnectionError:
|
|
1163
|
+
pytest.skip("Oxigraph is not running - skipping property paths test")
|
|
1164
|
+
|
|
1165
|
+
def test_describe_query(self, oxigraph_adapter, integration_graph):
|
|
1166
|
+
"""Test DESCRIBE query."""
|
|
1167
|
+
try:
|
|
1168
|
+
oxigraph_adapter.insert(integration_graph)
|
|
1169
|
+
|
|
1170
|
+
query = """
|
|
1171
|
+
DESCRIBE <http://test.example.org/integration_test>
|
|
1172
|
+
"""
|
|
1173
|
+
result = oxigraph_adapter.query(query)
|
|
1174
|
+
|
|
1175
|
+
assert isinstance(result, Graph)
|
|
1176
|
+
assert len(result) > 0
|
|
1177
|
+
|
|
1178
|
+
# Cleanup
|
|
1179
|
+
oxigraph_adapter.remove(integration_graph)
|
|
1180
|
+
|
|
1181
|
+
except requests.exceptions.ConnectionError:
|
|
1182
|
+
pytest.skip("Oxigraph is not running - skipping DESCRIBE test")
|
|
1183
|
+
|
|
1184
|
+
def test_empty_graph_operations(self, oxigraph_adapter):
|
|
1185
|
+
"""Test operations with empty graphs."""
|
|
1186
|
+
try:
|
|
1187
|
+
empty_graph = Graph()
|
|
1188
|
+
|
|
1189
|
+
# Insert empty graph should not fail
|
|
1190
|
+
oxigraph_adapter.insert(empty_graph)
|
|
1191
|
+
|
|
1192
|
+
# Remove empty graph should not fail
|
|
1193
|
+
oxigraph_adapter.remove(empty_graph)
|
|
1194
|
+
|
|
1195
|
+
except requests.exceptions.ConnectionError:
|
|
1196
|
+
pytest.skip("Oxigraph is not running - skipping empty graph test")
|
|
1197
|
+
|
|
1198
|
+
def test_update_operations(self, oxigraph_adapter):
|
|
1199
|
+
"""Test SPARQL UPDATE operations."""
|
|
1200
|
+
try:
|
|
1201
|
+
g = Graph()
|
|
1202
|
+
g.bind("test", "http://test.example.org/")
|
|
1203
|
+
|
|
1204
|
+
subject = URIRef("http://test.example.org/updatetest")
|
|
1205
|
+
g.add(
|
|
1206
|
+
(subject, URIRef("http://test.example.org/value"), Literal("initial"))
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
oxigraph_adapter.insert(g)
|
|
1210
|
+
|
|
1211
|
+
# Update using SPARQL DELETE/INSERT
|
|
1212
|
+
update_query = """
|
|
1213
|
+
DELETE { <http://test.example.org/updatetest> <http://test.example.org/value> ?old }
|
|
1214
|
+
INSERT { <http://test.example.org/updatetest> <http://test.example.org/value> "updated" }
|
|
1215
|
+
WHERE { <http://test.example.org/updatetest> <http://test.example.org/value> ?old }
|
|
1216
|
+
"""
|
|
1217
|
+
oxigraph_adapter.query(update_query)
|
|
1218
|
+
|
|
1219
|
+
# Query to verify update
|
|
1220
|
+
query = """
|
|
1221
|
+
SELECT ?value WHERE {
|
|
1222
|
+
<http://test.example.org/updatetest> <http://test.example.org/value> ?value .
|
|
1223
|
+
}
|
|
1224
|
+
"""
|
|
1225
|
+
results = list(oxigraph_adapter.query(query))
|
|
1226
|
+
|
|
1227
|
+
assert len(results) == 1
|
|
1228
|
+
assert str(results[0].value) == "updated"
|
|
1229
|
+
|
|
1230
|
+
# Cleanup - remove all
|
|
1231
|
+
cleanup_graph = Graph()
|
|
1232
|
+
cleanup_graph.add(
|
|
1233
|
+
(subject, URIRef("http://test.example.org/value"), Literal("updated"))
|
|
1234
|
+
)
|
|
1235
|
+
oxigraph_adapter.remove(cleanup_graph)
|
|
1236
|
+
|
|
1237
|
+
except requests.exceptions.ConnectionError:
|
|
1238
|
+
pytest.skip("Oxigraph is not running - skipping UPDATE test")
|
|
1239
|
+
|
|
1240
|
+
def test_batch_insertions(self, oxigraph_adapter):
|
|
1241
|
+
"""Test multiple batch insertions."""
|
|
1242
|
+
try:
|
|
1243
|
+
from rdflib.term import Variable
|
|
1244
|
+
|
|
1245
|
+
graphs = []
|
|
1246
|
+
|
|
1247
|
+
# Create multiple graphs - use unique URI
|
|
1248
|
+
for batch in range(5):
|
|
1249
|
+
g = Graph()
|
|
1250
|
+
g.bind("test", "http://test.example.org/batches/")
|
|
1251
|
+
|
|
1252
|
+
for i in range(100):
|
|
1253
|
+
subject = URIRef(
|
|
1254
|
+
f"http://test.example.org/batches/batch{batch}/item{i}"
|
|
1255
|
+
)
|
|
1256
|
+
g.add(
|
|
1257
|
+
(
|
|
1258
|
+
subject,
|
|
1259
|
+
RDF.type,
|
|
1260
|
+
URIRef("http://test.example.org/batches/Item"),
|
|
1261
|
+
)
|
|
1262
|
+
)
|
|
1263
|
+
g.add(
|
|
1264
|
+
(
|
|
1265
|
+
subject,
|
|
1266
|
+
URIRef("http://test.example.org/batches/batch"),
|
|
1267
|
+
Literal(batch),
|
|
1268
|
+
)
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
graphs.append(g)
|
|
1272
|
+
oxigraph_adapter.insert(g)
|
|
1273
|
+
|
|
1274
|
+
# Verify total count
|
|
1275
|
+
count_query = """
|
|
1276
|
+
SELECT (COUNT(*) as ?count) WHERE {
|
|
1277
|
+
?s a <http://test.example.org/batches/Item> .
|
|
1278
|
+
}
|
|
1279
|
+
"""
|
|
1280
|
+
results = list(oxigraph_adapter.query(count_query))
|
|
1281
|
+
count_var = Variable("count")
|
|
1282
|
+
assert int(results[0][count_var]) == 500 # 5 batches * 100 items
|
|
1283
|
+
|
|
1284
|
+
# Cleanup
|
|
1285
|
+
for g in graphs:
|
|
1286
|
+
oxigraph_adapter.remove(g)
|
|
1287
|
+
|
|
1288
|
+
except requests.exceptions.ConnectionError:
|
|
1289
|
+
pytest.skip("Oxigraph is not running - skipping batch insertions test")
|
|
1290
|
+
|
|
1291
|
+
# @pytest.mark.integration
|
|
1292
|
+
# def test_full_workflow(self, oxigraph_adapter, integration_graph):
|
|
1293
|
+
# """Test complete workflow: insert, query, get subject, remove."""
|
|
1294
|
+
# try:
|
|
1295
|
+
# # Insert test data
|
|
1296
|
+
# oxigraph_adapter.insert(integration_graph)
|
|
1297
|
+
|
|
1298
|
+
# # Query for the data
|
|
1299
|
+
# query = """
|
|
1300
|
+
# SELECT ?s ?p ?o WHERE {
|
|
1301
|
+
# ?s <http://test.example.org/testProperty> ?o
|
|
1302
|
+
# }
|
|
1303
|
+
# """
|
|
1304
|
+
# results = list(oxigraph_adapter.query(query))
|
|
1305
|
+
# assert len(results) == 1
|
|
1306
|
+
|
|
1307
|
+
# # Get subject graph
|
|
1308
|
+
# subject = URIRef("http://test.example.org/integration_test")
|
|
1309
|
+
# subject_graph = oxigraph_adapter.get_subject_graph(subject)
|
|
1310
|
+
# assert len(subject_graph) == 2 # type and testProperty
|
|
1311
|
+
|
|
1312
|
+
# # Remove the data
|
|
1313
|
+
# oxigraph_adapter.remove(integration_graph)
|
|
1314
|
+
|
|
1315
|
+
# # Verify removal
|
|
1316
|
+
# results_after_remove = list(oxigraph_adapter.query(query))
|
|
1317
|
+
# assert len(results_after_remove) == 0
|
|
1318
|
+
|
|
1319
|
+
# except requests.exceptions.ConnectionError:
|
|
1320
|
+
# pytest.skip("Oxigraph is not running - skipping integration test")
|
|
1321
|
+
|
|
1322
|
+
# @pytest.mark.integration
|
|
1323
|
+
# def test_large_dataset_performance(self, oxigraph_adapter):
|
|
1324
|
+
# """Test performance with larger datasets."""
|
|
1325
|
+
# try:
|
|
1326
|
+
# # Create a larger test dataset
|
|
1327
|
+
# large_graph = Graph()
|
|
1328
|
+
# large_graph.bind("perf", "http://performance.test.org/")
|
|
1329
|
+
|
|
1330
|
+
# # Add 1000 triples
|
|
1331
|
+
# for i in range(1000):
|
|
1332
|
+
# subject = URIRef(f"http://performance.test.org/entity{i}")
|
|
1333
|
+
# large_graph.add((subject, RDF.type, URIRef("http://performance.test.org/Entity")))
|
|
1334
|
+
# large_graph.add((subject, URIRef("http://performance.test.org/id"), Literal(i)))
|
|
1335
|
+
|
|
1336
|
+
# # Measure insert time
|
|
1337
|
+
# start_time = time.time()
|
|
1338
|
+
# oxigraph_adapter.insert(large_graph)
|
|
1339
|
+
# insert_time = time.time() - start_time
|
|
1340
|
+
|
|
1341
|
+
# # Insert should complete in reasonable time (Oxigraph is fast!)
|
|
1342
|
+
# assert insert_time < 5.0, f"Insert took too long: {insert_time}s"
|
|
1343
|
+
|
|
1344
|
+
# # Test query performance
|
|
1345
|
+
# start_time = time.time()
|
|
1346
|
+
# results = list(oxigraph_adapter.query("SELECT ?s WHERE { ?s a <http://performance.test.org/Entity> }"))
|
|
1347
|
+
# query_time = time.time() - start_time
|
|
1348
|
+
|
|
1349
|
+
# assert len(results) == 1000
|
|
1350
|
+
# assert query_time < 2.0, f"Query took too long: {query_time}s"
|
|
1351
|
+
|
|
1352
|
+
# # Test COUNT query performance
|
|
1353
|
+
# start_time = time.time()
|
|
1354
|
+
# count_results = list(oxigraph_adapter.query("SELECT (COUNT(*) as ?count) WHERE { ?s a <http://performance.test.org/Entity> }"))
|
|
1355
|
+
# count_time = time.time() - start_time
|
|
1356
|
+
|
|
1357
|
+
# assert len(count_results) == 1
|
|
1358
|
+
# assert int(count_results[0].count) == 1000
|
|
1359
|
+
# assert count_time < 1.0, f"Count query took too long: {count_time}s"
|
|
1360
|
+
|
|
1361
|
+
# # Clean up
|
|
1362
|
+
# oxigraph_adapter.remove(large_graph)
|
|
1363
|
+
|
|
1364
|
+
# except requests.exceptions.ConnectionError:
|
|
1365
|
+
# pytest.skip("Oxigraph is not running - skipping performance test")
|
|
1366
|
+
|
|
1367
|
+
# @pytest.mark.integration
|
|
1368
|
+
# def test_concurrent_operations(self, oxigraph_adapter):
|
|
1369
|
+
# """Test concurrent insert and query operations."""
|
|
1370
|
+
# try:
|
|
1371
|
+
# import concurrent.futures
|
|
1372
|
+
|
|
1373
|
+
# def insert_data(thread_id):
|
|
1374
|
+
# """Insert data in a separate thread."""
|
|
1375
|
+
# graph = Graph()
|
|
1376
|
+
# for i in range(100):
|
|
1377
|
+
# subject = URIRef(f"http://concurrent.test.org/thread{thread_id}/entity{i}")
|
|
1378
|
+
# graph.add((subject, RDF.type, URIRef("http://concurrent.test.org/Entity")))
|
|
1379
|
+
# graph.add((subject, URIRef("http://concurrent.test.org/threadId"), Literal(thread_id)))
|
|
1380
|
+
# oxigraph_adapter.insert(graph)
|
|
1381
|
+
# return thread_id
|
|
1382
|
+
|
|
1383
|
+
# # Run multiple insert operations concurrently
|
|
1384
|
+
# with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
|
|
1385
|
+
# futures = [executor.submit(insert_data, i) for i in range(3)]
|
|
1386
|
+
# results = [future.result() for future in concurrent.futures.as_completed(futures)]
|
|
1387
|
+
|
|
1388
|
+
# assert len(results) == 3
|
|
1389
|
+
|
|
1390
|
+
# # Verify all data was inserted correctly
|
|
1391
|
+
# total_results = list(oxigraph_adapter.query(
|
|
1392
|
+
# "SELECT ?s WHERE { ?s a <http://concurrent.test.org/Entity> }"
|
|
1393
|
+
# ))
|
|
1394
|
+
# assert len(total_results) == 300 # 3 threads × 100 entities each
|
|
1395
|
+
|
|
1396
|
+
# # Clean up
|
|
1397
|
+
# cleanup_graph = Graph()
|
|
1398
|
+
# for result in total_results:
|
|
1399
|
+
# subject_graph = oxigraph_adapter.get_subject_graph(result.s)
|
|
1400
|
+
# cleanup_graph += subject_graph
|
|
1401
|
+
# oxigraph_adapter.remove(cleanup_graph)
|
|
1402
|
+
|
|
1403
|
+
# except requests.exceptions.ConnectionError:
|
|
1404
|
+
# pytest.skip("Oxigraph is not running - skipping concurrent test")
|
|
1405
|
+
|
|
1406
|
+
# @pytest.mark.integration
|
|
1407
|
+
# def test_complex_sparql_queries(self, oxigraph_adapter):
|
|
1408
|
+
# """Test complex SPARQL queries with various features."""
|
|
1409
|
+
# try:
|
|
1410
|
+
# # Setup test data
|
|
1411
|
+
# test_graph = Graph()
|
|
1412
|
+
# test_graph.bind("test", "http://complex.test.org/")
|
|
1413
|
+
|
|
1414
|
+
# # Create hierarchical data
|
|
1415
|
+
# for i in range(5):
|
|
1416
|
+
# person = URIRef(f"http://complex.test.org/person{i}")
|
|
1417
|
+
# test_graph.add((person, RDF.type, URIRef("http://complex.test.org/Person")))
|
|
1418
|
+
# test_graph.add((person, URIRef("http://complex.test.org/name"), Literal(f"Person {i}")))
|
|
1419
|
+
# test_graph.add((person, URIRef("http://complex.test.org/age"), Literal(20 + i)))
|
|
1420
|
+
|
|
1421
|
+
# if i > 0:
|
|
1422
|
+
# friend = URIRef(f"http://complex.test.org/person{i-1}")
|
|
1423
|
+
# test_graph.add((person, URIRef("http://complex.test.org/knows"), friend))
|
|
1424
|
+
|
|
1425
|
+
# oxigraph_adapter.insert(test_graph)
|
|
1426
|
+
|
|
1427
|
+
# # Test FILTER query
|
|
1428
|
+
# filter_results = list(oxigraph_adapter.query("""
|
|
1429
|
+
# PREFIX test: <http://complex.test.org/>
|
|
1430
|
+
# SELECT ?person ?age WHERE {
|
|
1431
|
+
# ?person a test:Person ;
|
|
1432
|
+
# test:age ?age .
|
|
1433
|
+
# FILTER(?age > 22)
|
|
1434
|
+
# }
|
|
1435
|
+
# ORDER BY ?age
|
|
1436
|
+
# """))
|
|
1437
|
+
|
|
1438
|
+
# assert len(filter_results) == 2 # persons with age > 22
|
|
1439
|
+
|
|
1440
|
+
# # Test OPTIONAL query
|
|
1441
|
+
# optional_results = list(oxigraph_adapter.query("""
|
|
1442
|
+
# PREFIX test: <http://complex.test.org/>
|
|
1443
|
+
# SELECT ?person ?name ?friend WHERE {
|
|
1444
|
+
# ?person a test:Person ;
|
|
1445
|
+
# test:name ?name .
|
|
1446
|
+
# OPTIONAL { ?person test:knows ?friend }
|
|
1447
|
+
# }
|
|
1448
|
+
# ORDER BY ?name
|
|
1449
|
+
# """))
|
|
1450
|
+
|
|
1451
|
+
# assert len(optional_results) == 5 # All persons
|
|
1452
|
+
|
|
1453
|
+
# # Test GROUP BY and COUNT
|
|
1454
|
+
# group_results = list(oxigraph_adapter.query("""
|
|
1455
|
+
# PREFIX test: <http://complex.test.org/>
|
|
1456
|
+
# SELECT (COUNT(?friend) as ?friendCount) WHERE {
|
|
1457
|
+
# ?person a test:Person .
|
|
1458
|
+
# OPTIONAL { ?person test:knows ?friend }
|
|
1459
|
+
# }
|
|
1460
|
+
# """))
|
|
1461
|
+
|
|
1462
|
+
# assert len(group_results) == 1
|
|
1463
|
+
# assert int(group_results[0].friendCount) == 4 # Total friendship connections
|
|
1464
|
+
|
|
1465
|
+
# # Clean up
|
|
1466
|
+
# oxigraph_adapter.remove(test_graph)
|
|
1467
|
+
|
|
1468
|
+
# except requests.exceptions.ConnectionError:
|
|
1469
|
+
# pytest.skip("Oxigraph is not running - skipping complex query test")
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
if __name__ == "__main__":
|
|
1473
|
+
# Run basic tests
|
|
1474
|
+
pytest.main([__file__, "-v"])
|