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,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")
|