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.
Files changed (124) hide show
  1. assets/favicon.ico +0 -0
  2. assets/logo.png +0 -0
  3. naas_abi_core/__init__.py +1 -0
  4. naas_abi_core/apps/api/api.py +245 -0
  5. naas_abi_core/apps/api/api_test.py +281 -0
  6. naas_abi_core/apps/api/openapi_doc.py +144 -0
  7. naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
  8. naas_abi_core/apps/mcp/mcp_server.py +243 -0
  9. naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
  10. naas_abi_core/apps/terminal_agent/main.py +555 -0
  11. naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
  12. naas_abi_core/engine/Engine.py +87 -0
  13. naas_abi_core/engine/EngineProxy.py +109 -0
  14. naas_abi_core/engine/Engine_test.py +6 -0
  15. naas_abi_core/engine/IEngine.py +91 -0
  16. naas_abi_core/engine/conftest.py +45 -0
  17. naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
  18. naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
  19. naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
  20. naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
  21. naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
  22. naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
  23. naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
  24. naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
  25. naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
  26. naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
  27. naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
  28. naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
  29. naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
  30. naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
  31. naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
  32. naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
  33. naas_abi_core/integration/__init__.py +7 -0
  34. naas_abi_core/integration/integration.py +28 -0
  35. naas_abi_core/models/Model.py +198 -0
  36. naas_abi_core/models/OpenRouter.py +18 -0
  37. naas_abi_core/models/OpenRouter_test.py +36 -0
  38. naas_abi_core/module/Module.py +252 -0
  39. naas_abi_core/module/ModuleAgentLoader.py +50 -0
  40. naas_abi_core/module/ModuleUtils.py +20 -0
  41. naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
  42. naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
  43. naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
  44. naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
  45. naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
  46. naas_abi_core/pipeline/__init__.py +6 -0
  47. naas_abi_core/pipeline/pipeline.py +70 -0
  48. naas_abi_core/services/__init__.py +0 -0
  49. naas_abi_core/services/agent/Agent.py +1619 -0
  50. naas_abi_core/services/agent/AgentMemory_test.py +28 -0
  51. naas_abi_core/services/agent/Agent_test.py +214 -0
  52. naas_abi_core/services/agent/IntentAgent.py +1179 -0
  53. naas_abi_core/services/agent/IntentAgent_test.py +139 -0
  54. naas_abi_core/services/agent/beta/Embeddings.py +181 -0
  55. naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
  56. naas_abi_core/services/agent/beta/LocalModel.py +88 -0
  57. naas_abi_core/services/agent/beta/VectorStore.py +89 -0
  58. naas_abi_core/services/agent/test_agent_memory.py +278 -0
  59. naas_abi_core/services/agent/test_postgres_integration.py +145 -0
  60. naas_abi_core/services/cache/CacheFactory.py +31 -0
  61. naas_abi_core/services/cache/CachePort.py +63 -0
  62. naas_abi_core/services/cache/CacheService.py +246 -0
  63. naas_abi_core/services/cache/CacheService_test.py +85 -0
  64. naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
  65. naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
  66. naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
  67. naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
  68. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
  69. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
  70. naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
  71. naas_abi_core/services/ontology/OntologyPorts.py +36 -0
  72. naas_abi_core/services/ontology/OntologyService.py +17 -0
  73. naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
  74. naas_abi_core/services/secret/Secret.py +138 -0
  75. naas_abi_core/services/secret/SecretPorts.py +45 -0
  76. naas_abi_core/services/secret/Secret_test.py +65 -0
  77. naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
  78. naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
  79. naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
  80. naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
  81. naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
  82. naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
  83. naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
  84. naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
  85. naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
  86. naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
  87. naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
  88. naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
  89. naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
  90. naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
  91. naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
  92. naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
  93. naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
  94. naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
  95. naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
  96. naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
  97. naas_abi_core/services/vector_store/__init__.py +13 -0
  98. naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
  99. naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
  100. naas_abi_core/tests/test_services_imports.py +69 -0
  101. naas_abi_core/utils/Expose.py +55 -0
  102. naas_abi_core/utils/Graph.py +182 -0
  103. naas_abi_core/utils/JSON.py +49 -0
  104. naas_abi_core/utils/LazyLoader.py +44 -0
  105. naas_abi_core/utils/Logger.py +12 -0
  106. naas_abi_core/utils/OntologyReasoner.py +141 -0
  107. naas_abi_core/utils/OntologyYaml.py +681 -0
  108. naas_abi_core/utils/SPARQL.py +256 -0
  109. naas_abi_core/utils/Storage.py +33 -0
  110. naas_abi_core/utils/StorageUtils.py +398 -0
  111. naas_abi_core/utils/String.py +52 -0
  112. naas_abi_core/utils/Workers.py +114 -0
  113. naas_abi_core/utils/__init__.py +0 -0
  114. naas_abi_core/utils/onto2py/README.md +0 -0
  115. naas_abi_core/utils/onto2py/__init__.py +10 -0
  116. naas_abi_core/utils/onto2py/__main__.py +29 -0
  117. naas_abi_core/utils/onto2py/onto2py.py +611 -0
  118. naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
  119. naas_abi_core/workflow/__init__.py +5 -0
  120. naas_abi_core/workflow/workflow.py +48 -0
  121. naas_abi_core-1.4.1.dist-info/METADATA +630 -0
  122. naas_abi_core-1.4.1.dist-info/RECORD +124 -0
  123. naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
  124. 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"])