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,597 @@
1
+ """
2
+ Oxigraph Triple Store Adapter
3
+
4
+ This module provides an adapter for connecting to Oxigraph graph database instances,
5
+ enabling lightweight, high-performance RDF triple storage and SPARQL querying capabilities.
6
+
7
+ Features:
8
+ - Direct HTTP/REST API connection to Oxigraph
9
+ - Full SPARQL 1.1 query and update operations
10
+ - Named graph support
11
+ - RDFLib integration for seamless graph operations
12
+ - Minimal resource footprint, ideal for development and Apple Silicon
13
+
14
+ Classes:
15
+ Oxigraph: Triple store adapter for Oxigraph instances
16
+
17
+ Example:
18
+ >>> oxigraph = Oxigraph(
19
+ ... oxigraph_url="http://localhost:7878"
20
+ ... )
21
+ >>>
22
+ >>> # Insert RDF triples
23
+ >>> graph = Graph()
24
+ >>> graph.add((URIRef("http://example.org/person1"),
25
+ ... RDF.type,
26
+ ... URIRef("http://example.org/Person")))
27
+ >>> oxigraph.insert(graph)
28
+ >>>
29
+ >>> # Query the data
30
+ >>> results = oxigraph.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
31
+
32
+ Author: ABI Project
33
+ License: MIT
34
+ """
35
+
36
+ import logging
37
+ from typing import Tuple, Union
38
+
39
+ import rdflib
40
+ import requests
41
+ from naas_abi_core.services.triple_store.TripleStorePorts import (
42
+ ITripleStorePort,
43
+ OntologyEvent,
44
+ )
45
+ from rdflib import BNode, Graph, URIRef
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ class Oxigraph(ITripleStorePort):
51
+ """
52
+ Oxigraph Triple Store Adapter
53
+
54
+ This adapter provides a connection to Oxigraph graph database instances,
55
+ enabling storage and querying of RDF triples using SPARQL. It implements
56
+ the ITripleStorePort interface for seamless integration with the triple
57
+ store service.
58
+
59
+ The adapter handles:
60
+ - HTTP REST API communication with Oxigraph
61
+ - SPARQL query and update operations
62
+ - RDFLib Graph integration
63
+ - Content negotiation for different RDF formats
64
+
65
+ Attributes:
66
+ oxigraph_url (str): Base URL of the Oxigraph instance
67
+ query_endpoint (str): SPARQL query endpoint URL
68
+ update_endpoint (str): SPARQL update endpoint URL
69
+ store_endpoint (str): RDF store endpoint URL
70
+ timeout (int): HTTP request timeout in seconds
71
+
72
+ Example:
73
+ >>> oxigraph = Oxigraph(
74
+ ... oxigraph_url="http://localhost:7878"
75
+ ... )
76
+ >>>
77
+ >>> # Create and insert triples
78
+ >>> from rdflib import Graph, URIRef, RDF, Literal
79
+ >>> g = Graph()
80
+ >>> g.add((URIRef("http://example.org/alice"),
81
+ ... RDF.type,
82
+ ... URIRef("http://example.org/Person")))
83
+ >>> g.add((URIRef("http://example.org/alice"),
84
+ ... URIRef("http://example.org/name"),
85
+ ... Literal("Alice")))
86
+ >>> oxigraph.insert(g)
87
+ >>>
88
+ >>> # Query the data
89
+ >>> result = oxigraph.query('''
90
+ ... SELECT ?person ?name WHERE {
91
+ ... ?person a <http://example.org/Person> .
92
+ ... ?person <http://example.org/name> ?name .
93
+ ... }
94
+ ... ''')
95
+ >>> for row in result:
96
+ ... print(f"Person: {row.person}, Name: {row.name}")
97
+ """
98
+
99
+ def __init__(self, oxigraph_url: str = "http://localhost:7878", timeout: int = 60):
100
+ """
101
+ Initialize Oxigraph adapter.
102
+
103
+ Args:
104
+ oxigraph_url (str): Base URL of the Oxigraph instance.
105
+ Defaults to "http://localhost:7878"
106
+ timeout (int): Request timeout in seconds. Defaults to 60
107
+
108
+ Raises:
109
+ requests.exceptions.ConnectionError: If Oxigraph is not accessible
110
+ """
111
+ self.oxigraph_url = oxigraph_url.rstrip("/")
112
+ self.query_endpoint = f"{self.oxigraph_url}/query"
113
+ self.update_endpoint = f"{self.oxigraph_url}/update"
114
+ self.store_endpoint = f"{self.oxigraph_url}/store"
115
+ self.timeout = timeout
116
+
117
+ # Test connection
118
+ self._test_connection()
119
+
120
+ logger.info(f"Oxigraph adapter initialized with endpoint: {self.oxigraph_url}")
121
+
122
+ def _test_connection(self):
123
+ """
124
+ Test if Oxigraph is accessible.
125
+
126
+ Raises:
127
+ requests.exceptions.ConnectionError: If Oxigraph is not accessible
128
+ """
129
+ try:
130
+ response = requests.get(
131
+ self.query_endpoint,
132
+ params={"query": "SELECT * WHERE { ?s ?p ?o } LIMIT 1"},
133
+ timeout=self.timeout,
134
+ )
135
+ response.raise_for_status()
136
+ except Exception as e:
137
+ logger.error(f"Failed to connect to Oxigraph at {self.oxigraph_url}: {e}")
138
+ raise
139
+
140
+ def __remove_blank_nodes(self, triples: Graph) -> Graph:
141
+ """
142
+ Sanitize a graph by removing blank nodes.
143
+
144
+ Args:
145
+ triples (Graph): RDFLib Graph to sanitize
146
+ """
147
+ clean_graph = Graph()
148
+ for s, p, o in triples:
149
+ if (
150
+ not isinstance(s, BNode)
151
+ and not isinstance(p, BNode)
152
+ and not isinstance(o, BNode)
153
+ ):
154
+ clean_graph.add((s, p, o))
155
+
156
+ for prefix, namespace in triples.namespaces():
157
+ clean_graph.bind(prefix, namespace)
158
+
159
+ return clean_graph
160
+
161
+ def __insert_large_graph(self, triples: Graph):
162
+ """
163
+ Insert a large graph into Oxigraph.
164
+
165
+ Args:
166
+ triples (Graph): RDFLib Graph containing triples to insert
167
+ """
168
+ serialized = self.__remove_blank_nodes(triples).serialize(format="ntriples")
169
+ response = requests.post(
170
+ f"{self.store_endpoint}?default",
171
+ headers={"Content-Type": "application/n-triples"},
172
+ data=serialized.encode("utf-8"),
173
+ timeout=self.timeout,
174
+ )
175
+ response.raise_for_status()
176
+ logger.debug(f"Inserted {len(triples)} triples into Oxigraph")
177
+
178
+ def __remove_large_graph(self, triples: Graph):
179
+ """
180
+ Remove a large graph from Oxigraph.
181
+
182
+ Args:
183
+ triples (Graph): RDFLib Graph containing triples to remove
184
+ """
185
+ # Serialize as N-Triples
186
+ serialized = self.__remove_blank_nodes(triples).serialize(format="ntriples")
187
+ # Use the store endpoint with DELETE method to remove all these triples
188
+ response = requests.delete(
189
+ f"{self.store_endpoint}?default",
190
+ headers={"Content-Type": "application/n-triples"},
191
+ data=serialized,
192
+ timeout=self.timeout,
193
+ )
194
+ response.raise_for_status()
195
+ logger.debug(
196
+ f"Removed {len(triples)} triples from Oxigraph via store endpoint (large remove)"
197
+ )
198
+
199
+ def insert(self, triples: Graph):
200
+ """
201
+ Insert RDF triples into Oxigraph.
202
+
203
+ This method converts an RDFLib Graph into SPARQL INSERT DATA queries
204
+ and sends them to Oxigraph via the update endpoint. Batching is
205
+ performed based on the serialized UTF-8 byte size of the SPARQL
206
+ payload, targeting batches up to `chunk_size` bytes.
207
+
208
+ Args:
209
+ triples (Graph): RDFLib Graph containing triples to insert
210
+ chunk_size (int): Maximum payload size per request in bytes. Defaults to 1,000,000
211
+
212
+ Raises:
213
+ requests.exceptions.HTTPError: If the insert operation fails
214
+ requests.exceptions.Timeout: If the request times out
215
+ """
216
+ if len(triples) == 0:
217
+ return
218
+ elif len(triples) > 100000:
219
+ self.__insert_large_graph(triples)
220
+ else:
221
+ # Build INSERT DATA query
222
+ insert_query = "INSERT DATA {\n"
223
+ for s, p, o in triples:
224
+ if isinstance(s, BNode) or isinstance(p, BNode) or isinstance(o, BNode):
225
+ continue
226
+ insert_query += f" {s.n3()} {p.n3()} {o.n3()} .\n"
227
+ insert_query += "}"
228
+
229
+ response = requests.post(
230
+ self.update_endpoint,
231
+ headers={"Content-Type": "application/sparql-update"},
232
+ data=insert_query.encode("utf-8"),
233
+ timeout=self.timeout,
234
+ )
235
+
236
+ if response.status_code == 413:
237
+ self.__insert_large_graph(triples)
238
+ else:
239
+ response.raise_for_status()
240
+ logger.debug(f"Inserted {len(triples)} triples into Oxigraph")
241
+
242
+ def remove(self, triples: Graph, chunk_size: int = 1_000_000):
243
+ """
244
+ Remove RDF triples from Oxigraph.
245
+
246
+ This method constructs SPARQL DELETE DATA queries from the provided
247
+ graph and executes them against Oxigraph. For large graphs, it uses
248
+ the Oxigraph store endpoint with the DELETE method, similarly to how
249
+ insert handles large graphs.
250
+
251
+ Args:
252
+ triples (Graph): RDFLib Graph containing triples to remove
253
+ chunk_size (int): Maximum payload size per request in bytes. Defaults to 1,000,000
254
+
255
+ Raises:
256
+ requests.exceptions.HTTPError: If the remove operation fails
257
+ """
258
+ if len(triples) == 0:
259
+ return
260
+ elif len(triples) > 100000:
261
+ self.__remove_large_graph(triples)
262
+ else:
263
+ # Build DELETE DATA query
264
+ delete_query = "DELETE DATA {\n"
265
+ for s, p, o in triples:
266
+ if isinstance(s, BNode) or isinstance(p, BNode) or isinstance(o, BNode):
267
+ continue
268
+ delete_query += f" {s.n3()} {p.n3()} {o.n3()} .\n"
269
+ delete_query += "}"
270
+
271
+ response = requests.post(
272
+ self.update_endpoint,
273
+ headers={"Content-Type": "application/sparql-update"},
274
+ data=delete_query.encode("utf-8"),
275
+ timeout=self.timeout,
276
+ )
277
+ if response.status_code == 413:
278
+ self.__insert_large_graph(triples)
279
+ else:
280
+ response.raise_for_status()
281
+ logger.debug(f"Inserted {len(triples)} triples into Oxigraph")
282
+
283
+ def get(self) -> Graph:
284
+ """
285
+ Retrieve all triples from Oxigraph as an RDFLib Graph.
286
+
287
+ Warning:
288
+ This operation can be expensive for large datasets as it retrieves
289
+ ALL triples from the database. Consider using query() with specific
290
+ patterns for better performance.
291
+
292
+ Returns:
293
+ Graph: RDFLib Graph containing all triples from Oxigraph
294
+
295
+ Raises:
296
+ requests.exceptions.HTTPError: If the query fails
297
+ """
298
+ response = requests.get(
299
+ self.store_endpoint, headers={"Accept": "text/turtle"}, timeout=self.timeout
300
+ )
301
+
302
+ response.raise_for_status()
303
+
304
+ graph = Graph()
305
+ graph.parse(data=response.text, format="turtle")
306
+ return graph
307
+
308
+ def handle_view_event(
309
+ self,
310
+ view: Tuple[URIRef | None, URIRef | None, URIRef | None],
311
+ event: OntologyEvent,
312
+ triple: Tuple[URIRef | None, URIRef | None, URIRef | None],
313
+ ):
314
+ """
315
+ Handle ontology change events for views.
316
+
317
+ Note:
318
+ This method is part of the ITripleStorePort interface but is
319
+ currently not implemented for Oxigraph. Override this method
320
+ in a subclass if you need custom event handling.
321
+
322
+ Args:
323
+ view: View pattern (subject, predicate, object)
324
+ event: Type of event (INSERT or DELETE)
325
+ triple: The actual triple that triggered the event
326
+ """
327
+ pass
328
+
329
+ def query(self, query: str) -> rdflib.query.Result: # type: ignore
330
+ """
331
+ Execute a SPARQL query against Oxigraph.
332
+
333
+ This method submits SPARQL queries (SELECT, CONSTRUCT, ASK, DESCRIBE)
334
+ or updates (INSERT, DELETE, UPDATE) to Oxigraph and returns the results.
335
+
336
+ Args:
337
+ query (str): SPARQL query string
338
+
339
+ Returns:
340
+ rdflib.query.Result: Query results that can be iterated over
341
+
342
+ Raises:
343
+ requests.exceptions.HTTPError: If the query fails
344
+ ValueError: If query type cannot be determined
345
+
346
+ Example:
347
+ >>> # SELECT query
348
+ >>> result = oxigraph.query('''
349
+ ... SELECT ?person ?name WHERE {
350
+ ... ?person a <http://example.org/Person> .
351
+ ... ?person <http://example.org/name> ?name .
352
+ ... }
353
+ ... ''')
354
+ >>> for row in result:
355
+ ... print(f"Person: {row.person}, Name: {row.name}")
356
+ """
357
+ # Determine if this is a query or update
358
+ query_upper = query.strip().upper()
359
+ is_update = any(
360
+ query_upper.startswith(cmd)
361
+ for cmd in [
362
+ "INSERT",
363
+ "DELETE",
364
+ "CREATE",
365
+ "DROP",
366
+ "CLEAR",
367
+ "LOAD",
368
+ "COPY",
369
+ "MOVE",
370
+ "ADD",
371
+ ]
372
+ )
373
+
374
+ if is_update:
375
+ # SPARQL Update
376
+ response = requests.post(
377
+ self.update_endpoint,
378
+ headers={"Content-Type": "application/sparql-update"},
379
+ data=query.encode("utf-8"),
380
+ timeout=self.timeout,
381
+ )
382
+ else:
383
+ # SPARQL Query
384
+ response = requests.post(
385
+ self.query_endpoint,
386
+ headers={
387
+ "Content-Type": "application/sparql-query",
388
+ "Accept": "application/sparql-results+json,application/n-triples",
389
+ },
390
+ data=query.encode("utf-8"),
391
+ timeout=self.timeout,
392
+ )
393
+
394
+ response.raise_for_status()
395
+
396
+ if is_update:
397
+ # For updates, return an empty result
398
+ return rdflib.query.Result("SELECT")
399
+
400
+ # Parse the results based on content type
401
+ content_type = response.headers.get("Content-Type", "")
402
+
403
+ if "sparql-results" in content_type:
404
+ # SELECT or ASK query - parse JSON results
405
+ import json
406
+
407
+ result_data = json.loads(response.text)
408
+
409
+ # Create a result wrapper that's compatible with RDFLib's ResultRow
410
+ from rdflib.query import ResultRow
411
+ from rdflib.term import BNode, Literal, URIRef, Variable
412
+
413
+ # Extract variables
414
+ vars = result_data.get("head", {}).get("vars", [])
415
+ bindings = result_data.get("results", {}).get("bindings", [])
416
+
417
+ # Convert variable names to Variable objects
418
+ var_objects = [Variable(var) for var in vars]
419
+
420
+ # Convert bindings to result rows
421
+ results = []
422
+
423
+ for binding in bindings:
424
+ row_values = {}
425
+
426
+ for var in vars:
427
+ var_obj = Variable(var)
428
+
429
+ if var in binding:
430
+ binding_info = binding[var]
431
+ value_str = binding_info["value"]
432
+ binding_type = binding_info.get("type", "literal")
433
+
434
+ # Convert to appropriate RDFLib term
435
+ value: Union[URIRef, BNode, Literal, None]
436
+ if binding_type == "uri":
437
+ value = URIRef(value_str)
438
+ elif binding_type == "bnode":
439
+ value = BNode(value_str)
440
+ else: # literal
441
+ datatype = binding_info.get("datatype")
442
+ lang = binding_info.get("xml:lang")
443
+
444
+ if datatype:
445
+ # Handle numeric datatypes
446
+ if datatype in [
447
+ "http://www.w3.org/2001/XMLSchema#integer",
448
+ "http://www.w3.org/2001/XMLSchema#long",
449
+ ]:
450
+ try:
451
+ value = Literal(
452
+ int(value_str), datatype=URIRef(datatype)
453
+ )
454
+ except ValueError:
455
+ value = Literal(
456
+ value_str, datatype=URIRef(datatype)
457
+ )
458
+ else:
459
+ value = Literal(
460
+ value_str, datatype=URIRef(datatype)
461
+ )
462
+ elif lang:
463
+ value = Literal(value_str, lang=lang)
464
+ else:
465
+ value = Literal(value_str)
466
+
467
+ row_values[var_obj] = value
468
+ else:
469
+ row_values[var_obj] = None # type: ignore
470
+
471
+ # Create a ResultRow compatible object
472
+ row = ResultRow(row_values, var_objects)
473
+ results.append(row)
474
+
475
+ # Return an iterable result
476
+ return iter(results) # type: ignore
477
+ elif "n-triples" in content_type or "turtle" in content_type:
478
+ # CONSTRUCT or DESCRIBE query
479
+ graph = Graph()
480
+ format_type = "nt" if "n-triples" in content_type else "turtle"
481
+ graph.parse(data=response.text, format=format_type)
482
+ return graph # type: ignore
483
+ else:
484
+ raise ValueError(f"Unexpected content type: {content_type}")
485
+
486
+ def query_view(self, view: str, query: str) -> rdflib.query.Result: # type: ignore
487
+ """
488
+ Execute a SPARQL query against a specific view.
489
+
490
+ Note:
491
+ This implementation currently ignores the view parameter and
492
+ executes the query against the entire dataset. Override this
493
+ method if you need view-specific querying behavior.
494
+
495
+ Args:
496
+ view (str): View identifier (currently ignored)
497
+ query (str): SPARQL query string
498
+
499
+ Returns:
500
+ rdflib.query.Result: Query results
501
+ """
502
+ return self.query(query)
503
+
504
+ def get_subject_graph(self, subject: URIRef) -> Graph:
505
+ """
506
+ Get all triples for a specific subject as an RDFLib Graph.
507
+
508
+ Args:
509
+ subject (URIRef): The subject URI to get triples for
510
+
511
+ Returns:
512
+ Graph: RDFLib Graph containing all triples for the subject
513
+
514
+ Example:
515
+ >>> alice_uri = URIRef("http://example.org/alice")
516
+ >>> alice_graph = oxigraph.get_subject_graph(alice_uri)
517
+ >>> print(f"Alice has {len(alice_graph)} properties")
518
+ """
519
+ query = f"""
520
+ CONSTRUCT {{ <{str(subject)}> ?p ?o }}
521
+ WHERE {{ <{str(subject)}> ?p ?o }}
522
+ """
523
+
524
+ result = self.query(query)
525
+
526
+ if isinstance(result, Graph):
527
+ return result
528
+ else:
529
+ # If query returns non-graph result, create empty graph
530
+ return Graph()
531
+
532
+
533
+ if __name__ == "__main__":
534
+ """
535
+ Example usage and interactive testing for Oxigraph adapter.
536
+
537
+ Usage:
538
+ python Oxigraph.py
539
+ """
540
+ import os
541
+
542
+ from dotenv import load_dotenv
543
+
544
+ load_dotenv()
545
+
546
+ # Initialize Oxigraph adapter
547
+ oxigraph_url = os.getenv("OXIGRAPH_URL", "http://localhost:7878")
548
+
549
+ print(f"Connecting to Oxigraph at {oxigraph_url}")
550
+
551
+ try:
552
+ adapter = Oxigraph(oxigraph_url=oxigraph_url)
553
+ print("✓ Connected successfully")
554
+
555
+ # Test operations
556
+ print("\nTesting basic operations...")
557
+
558
+ # Create test data
559
+ test_graph = Graph()
560
+ test_subject = URIRef("http://example.org/test/person1")
561
+ test_graph.add(
562
+ (test_subject, rdflib.RDF.type, URIRef("http://example.org/Person"))
563
+ )
564
+ test_graph.add(
565
+ (
566
+ test_subject,
567
+ URIRef("http://example.org/name"),
568
+ rdflib.Literal("Test Person"),
569
+ )
570
+ )
571
+
572
+ # Insert
573
+ print("- Inserting test data...")
574
+ adapter.insert(test_graph)
575
+ print(" ✓ Insert successful")
576
+
577
+ # Query
578
+ print("- Querying data...")
579
+ result = adapter.query("SELECT (COUNT(*) as ?count) WHERE { ?s ?p ?o }")
580
+ for row in result: # type: ignore
581
+ print(f" Total triples: {row.count}") # type: ignore
582
+
583
+ # Get subject graph
584
+ print("- Getting subject graph...")
585
+ subject_graph = adapter.get_subject_graph(test_subject)
586
+ print(f" Subject has {len(subject_graph)} triples")
587
+
588
+ # Clean up
589
+ print("- Removing test data...")
590
+ adapter.remove(test_graph)
591
+ print(" ✓ Remove successful")
592
+
593
+ print("\n✓ All tests passed!")
594
+
595
+ except Exception as e:
596
+ print(f"✗ Error: {e}")
597
+ print("Make sure Oxigraph is running and accessible")