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,1300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AWS Neptune Triple Store Adapter
|
|
3
|
+
|
|
4
|
+
This module provides adapters for connecting to AWS Neptune graph database instances,
|
|
5
|
+
enabling high-performance RDF triple storage and SPARQL querying capabilities for
|
|
6
|
+
semantic data applications.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Direct connection to AWS Neptune instances
|
|
10
|
+
- SSH tunnel support for VPC-deployed Neptune instances
|
|
11
|
+
- AWS IAM authentication with SigV4 signing
|
|
12
|
+
- Named graph support and management
|
|
13
|
+
- Full SPARQL query and update operations
|
|
14
|
+
- RDFLib integration for seamless graph operations
|
|
15
|
+
|
|
16
|
+
Classes:
|
|
17
|
+
AWSNeptune: Basic Neptune adapter for direct connections
|
|
18
|
+
AWSNeptuneSSHTunnel: Neptune adapter with SSH tunnel support for VPC deployments
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
Basic Neptune connection:
|
|
22
|
+
|
|
23
|
+
>>> neptune = AWSNeptune(
|
|
24
|
+
... aws_region_name="us-east-1",
|
|
25
|
+
... aws_access_key_id="AKIA...",
|
|
26
|
+
... aws_secret_access_key="...",
|
|
27
|
+
... db_instance_identifier="my-neptune-instance"
|
|
28
|
+
... )
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Insert RDF triples
|
|
31
|
+
>>> graph = Graph()
|
|
32
|
+
>>> graph.add((URIRef("http://example.org/person1"),
|
|
33
|
+
... RDF.type,
|
|
34
|
+
... URIRef("http://example.org/Person")))
|
|
35
|
+
>>> neptune.insert(graph)
|
|
36
|
+
>>>
|
|
37
|
+
>>> # Query the data
|
|
38
|
+
>>> results = neptune.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
39
|
+
|
|
40
|
+
SSH tunnel connection for VPC-deployed Neptune:
|
|
41
|
+
|
|
42
|
+
>>> neptune_ssh = AWSNeptuneSSHTunnel(
|
|
43
|
+
... aws_region_name="us-east-1",
|
|
44
|
+
... aws_access_key_id="AKIA...",
|
|
45
|
+
... aws_secret_access_key="...",
|
|
46
|
+
... db_instance_identifier="my-vpc-neptune-instance",
|
|
47
|
+
... bastion_host="bastion.example.com",
|
|
48
|
+
... bastion_port=22,
|
|
49
|
+
... bastion_user="ubuntu",
|
|
50
|
+
... bastion_private_key="-----BEGIN RSA PRIVATE KEY-----\\n..."
|
|
51
|
+
... )
|
|
52
|
+
|
|
53
|
+
Dependencies:
|
|
54
|
+
- boto3: AWS SDK for Python
|
|
55
|
+
- botocore: Core AWS functionality
|
|
56
|
+
- requests: HTTP client for SPARQL endpoints
|
|
57
|
+
- rdflib: RDF graph processing
|
|
58
|
+
- paramiko: SSH client (for tunnel support)
|
|
59
|
+
- sshtunnel: SSH tunnel management (for tunnel support)
|
|
60
|
+
|
|
61
|
+
Author: Maxime Jublou <maxime@naas.ai>
|
|
62
|
+
License: MIT
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
import socket
|
|
66
|
+
import tempfile
|
|
67
|
+
from io import StringIO
|
|
68
|
+
from typing import TYPE_CHECKING, Any, Tuple, overload
|
|
69
|
+
|
|
70
|
+
import boto3
|
|
71
|
+
import botocore
|
|
72
|
+
import rdflib
|
|
73
|
+
import requests
|
|
74
|
+
from botocore.auth import SigV4Auth
|
|
75
|
+
from botocore.awsrequest import AWSRequest
|
|
76
|
+
from naas_abi_core.services.triple_store.TripleStorePorts import OntologyEvent
|
|
77
|
+
from naas_abi_core.services.triple_store.TripleStoreService import ITripleStorePort
|
|
78
|
+
from rdflib import Graph, URIRef
|
|
79
|
+
|
|
80
|
+
# Import SSH dependencies only when needed for type checking
|
|
81
|
+
if TYPE_CHECKING:
|
|
82
|
+
from sshtunnel import SSHTunnelForwarder
|
|
83
|
+
|
|
84
|
+
from enum import Enum
|
|
85
|
+
|
|
86
|
+
from rdflib.namespace import _NAMESPACE_PREFIXES_CORE, _NAMESPACE_PREFIXES_RDFLIB
|
|
87
|
+
from rdflib.plugins.sparql.results.rdfresults import RDFResultParser
|
|
88
|
+
from rdflib.plugins.sparql.results.xmlresults import XMLResultParser
|
|
89
|
+
from rdflib.query import ResultParser, ResultRow
|
|
90
|
+
from rdflib.term import Identifier
|
|
91
|
+
from SPARQLWrapper import SPARQLWrapper
|
|
92
|
+
|
|
93
|
+
ORIGINAL_GETADDRINFO = socket.getaddrinfo
|
|
94
|
+
|
|
95
|
+
NEPTUNE_DEFAULT_GRAPH_NAME: URIRef = URIRef(
|
|
96
|
+
"http://aws.amazon.com/neptune/vocab/v01/DefaultNamedGraph"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class QueryType(Enum):
|
|
101
|
+
"""SPARQL query types supported by Neptune."""
|
|
102
|
+
|
|
103
|
+
INSERT_DATA = "INSERT DATA"
|
|
104
|
+
DELETE_DATA = "DELETE DATA"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class QueryMode(Enum):
|
|
108
|
+
"""SPARQL operation modes for Neptune endpoint."""
|
|
109
|
+
|
|
110
|
+
QUERY = "query"
|
|
111
|
+
UPDATE = "update"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AWSNeptune(ITripleStorePort):
|
|
115
|
+
"""
|
|
116
|
+
AWS Neptune Triple Store Adapter
|
|
117
|
+
|
|
118
|
+
This adapter provides a connection to AWS Neptune graph database instances,
|
|
119
|
+
enabling storage and querying of RDF triples using SPARQL. It implements
|
|
120
|
+
the ITripleStorePort interface for seamless integration with the triple
|
|
121
|
+
store service.
|
|
122
|
+
|
|
123
|
+
The adapter handles:
|
|
124
|
+
- AWS IAM authentication with SigV4 signing
|
|
125
|
+
- Automatic Neptune endpoint discovery
|
|
126
|
+
- SPARQL query and update operations
|
|
127
|
+
- Named graph management
|
|
128
|
+
- RDFLib Graph integration
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
aws_region_name (str): AWS region where Neptune instance is deployed
|
|
132
|
+
aws_access_key_id (str): AWS access key for authentication
|
|
133
|
+
aws_secret_access_key (str): AWS secret key for authentication
|
|
134
|
+
db_instance_identifier (str): Neptune database instance identifier
|
|
135
|
+
neptune_sparql_endpoint (str): Auto-discovered Neptune SPARQL endpoint
|
|
136
|
+
neptune_port (int): Neptune instance port
|
|
137
|
+
neptune_sparql_url (str): Full SPARQL endpoint URL
|
|
138
|
+
default_graph_name (URIRef): Default named graph for operations
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> neptune = AWSNeptune(
|
|
142
|
+
... aws_region_name="us-east-1",
|
|
143
|
+
... aws_access_key_id="AKIA1234567890EXAMPLE",
|
|
144
|
+
... aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
145
|
+
... db_instance_identifier="my-neptune-db"
|
|
146
|
+
... )
|
|
147
|
+
>>>
|
|
148
|
+
>>> # Create and insert triples
|
|
149
|
+
>>> from rdflib import Graph, URIRef, RDF, Literal
|
|
150
|
+
>>> g = Graph()
|
|
151
|
+
>>> g.add((URIRef("http://example.org/alice"),
|
|
152
|
+
... RDF.type,
|
|
153
|
+
... URIRef("http://example.org/Person")))
|
|
154
|
+
>>> g.add((URIRef("http://example.org/alice"),
|
|
155
|
+
... URIRef("http://example.org/name"),
|
|
156
|
+
... Literal("Alice")))
|
|
157
|
+
>>> neptune.insert(g)
|
|
158
|
+
>>>
|
|
159
|
+
>>> # Query the data
|
|
160
|
+
>>> result = neptune.query('''
|
|
161
|
+
... SELECT ?person ?name WHERE {
|
|
162
|
+
... ?person a <http://example.org/Person> .
|
|
163
|
+
... ?person <http://example.org/name> ?name .
|
|
164
|
+
... }
|
|
165
|
+
... ''')
|
|
166
|
+
>>> for row in result:
|
|
167
|
+
... print(f"Person: {row.person}, Name: {row.name}")
|
|
168
|
+
>>>
|
|
169
|
+
>>> # Get all triples for a specific subject
|
|
170
|
+
>>> alice_graph = neptune.get_subject_graph(URIRef("http://example.org/alice"))
|
|
171
|
+
>>> print(f"Alice has {len(alice_graph)} triples")
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
aws_region_name: str
|
|
175
|
+
aws_access_key_id: str
|
|
176
|
+
aws_secret_access_key: str
|
|
177
|
+
db_instance_identifier: str
|
|
178
|
+
|
|
179
|
+
bastion_host: str
|
|
180
|
+
bastion_port: int
|
|
181
|
+
bastion_user: str
|
|
182
|
+
|
|
183
|
+
neptune_sparql_endpoint: str
|
|
184
|
+
neptune_port: int
|
|
185
|
+
neptune_sparql_url: str
|
|
186
|
+
|
|
187
|
+
credentials: botocore.credentials.Credentials
|
|
188
|
+
|
|
189
|
+
# SSH tunnel to the Bastion host
|
|
190
|
+
tunnel: "SSHTunnelForwarder"
|
|
191
|
+
|
|
192
|
+
default_graph_name: URIRef
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self,
|
|
196
|
+
aws_region_name: str,
|
|
197
|
+
aws_access_key_id: str,
|
|
198
|
+
aws_secret_access_key: str,
|
|
199
|
+
db_instance_identifier: str,
|
|
200
|
+
default_graph_name: URIRef = NEPTUNE_DEFAULT_GRAPH_NAME,
|
|
201
|
+
):
|
|
202
|
+
"""
|
|
203
|
+
Initialize AWS Neptune adapter.
|
|
204
|
+
|
|
205
|
+
This constructor establishes a connection to the specified Neptune instance
|
|
206
|
+
by discovering the endpoint through AWS APIs and setting up authentication.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
aws_region_name (str): AWS region name (e.g., 'us-east-1', 'eu-west-1')
|
|
210
|
+
aws_access_key_id (str): AWS access key ID for authentication
|
|
211
|
+
aws_secret_access_key (str): AWS secret access key for authentication
|
|
212
|
+
db_instance_identifier (str): Neptune database instance identifier
|
|
213
|
+
default_graph_name (URIRef, optional): Default named graph URI.
|
|
214
|
+
Defaults to Neptune's default graph.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
AssertionError: If any required parameter is not a string
|
|
218
|
+
botocore.exceptions.ClientError: If AWS credentials are invalid or
|
|
219
|
+
Neptune instance cannot be found
|
|
220
|
+
botocore.exceptions.NoCredentialsError: If AWS credentials are missing
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> neptune = AWSNeptune(
|
|
224
|
+
... aws_region_name="us-east-1",
|
|
225
|
+
... aws_access_key_id="AKIA1234567890EXAMPLE",
|
|
226
|
+
... aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
227
|
+
... db_instance_identifier="my-neptune-cluster"
|
|
228
|
+
... )
|
|
229
|
+
"""
|
|
230
|
+
assert isinstance(aws_region_name, str)
|
|
231
|
+
assert isinstance(aws_access_key_id, str)
|
|
232
|
+
assert isinstance(aws_secret_access_key, str)
|
|
233
|
+
assert isinstance(db_instance_identifier, str)
|
|
234
|
+
|
|
235
|
+
self.aws_region_name = aws_region_name
|
|
236
|
+
|
|
237
|
+
self.session = boto3.Session(
|
|
238
|
+
aws_access_key_id=aws_access_key_id,
|
|
239
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
240
|
+
region_name=aws_region_name,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
self.db_instance_identifier = db_instance_identifier
|
|
244
|
+
|
|
245
|
+
self.client = self.session.client("neptune", region_name=self.aws_region_name)
|
|
246
|
+
|
|
247
|
+
cluster_endpoints = self.client.describe_db_instances(
|
|
248
|
+
DBInstanceIdentifier=self.db_instance_identifier
|
|
249
|
+
)["DBInstances"]
|
|
250
|
+
self.neptune_sparql_endpoint = cluster_endpoints[0]["Endpoint"]["Address"]
|
|
251
|
+
|
|
252
|
+
self.neptune_port = cluster_endpoints[0]["Endpoint"]["Port"]
|
|
253
|
+
|
|
254
|
+
credentials = self.session.get_credentials()
|
|
255
|
+
if credentials is None:
|
|
256
|
+
raise ValueError("Failed to get credentials from session")
|
|
257
|
+
self.credentials = credentials
|
|
258
|
+
|
|
259
|
+
self.neptune_sparql_url = (
|
|
260
|
+
f"https://{self.neptune_sparql_endpoint}:{self.neptune_port}/sparql"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.default_graph_name = default_graph_name
|
|
264
|
+
|
|
265
|
+
def __get_signed_headers(
|
|
266
|
+
self,
|
|
267
|
+
method: str,
|
|
268
|
+
url: str,
|
|
269
|
+
data: Any | None = None,
|
|
270
|
+
params: Any | None = None,
|
|
271
|
+
headers: Any | None = None,
|
|
272
|
+
):
|
|
273
|
+
"""
|
|
274
|
+
Generate AWS SigV4 signed headers for Neptune authentication.
|
|
275
|
+
|
|
276
|
+
This method creates the necessary authentication headers for making
|
|
277
|
+
requests to AWS Neptune using AWS Identity and Access Management (IAM)
|
|
278
|
+
credentials and the SigV4 signing process.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
method (str): HTTP method (e.g., 'GET', 'POST')
|
|
282
|
+
url (str): The target URL for the request
|
|
283
|
+
data (Any, optional): Request body data
|
|
284
|
+
params (Any, optional): URL parameters
|
|
285
|
+
headers (Any, optional): Additional headers to include
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
dict: Dictionary containing signed headers for AWS authentication
|
|
289
|
+
|
|
290
|
+
Note:
|
|
291
|
+
This is an internal method used by submit_query() and should not
|
|
292
|
+
be called directly by users of the adapter.
|
|
293
|
+
"""
|
|
294
|
+
request = AWSRequest(
|
|
295
|
+
method=method, url=url, data=data, params=params, headers=headers
|
|
296
|
+
)
|
|
297
|
+
assert self.credentials is not None, (
|
|
298
|
+
"Credentials must be set during initialization"
|
|
299
|
+
)
|
|
300
|
+
SigV4Auth(
|
|
301
|
+
self.credentials, "neptune-db", region_name=self.aws_region_name
|
|
302
|
+
).add_auth(request)
|
|
303
|
+
return request.headers
|
|
304
|
+
|
|
305
|
+
def submit_query(self, data: Any, timeout: int = 60) -> requests.Response:
|
|
306
|
+
"""
|
|
307
|
+
Submit a SPARQL query or update to the Neptune endpoint.
|
|
308
|
+
|
|
309
|
+
This method handles the low-level communication with Neptune, including
|
|
310
|
+
authentication, proper headers, and error handling.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
data (Any): Query data containing either 'query' or 'update' key
|
|
314
|
+
with the SPARQL statement as the value
|
|
315
|
+
timeout (int, optional): Request timeout in seconds. Defaults to 60.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
requests.Response: HTTP response from Neptune endpoint
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
requests.exceptions.HTTPError: If the HTTP request fails
|
|
322
|
+
requests.exceptions.Timeout: If the request times out
|
|
323
|
+
requests.exceptions.ConnectionError: If connection fails
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
>>> # This is typically called internally by other methods
|
|
327
|
+
>>> response = neptune.submit_query({'query': 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }'})
|
|
328
|
+
>>> print(response.status_code) # Should be 200 for success
|
|
329
|
+
"""
|
|
330
|
+
headers = {}
|
|
331
|
+
headers["Accept"] = "application/sparql-results+xml"
|
|
332
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
333
|
+
|
|
334
|
+
headers = self.__get_signed_headers(
|
|
335
|
+
"POST",
|
|
336
|
+
self.neptune_sparql_url,
|
|
337
|
+
data=data,
|
|
338
|
+
headers=headers,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
response = requests.post(
|
|
342
|
+
self.neptune_sparql_url,
|
|
343
|
+
headers=headers,
|
|
344
|
+
timeout=timeout,
|
|
345
|
+
verify=True,
|
|
346
|
+
data=data,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
response.raise_for_status()
|
|
351
|
+
except requests.exceptions.HTTPError as e:
|
|
352
|
+
print(response.text)
|
|
353
|
+
raise e
|
|
354
|
+
|
|
355
|
+
return response
|
|
356
|
+
|
|
357
|
+
@overload
|
|
358
|
+
def insert(self, triples: Graph, graph_name: URIRef): ...
|
|
359
|
+
@overload
|
|
360
|
+
def insert(self, triples: Graph): ...
|
|
361
|
+
|
|
362
|
+
def insert(self, triples: Graph, graph_name: URIRef | None = None):
|
|
363
|
+
"""
|
|
364
|
+
Insert RDF triples into Neptune.
|
|
365
|
+
|
|
366
|
+
This method converts an RDFLib Graph into SPARQL INSERT DATA statements
|
|
367
|
+
and executes them against Neptune. The triples are inserted into the
|
|
368
|
+
specified named graph or the default graph if none is provided.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
triples (Graph): RDFLib Graph containing triples to insert
|
|
372
|
+
graph_name (URIRef, optional): Named graph URI to insert into.
|
|
373
|
+
If None, uses the default graph.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
requests.Response: HTTP response from Neptune
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
requests.exceptions.HTTPError: If the insert operation fails
|
|
380
|
+
|
|
381
|
+
Example:
|
|
382
|
+
>>> from rdflib import Graph, URIRef, RDF, Literal
|
|
383
|
+
>>> g = Graph()
|
|
384
|
+
>>> g.add((URIRef("http://example.org/alice"),
|
|
385
|
+
... RDF.type,
|
|
386
|
+
... URIRef("http://example.org/Person")))
|
|
387
|
+
>>> g.add((URIRef("http://example.org/alice"),
|
|
388
|
+
... URIRef("http://example.org/name"),
|
|
389
|
+
... Literal("Alice")))
|
|
390
|
+
>>>
|
|
391
|
+
>>> # Insert into default graph
|
|
392
|
+
>>> neptune.insert(g)
|
|
393
|
+
>>>
|
|
394
|
+
>>> # Insert into specific named graph
|
|
395
|
+
>>> custom_graph = URIRef("http://example.org/graph/people")
|
|
396
|
+
>>> neptune.insert(g, custom_graph)
|
|
397
|
+
"""
|
|
398
|
+
if graph_name is None:
|
|
399
|
+
graph_name = self.default_graph_name
|
|
400
|
+
|
|
401
|
+
query = self.graph_to_query(triples, QueryType.INSERT_DATA, graph_name)
|
|
402
|
+
|
|
403
|
+
response = self.submit_query({QueryMode.UPDATE.value: query})
|
|
404
|
+
return response
|
|
405
|
+
|
|
406
|
+
@overload
|
|
407
|
+
def remove(self, triples: Graph, graph_name: URIRef): ...
|
|
408
|
+
@overload
|
|
409
|
+
def remove(self, triples: Graph): ...
|
|
410
|
+
|
|
411
|
+
def remove(self, triples: Graph, graph_name: URIRef | None = None):
|
|
412
|
+
"""
|
|
413
|
+
Remove RDF triples from Neptune.
|
|
414
|
+
|
|
415
|
+
This method converts an RDFLib Graph into SPARQL DELETE DATA statements
|
|
416
|
+
and executes them against Neptune. Only exact matching triples will be
|
|
417
|
+
removed from the specified named graph.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
triples (Graph): RDFLib Graph containing triples to remove
|
|
421
|
+
graph_name (URIRef, optional): Named graph URI to remove from.
|
|
422
|
+
If None, uses the default graph.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
requests.Response: HTTP response from Neptune
|
|
426
|
+
|
|
427
|
+
Raises:
|
|
428
|
+
requests.exceptions.HTTPError: If the remove operation fails
|
|
429
|
+
|
|
430
|
+
Example:
|
|
431
|
+
>>> from rdflib import Graph, URIRef, RDF, Literal
|
|
432
|
+
>>> g = Graph()
|
|
433
|
+
>>> g.add((URIRef("http://example.org/alice"),
|
|
434
|
+
... URIRef("http://example.org/name"),
|
|
435
|
+
... Literal("Alice")))
|
|
436
|
+
>>>
|
|
437
|
+
>>> # Remove from default graph
|
|
438
|
+
>>> neptune.remove(g)
|
|
439
|
+
>>>
|
|
440
|
+
>>> # Remove from specific named graph
|
|
441
|
+
>>> custom_graph = URIRef("http://example.org/graph/people")
|
|
442
|
+
>>> neptune.remove(g, custom_graph)
|
|
443
|
+
"""
|
|
444
|
+
if graph_name is None:
|
|
445
|
+
graph_name = self.default_graph_name
|
|
446
|
+
query = self.graph_to_query(triples, QueryType.DELETE_DATA, graph_name)
|
|
447
|
+
response = self.submit_query({"update": query})
|
|
448
|
+
return response
|
|
449
|
+
|
|
450
|
+
def get(self) -> Graph:
|
|
451
|
+
"""
|
|
452
|
+
Retrieve all triples from Neptune as an RDFLib Graph.
|
|
453
|
+
|
|
454
|
+
This method executes a SPARQL SELECT query to fetch all triples
|
|
455
|
+
from Neptune and constructs an RDFLib Graph from the results.
|
|
456
|
+
|
|
457
|
+
Warning:
|
|
458
|
+
This operation can be expensive for large datasets as it retrieves
|
|
459
|
+
ALL triples from the database. Consider using query() with specific
|
|
460
|
+
patterns for better performance on large graphs.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Graph: RDFLib Graph containing all triples from Neptune
|
|
464
|
+
|
|
465
|
+
Raises:
|
|
466
|
+
requests.exceptions.HTTPError: If the query fails
|
|
467
|
+
|
|
468
|
+
Example:
|
|
469
|
+
>>> # Get all triples (use carefully with large datasets)
|
|
470
|
+
>>> all_triples = neptune.get()
|
|
471
|
+
>>> print(f"Total triples: {len(all_triples)}")
|
|
472
|
+
>>>
|
|
473
|
+
>>> # Iterate through triples
|
|
474
|
+
>>> for subject, predicate, obj in all_triples:
|
|
475
|
+
... print(f"{subject} {predicate} {obj}")
|
|
476
|
+
"""
|
|
477
|
+
response = self.submit_query(
|
|
478
|
+
{QueryMode.QUERY.value: "select ?s ?p ?o where {?s ?p ?o}"}
|
|
479
|
+
)
|
|
480
|
+
result = XMLResultParser().parse(StringIO(response.text))
|
|
481
|
+
|
|
482
|
+
graph = Graph()
|
|
483
|
+
for row in result:
|
|
484
|
+
assert isinstance(row, ResultRow)
|
|
485
|
+
|
|
486
|
+
s: Identifier | None = row.get("s")
|
|
487
|
+
p: Identifier | None = row.get("p")
|
|
488
|
+
o: Identifier | None = row.get("o")
|
|
489
|
+
|
|
490
|
+
assert s is not None and p is not None and o is not None
|
|
491
|
+
|
|
492
|
+
graph.add((s, p, o))
|
|
493
|
+
|
|
494
|
+
return graph
|
|
495
|
+
|
|
496
|
+
def handle_view_event(
|
|
497
|
+
self,
|
|
498
|
+
view: Tuple[URIRef | None, URIRef | None, URIRef | None],
|
|
499
|
+
event: OntologyEvent,
|
|
500
|
+
triple: Tuple[URIRef | None, URIRef | None, URIRef | None],
|
|
501
|
+
):
|
|
502
|
+
"""
|
|
503
|
+
Handle ontology change events for views.
|
|
504
|
+
|
|
505
|
+
This method is called when ontology events occur that match registered
|
|
506
|
+
view patterns. Currently, this is a no-op implementation that can be
|
|
507
|
+
extended for custom event handling.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
view (Tuple[URIRef | None, URIRef | None, URIRef | None]):
|
|
511
|
+
View pattern (subject, predicate, object) where None matches any
|
|
512
|
+
event (OntologyEvent): Type of event (INSERT or DELETE)
|
|
513
|
+
triple (Tuple[URIRef | None, URIRef | None, URIRef | None]):
|
|
514
|
+
The actual triple that triggered the event
|
|
515
|
+
|
|
516
|
+
Note:
|
|
517
|
+
This method is part of the ITripleStorePort interface but is
|
|
518
|
+
currently not implemented for Neptune. Override this method
|
|
519
|
+
in a subclass if you need custom event handling.
|
|
520
|
+
"""
|
|
521
|
+
pass
|
|
522
|
+
|
|
523
|
+
def query(
|
|
524
|
+
self, query: str, query_mode: QueryMode = QueryMode.QUERY
|
|
525
|
+
) -> rdflib.query.Result:
|
|
526
|
+
"""
|
|
527
|
+
Execute a SPARQL query against Neptune.
|
|
528
|
+
|
|
529
|
+
This method submits SPARQL queries (SELECT, CONSTRUCT, ASK, DESCRIBE)
|
|
530
|
+
or updates (INSERT, DELETE, UPDATE) to Neptune and returns the results.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
query (str): SPARQL query string
|
|
534
|
+
query_mode (QueryMode, optional): Whether this is a query or update
|
|
535
|
+
operation. Defaults to QueryMode.QUERY.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
rdflib.query.Result: Query results that can be iterated over
|
|
539
|
+
|
|
540
|
+
Raises:
|
|
541
|
+
requests.exceptions.HTTPError: If the query fails
|
|
542
|
+
Exception: If result parsing fails
|
|
543
|
+
|
|
544
|
+
Example:
|
|
545
|
+
>>> # SELECT query
|
|
546
|
+
>>> result = neptune.query('''
|
|
547
|
+
... SELECT ?person ?name WHERE {
|
|
548
|
+
... ?person a <http://example.org/Person> .
|
|
549
|
+
... ?person <http://example.org/name> ?name .
|
|
550
|
+
... }
|
|
551
|
+
... ''')
|
|
552
|
+
>>> for row in result:
|
|
553
|
+
... print(f"Person: {row.person}, Name: {row.name}")
|
|
554
|
+
>>>
|
|
555
|
+
>>> # CONSTRUCT query
|
|
556
|
+
>>> result = neptune.query('''
|
|
557
|
+
... CONSTRUCT { ?s ?p ?o }
|
|
558
|
+
... WHERE { ?s a <http://example.org/Person> . ?s ?p ?o }
|
|
559
|
+
... ''')
|
|
560
|
+
>>> print(f"Constructed graph has {len(result)} triples")
|
|
561
|
+
>>>
|
|
562
|
+
>>> # UPDATE operation
|
|
563
|
+
>>> neptune.query('''
|
|
564
|
+
... INSERT DATA {
|
|
565
|
+
... <http://example.org/bob> a <http://example.org/Person> .
|
|
566
|
+
... <http://example.org/bob> <http://example.org/name> "Bob" .
|
|
567
|
+
... }
|
|
568
|
+
... ''', QueryMode.UPDATE)
|
|
569
|
+
"""
|
|
570
|
+
response = self.submit_query({query_mode.value: query})
|
|
571
|
+
|
|
572
|
+
# Detect if SELECT, ASK or CONSTRUCT, DESCRIBE
|
|
573
|
+
sparql = SPARQLWrapper(self.neptune_sparql_url)
|
|
574
|
+
sparql.setQuery(query)
|
|
575
|
+
|
|
576
|
+
parser: ResultParser | None = None
|
|
577
|
+
|
|
578
|
+
if sparql.queryType in ["SELECT", "ASK"]:
|
|
579
|
+
parser = XMLResultParser()
|
|
580
|
+
elif sparql.queryType in ["CONSTRUCT", "DESCRIBE"]:
|
|
581
|
+
parser = RDFResultParser()
|
|
582
|
+
else:
|
|
583
|
+
raise ValueError(f"Unsupported query type: {sparql.queryType}")
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
result = parser.parse(StringIO(response.text))
|
|
587
|
+
return result
|
|
588
|
+
except Exception as e:
|
|
589
|
+
print(response.text)
|
|
590
|
+
raise e
|
|
591
|
+
|
|
592
|
+
def query_view(self, view: str, query: str) -> rdflib.query.Result:
|
|
593
|
+
"""
|
|
594
|
+
Execute a SPARQL query against a specific view.
|
|
595
|
+
|
|
596
|
+
Note:
|
|
597
|
+
This implementation currently ignores the view parameter and
|
|
598
|
+
executes the query against the entire dataset. Override this
|
|
599
|
+
method if you need view-specific querying behavior.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
view (str): View identifier (currently ignored)
|
|
603
|
+
query (str): SPARQL query string
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
rdflib.query.Result: Query results
|
|
607
|
+
|
|
608
|
+
Example:
|
|
609
|
+
>>> result = neptune.query_view("people_view", '''
|
|
610
|
+
... SELECT ?person WHERE { ?person a <http://example.org/Person> }
|
|
611
|
+
... ''')
|
|
612
|
+
"""
|
|
613
|
+
return self.query(query)
|
|
614
|
+
|
|
615
|
+
def get_subject_graph(self, subject: URIRef) -> Graph:
|
|
616
|
+
"""
|
|
617
|
+
Get all triples for a specific subject as an RDFLib Graph.
|
|
618
|
+
|
|
619
|
+
This method retrieves all triples where the given URI is the subject
|
|
620
|
+
and returns them as an RDFLib Graph object.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
subject (URIRef): The subject URI to get triples for
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Graph: RDFLib Graph containing all triples for the subject
|
|
627
|
+
|
|
628
|
+
Example:
|
|
629
|
+
>>> alice_uri = URIRef("http://example.org/alice")
|
|
630
|
+
>>> alice_graph = neptune.get_subject_graph(alice_uri)
|
|
631
|
+
>>> print(f"Alice has {len(alice_graph)} properties")
|
|
632
|
+
>>>
|
|
633
|
+
>>> # Print all properties of Alice
|
|
634
|
+
>>> for _, predicate, obj in alice_graph:
|
|
635
|
+
... print(f"Alice {predicate} {obj}")
|
|
636
|
+
"""
|
|
637
|
+
res = self.query(f"SELECT ?s ?p ?o WHERE {{ <{str(subject)}> ?p ?o }}")
|
|
638
|
+
|
|
639
|
+
graph = Graph()
|
|
640
|
+
for row in res:
|
|
641
|
+
assert isinstance(row, ResultRow)
|
|
642
|
+
assert len(row) == 3
|
|
643
|
+
_, p, o = row
|
|
644
|
+
graph.add((subject, p, o))
|
|
645
|
+
|
|
646
|
+
return graph
|
|
647
|
+
|
|
648
|
+
def graph_to_query(
|
|
649
|
+
self, graph: Graph, query_type: QueryType, graph_name: URIRef
|
|
650
|
+
) -> str:
|
|
651
|
+
"""
|
|
652
|
+
Convert an RDFLib graph to a SPARQL INSERT/DELETE statement.
|
|
653
|
+
|
|
654
|
+
This method takes an RDFLib Graph and converts it into a properly
|
|
655
|
+
formatted SPARQL statement that can be executed against Neptune.
|
|
656
|
+
It handles namespace prefixes and converts triples to N3 format.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
graph (Graph): The RDFLib graph to convert
|
|
660
|
+
query_type (QueryType): Whether to generate INSERT DATA or DELETE DATA
|
|
661
|
+
graph_name (URIRef): Named graph to target for the operation
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
str: A SPARQL INSERT/DELETE statement ready for execution
|
|
665
|
+
|
|
666
|
+
Example:
|
|
667
|
+
>>> from rdflib import Graph, URIRef, RDF, Literal
|
|
668
|
+
>>> g = Graph()
|
|
669
|
+
>>> g.add((URIRef("http://example.org/alice"),
|
|
670
|
+
... RDF.type,
|
|
671
|
+
... URIRef("http://example.org/Person")))
|
|
672
|
+
>>>
|
|
673
|
+
>>> query = neptune.graph_to_query(
|
|
674
|
+
... g,
|
|
675
|
+
... QueryType.INSERT_DATA,
|
|
676
|
+
... URIRef("http://example.org/mygraph")
|
|
677
|
+
... )
|
|
678
|
+
>>> print(query)
|
|
679
|
+
INSERT DATA { GRAPH <http://example.org/mygraph> {
|
|
680
|
+
<http://example.org/alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Person> .
|
|
681
|
+
}}
|
|
682
|
+
"""
|
|
683
|
+
# Get all namespaces from the graph
|
|
684
|
+
namespaces = []
|
|
685
|
+
for prefix, namespace in graph.namespaces():
|
|
686
|
+
if (
|
|
687
|
+
prefix in _NAMESPACE_PREFIXES_RDFLIB
|
|
688
|
+
or prefix in _NAMESPACE_PREFIXES_CORE
|
|
689
|
+
):
|
|
690
|
+
continue
|
|
691
|
+
namespaces.append(f"PREFIX {prefix}: <{namespace}>")
|
|
692
|
+
|
|
693
|
+
# Build the INSERT DATA statement
|
|
694
|
+
triples = []
|
|
695
|
+
for s, p, o in graph:
|
|
696
|
+
# Skip if any term is a blank node
|
|
697
|
+
if (
|
|
698
|
+
isinstance(s, rdflib.BNode)
|
|
699
|
+
or isinstance(p, rdflib.BNode)
|
|
700
|
+
or isinstance(o, rdflib.BNode)
|
|
701
|
+
):
|
|
702
|
+
continue
|
|
703
|
+
# Convert each term to N3 format
|
|
704
|
+
s_str = s.n3()
|
|
705
|
+
p_str = p.n3()
|
|
706
|
+
o_str = o.n3()
|
|
707
|
+
triples.append(f"{s_str} {p_str} {o_str} .")
|
|
708
|
+
|
|
709
|
+
# Combine everything into a SPARQL query
|
|
710
|
+
query = "\n".join(namespaces)
|
|
711
|
+
query += f"\n\n{query_type.value} {{ GRAPH <{str(graph_name)}> {{\n"
|
|
712
|
+
query += "\n".join(triples)
|
|
713
|
+
query += "\n}}"
|
|
714
|
+
|
|
715
|
+
return query
|
|
716
|
+
|
|
717
|
+
# Graph management
|
|
718
|
+
|
|
719
|
+
def create_graph(self, graph_name: URIRef):
|
|
720
|
+
"""
|
|
721
|
+
Create a new named graph in Neptune.
|
|
722
|
+
|
|
723
|
+
This method creates a new named graph with the specified URI. The graph
|
|
724
|
+
will be empty after creation and ready to receive triples.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
graph_name (URIRef): URI of the named graph to create
|
|
728
|
+
|
|
729
|
+
Raises:
|
|
730
|
+
AssertionError: If graph_name is None or not a URIRef
|
|
731
|
+
requests.exceptions.HTTPError: If the graph creation fails
|
|
732
|
+
|
|
733
|
+
Example:
|
|
734
|
+
>>> my_graph = URIRef("http://example.org/graphs/people")
|
|
735
|
+
>>> neptune.create_graph(my_graph)
|
|
736
|
+
>>> print("Graph created successfully")
|
|
737
|
+
"""
|
|
738
|
+
assert graph_name is not None
|
|
739
|
+
assert isinstance(graph_name, URIRef)
|
|
740
|
+
|
|
741
|
+
result = self.submit_query(
|
|
742
|
+
{QueryMode.UPDATE.value: f"CREATE GRAPH <{str(graph_name)}>"}
|
|
743
|
+
)
|
|
744
|
+
print(result.text)
|
|
745
|
+
|
|
746
|
+
def clear_graph(self, graph_name: URIRef = NEPTUNE_DEFAULT_GRAPH_NAME):
|
|
747
|
+
"""
|
|
748
|
+
Remove all triples from a named graph.
|
|
749
|
+
|
|
750
|
+
This method deletes all triples from the specified graph but keeps
|
|
751
|
+
the graph itself. The graph will be empty after this operation.
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
graph_name (URIRef, optional): URI of the named graph to clear.
|
|
755
|
+
Defaults to Neptune's default graph.
|
|
756
|
+
|
|
757
|
+
Raises:
|
|
758
|
+
AssertionError: If graph_name is None or not a URIRef
|
|
759
|
+
requests.exceptions.HTTPError: If the clear operation fails
|
|
760
|
+
|
|
761
|
+
Warning:
|
|
762
|
+
This operation cannot be undone. All data in the specified graph
|
|
763
|
+
will be permanently deleted.
|
|
764
|
+
|
|
765
|
+
Example:
|
|
766
|
+
>>> # Clear the default graph
|
|
767
|
+
>>> neptune.clear_graph()
|
|
768
|
+
>>>
|
|
769
|
+
>>> # Clear a specific named graph
|
|
770
|
+
>>> my_graph = URIRef("http://example.org/graphs/people")
|
|
771
|
+
>>> neptune.clear_graph(my_graph)
|
|
772
|
+
>>> print("Graph cleared successfully")
|
|
773
|
+
"""
|
|
774
|
+
assert graph_name is not None
|
|
775
|
+
assert isinstance(graph_name, URIRef)
|
|
776
|
+
|
|
777
|
+
self.submit_query({QueryMode.UPDATE.value: f"CLEAR GRAPH <{str(graph_name)}>"})
|
|
778
|
+
|
|
779
|
+
def drop_graph(self, graph_name: URIRef):
|
|
780
|
+
"""
|
|
781
|
+
Delete a named graph and all its triples from Neptune.
|
|
782
|
+
|
|
783
|
+
This method completely removes the specified graph and all triples
|
|
784
|
+
it contains. The graph will no longer exist after this operation.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
graph_name (URIRef): URI of the named graph to drop
|
|
788
|
+
|
|
789
|
+
Raises:
|
|
790
|
+
AssertionError: If graph_name is None or not a URIRef
|
|
791
|
+
requests.exceptions.HTTPError: If the drop operation fails
|
|
792
|
+
|
|
793
|
+
Warning:
|
|
794
|
+
This operation cannot be undone. The graph and all its data
|
|
795
|
+
will be permanently deleted.
|
|
796
|
+
|
|
797
|
+
Example:
|
|
798
|
+
>>> my_graph = URIRef("http://example.org/graphs/temporary")
|
|
799
|
+
>>> neptune.drop_graph(my_graph)
|
|
800
|
+
>>> print("Graph dropped successfully")
|
|
801
|
+
"""
|
|
802
|
+
assert graph_name is not None
|
|
803
|
+
assert isinstance(graph_name, URIRef)
|
|
804
|
+
|
|
805
|
+
self.submit_query({QueryMode.UPDATE.value: f"DROP GRAPH <{str(graph_name)}>"})
|
|
806
|
+
|
|
807
|
+
def copy_graph(self, source_graph_name: URIRef, target_graph_name: URIRef):
|
|
808
|
+
"""
|
|
809
|
+
Copy all triples from one named graph to another.
|
|
810
|
+
|
|
811
|
+
This method copies all triples from the source graph to the target graph.
|
|
812
|
+
If the target graph already exists, its contents will be replaced.
|
|
813
|
+
The source graph remains unchanged.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
source_graph_name (URIRef): URI of the source graph to copy from
|
|
817
|
+
target_graph_name (URIRef): URI of the target graph to copy to
|
|
818
|
+
|
|
819
|
+
Raises:
|
|
820
|
+
AssertionError: If either graph name is None or not a URIRef
|
|
821
|
+
requests.exceptions.HTTPError: If the copy operation fails
|
|
822
|
+
|
|
823
|
+
Warning:
|
|
824
|
+
If the target graph already exists, its contents will be completely
|
|
825
|
+
replaced by the contents of the source graph.
|
|
826
|
+
|
|
827
|
+
Example:
|
|
828
|
+
>>> source = URIRef("http://example.org/graphs/original")
|
|
829
|
+
>>> backup = URIRef("http://example.org/graphs/backup")
|
|
830
|
+
>>> neptune.copy_graph(source, backup)
|
|
831
|
+
>>> print("Graph copied successfully")
|
|
832
|
+
"""
|
|
833
|
+
assert source_graph_name is not None
|
|
834
|
+
assert isinstance(source_graph_name, URIRef)
|
|
835
|
+
assert target_graph_name is not None
|
|
836
|
+
assert isinstance(target_graph_name, URIRef)
|
|
837
|
+
|
|
838
|
+
self.submit_query(
|
|
839
|
+
{
|
|
840
|
+
QueryMode.UPDATE.value: f"COPY GRAPH <{str(source_graph_name)}> TO <{str(target_graph_name)}>"
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
def add_graph_to_graph(self, source_graph_name: URIRef, target_graph_name: URIRef):
|
|
845
|
+
"""
|
|
846
|
+
Add all triples from one named graph to another.
|
|
847
|
+
|
|
848
|
+
This method adds all triples from the source graph to the target graph.
|
|
849
|
+
Unlike copy_graph(), this operation preserves existing triples in the
|
|
850
|
+
target graph and adds new ones from the source graph.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
source_graph_name (URIRef): URI of the source graph to add from
|
|
854
|
+
target_graph_name (URIRef): URI of the target graph to add to
|
|
855
|
+
|
|
856
|
+
Raises:
|
|
857
|
+
AssertionError: If either graph name is None or not a URIRef
|
|
858
|
+
requests.exceptions.HTTPError: If the add operation fails
|
|
859
|
+
|
|
860
|
+
Note:
|
|
861
|
+
This operation merges graphs rather than replacing. Existing triples
|
|
862
|
+
in the target graph are preserved, and new triples from the source
|
|
863
|
+
graph are added.
|
|
864
|
+
|
|
865
|
+
Example:
|
|
866
|
+
>>> people_graph = URIRef("http://example.org/graphs/people")
|
|
867
|
+
>>> all_data = URIRef("http://example.org/graphs/complete")
|
|
868
|
+
>>> neptune.add_graph_to_graph(people_graph, all_data)
|
|
869
|
+
>>> print("Graph content added successfully")
|
|
870
|
+
"""
|
|
871
|
+
assert source_graph_name is not None
|
|
872
|
+
assert isinstance(source_graph_name, URIRef)
|
|
873
|
+
assert target_graph_name is not None
|
|
874
|
+
assert isinstance(target_graph_name, URIRef)
|
|
875
|
+
|
|
876
|
+
self.submit_query(
|
|
877
|
+
{
|
|
878
|
+
QueryMode.UPDATE.value: f"ADD GRAPH <{str(source_graph_name)}> TO <{str(target_graph_name)}>"
|
|
879
|
+
}
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class AWSNeptuneSSHTunnel(AWSNeptune):
|
|
884
|
+
"""
|
|
885
|
+
AWS Neptune Triple Store Adapter with SSH Tunnel Support
|
|
886
|
+
|
|
887
|
+
This adapter extends AWSNeptune to provide secure access to Neptune instances
|
|
888
|
+
deployed in private VPCs through SSH tunneling via a bastion host. It's ideal
|
|
889
|
+
for production environments where Neptune is not directly accessible from
|
|
890
|
+
the internet.
|
|
891
|
+
|
|
892
|
+
The adapter establishes an SSH tunnel from your local machine through a bastion
|
|
893
|
+
host to the Neptune instance, then routes all SPARQL requests through this tunnel.
|
|
894
|
+
This provides secure access while maintaining all the functionality of the base
|
|
895
|
+
AWSNeptune adapter.
|
|
896
|
+
|
|
897
|
+
Features:
|
|
898
|
+
- All AWSNeptune functionality via SSH tunnel
|
|
899
|
+
- Secure access to VPC-deployed Neptune instances
|
|
900
|
+
- SSH key-based authentication to bastion host
|
|
901
|
+
- Automatic port forwarding and connection management
|
|
902
|
+
- Socket address monkey-patching for transparent operation
|
|
903
|
+
|
|
904
|
+
Architecture:
|
|
905
|
+
Your Application → SSH Tunnel → Bastion Host → VPC → Neptune Instance
|
|
906
|
+
|
|
907
|
+
Attributes:
|
|
908
|
+
bastion_host (str): Hostname or IP of the bastion host
|
|
909
|
+
bastion_port (int): SSH port on the bastion host (typically 22)
|
|
910
|
+
bastion_user (str): SSH username for bastion host connection
|
|
911
|
+
bastion_private_key (str): SSH private key content for authentication
|
|
912
|
+
tunnel (SSHTunnelForwarder): Active SSH tunnel instance
|
|
913
|
+
|
|
914
|
+
Example:
|
|
915
|
+
>>> # SSH tunnel connection to VPC-deployed Neptune
|
|
916
|
+
>>> neptune_ssh = AWSNeptuneSSHTunnel(
|
|
917
|
+
... aws_region_name="us-east-1",
|
|
918
|
+
... aws_access_key_id="AKIA1234567890EXAMPLE",
|
|
919
|
+
... aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
920
|
+
... db_instance_identifier="my-vpc-neptune-cluster",
|
|
921
|
+
... bastion_host="bastion.mycompany.com",
|
|
922
|
+
... bastion_port=22,
|
|
923
|
+
... bastion_user="ubuntu",
|
|
924
|
+
... bastion_private_key='''-----BEGIN RSA PRIVATE KEY-----
|
|
925
|
+
... MIIEpAIBAAKCAQEA2...
|
|
926
|
+
... ...private key content...
|
|
927
|
+
... -----END RSA PRIVATE KEY-----'''
|
|
928
|
+
... )
|
|
929
|
+
>>>
|
|
930
|
+
>>> # Use exactly like regular AWSNeptune - tunnel is transparent
|
|
931
|
+
>>> from rdflib import Graph, URIRef, RDF, Literal
|
|
932
|
+
>>> g = Graph()
|
|
933
|
+
>>> g.add((URIRef("http://example.org/alice"),
|
|
934
|
+
... RDF.type,
|
|
935
|
+
... URIRef("http://example.org/Person")))
|
|
936
|
+
>>> neptune_ssh.insert(g)
|
|
937
|
+
>>>
|
|
938
|
+
>>> # Query through the tunnel
|
|
939
|
+
>>> result = neptune_ssh.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
940
|
+
>>> print(f"Found {len(list(result))} triples")
|
|
941
|
+
|
|
942
|
+
Security Notes:
|
|
943
|
+
- Always use SSH key authentication instead of passwords
|
|
944
|
+
- Ensure your bastion host is properly secured and monitored
|
|
945
|
+
- Consider using temporary SSH keys for enhanced security
|
|
946
|
+
- The private key is temporarily written to disk during tunnel creation
|
|
947
|
+
"""
|
|
948
|
+
|
|
949
|
+
def __init__(
|
|
950
|
+
self,
|
|
951
|
+
aws_region_name: str,
|
|
952
|
+
aws_access_key_id: str,
|
|
953
|
+
aws_secret_access_key: str,
|
|
954
|
+
db_instance_identifier: str,
|
|
955
|
+
bastion_host: str,
|
|
956
|
+
bastion_port: int | str,
|
|
957
|
+
bastion_user: str,
|
|
958
|
+
bastion_private_key: str,
|
|
959
|
+
default_graph_name: URIRef = NEPTUNE_DEFAULT_GRAPH_NAME,
|
|
960
|
+
):
|
|
961
|
+
"""
|
|
962
|
+
Initialize AWS Neptune adapter with SSH tunnel support.
|
|
963
|
+
|
|
964
|
+
This constructor first initializes the base AWSNeptune adapter to discover
|
|
965
|
+
the Neptune endpoint, then establishes an SSH tunnel through the specified
|
|
966
|
+
bastion host to enable secure access to VPC-deployed Neptune instances.
|
|
967
|
+
|
|
968
|
+
Args:
|
|
969
|
+
aws_region_name (str): AWS region name (e.g., 'us-east-1')
|
|
970
|
+
aws_access_key_id (str): AWS access key ID for Neptune authentication
|
|
971
|
+
aws_secret_access_key (str): AWS secret key for Neptune authentication
|
|
972
|
+
db_instance_identifier (str): Neptune database instance identifier
|
|
973
|
+
bastion_host (str): Hostname or IP address of the bastion host
|
|
974
|
+
bastion_port (int | str): SSH port on the bastion host (typically 22)
|
|
975
|
+
bastion_user (str): SSH username for bastion host authentication
|
|
976
|
+
bastion_private_key (str): Complete SSH private key content as a string
|
|
977
|
+
default_graph_name (URIRef, optional): Default named graph URI
|
|
978
|
+
|
|
979
|
+
Raises:
|
|
980
|
+
AssertionError: If any parameter has incorrect type
|
|
981
|
+
paramiko.AuthenticationException: If SSH authentication fails
|
|
982
|
+
paramiko.SSHException: If SSH connection fails
|
|
983
|
+
socket.gaierror: If bastion host cannot be resolved
|
|
984
|
+
TimeoutError: If SSH connection times out
|
|
985
|
+
|
|
986
|
+
Example:
|
|
987
|
+
>>> # Load private key from file
|
|
988
|
+
>>> with open('/path/to/ssh/key.pem', 'r') as f:
|
|
989
|
+
... private_key = f.read()
|
|
990
|
+
>>>
|
|
991
|
+
>>> neptune_ssh = AWSNeptuneSSHTunnel(
|
|
992
|
+
... aws_region_name="us-east-1",
|
|
993
|
+
... aws_access_key_id="AKIA1234567890EXAMPLE",
|
|
994
|
+
... aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
995
|
+
... db_instance_identifier="my-vpc-neptune",
|
|
996
|
+
... bastion_host="bastion.mycompany.com",
|
|
997
|
+
... bastion_port=22,
|
|
998
|
+
... bastion_user="ec2-user",
|
|
999
|
+
... bastion_private_key=private_key
|
|
1000
|
+
... )
|
|
1001
|
+
"""
|
|
1002
|
+
super().__init__(
|
|
1003
|
+
aws_region_name=aws_region_name,
|
|
1004
|
+
aws_access_key_id=aws_access_key_id,
|
|
1005
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
1006
|
+
db_instance_identifier=db_instance_identifier,
|
|
1007
|
+
default_graph_name=default_graph_name,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
assert isinstance(bastion_host, str)
|
|
1011
|
+
if not isinstance(bastion_port, int):
|
|
1012
|
+
# This block is added because the "naas" secret adapter does not always manage int values:
|
|
1013
|
+
# it may provide 'bastion_port' as a string; convert it to int if needed.
|
|
1014
|
+
if isinstance(bastion_port, str):
|
|
1015
|
+
try:
|
|
1016
|
+
bastion_port = int(bastion_port)
|
|
1017
|
+
except ValueError:
|
|
1018
|
+
raise ValueError(
|
|
1019
|
+
f"bastion_port must be an int or a string convertible to int, got string: {bastion_port!r}"
|
|
1020
|
+
)
|
|
1021
|
+
assert isinstance(bastion_port, int)
|
|
1022
|
+
assert isinstance(bastion_user, str)
|
|
1023
|
+
assert isinstance(bastion_private_key, str)
|
|
1024
|
+
|
|
1025
|
+
# Import SSH dependencies when actually needed
|
|
1026
|
+
try:
|
|
1027
|
+
import paramiko
|
|
1028
|
+
except ImportError as e:
|
|
1029
|
+
raise ImportError(
|
|
1030
|
+
"SSH tunnel support requires optional dependencies. "
|
|
1031
|
+
"Install them with: pip install 'abi[ssh]' or install paramiko and sshtunnel separately"
|
|
1032
|
+
) from e
|
|
1033
|
+
|
|
1034
|
+
self.bastion_host = bastion_host
|
|
1035
|
+
self.bastion_port = bastion_port
|
|
1036
|
+
self.bastion_user = bastion_user
|
|
1037
|
+
self.bastion_private_key = bastion_private_key
|
|
1038
|
+
|
|
1039
|
+
self.bastion_client = paramiko.SSHClient()
|
|
1040
|
+
self.bastion_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
1041
|
+
|
|
1042
|
+
# self.bastion_client.connect(self.bastion_host, port=self.bastion_port, username=self.bastion_user, pkey=paramiko.RSAKey.from_private_key(StringIO(self.bastion_private_key)))
|
|
1043
|
+
self.tunnel = self.__create_ssh_tunnel()
|
|
1044
|
+
|
|
1045
|
+
# We patch the neptune_sparql_url to use the tunnel.
|
|
1046
|
+
self.neptune_sparql_url = f"https://{self.neptune_sparql_endpoint}:{self.tunnel.local_bind_port}/sparql"
|
|
1047
|
+
|
|
1048
|
+
def __monkey_patch_getaddrinfo(self):
|
|
1049
|
+
"""
|
|
1050
|
+
Monkey patch socket.getaddrinfo to redirect Neptune endpoint to localhost.
|
|
1051
|
+
|
|
1052
|
+
This method modifies the global socket.getaddrinfo function to intercept
|
|
1053
|
+
DNS resolution requests for the Neptune endpoint and redirect them to
|
|
1054
|
+
localhost (127.0.0.1). This is necessary because:
|
|
1055
|
+
|
|
1056
|
+
1. The SSH tunnel creates a local port that forwards to Neptune
|
|
1057
|
+
2. HTTPS requests need to connect to localhost to use the tunnel
|
|
1058
|
+
3. The original Neptune endpoint hostname needs to be preserved for SSL
|
|
1059
|
+
|
|
1060
|
+
The patching ensures that when the HTTP client tries to connect to the
|
|
1061
|
+
Neptune endpoint, it actually connects to the local tunnel port instead.
|
|
1062
|
+
|
|
1063
|
+
Note:
|
|
1064
|
+
This is an internal method that modifies global socket behavior.
|
|
1065
|
+
It should only be called once during tunnel setup.
|
|
1066
|
+
|
|
1067
|
+
Warning:
|
|
1068
|
+
This method modifies global socket behavior and could affect other
|
|
1069
|
+
network operations in the same process. Use with caution in multi-
|
|
1070
|
+
threaded or complex applications.
|
|
1071
|
+
"""
|
|
1072
|
+
assert self.neptune_sparql_endpoint is not None
|
|
1073
|
+
|
|
1074
|
+
def new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
|
1075
|
+
if host == self.neptune_sparql_endpoint:
|
|
1076
|
+
return ORIGINAL_GETADDRINFO(
|
|
1077
|
+
"127.0.0.1", port, family, type, proto, flags
|
|
1078
|
+
)
|
|
1079
|
+
else:
|
|
1080
|
+
return ORIGINAL_GETADDRINFO(host, port, family, type, proto, flags)
|
|
1081
|
+
|
|
1082
|
+
socket.getaddrinfo = new_getaddrinfo
|
|
1083
|
+
|
|
1084
|
+
def __create_ssh_tunnel(self):
|
|
1085
|
+
"""
|
|
1086
|
+
Create and start an SSH tunnel to the Neptune database.
|
|
1087
|
+
|
|
1088
|
+
This method establishes an SSH tunnel from a local port through the
|
|
1089
|
+
bastion host to the Neptune database endpoint. The tunnel enables
|
|
1090
|
+
secure access to VPC-deployed Neptune instances that are not directly
|
|
1091
|
+
accessible from the internet.
|
|
1092
|
+
|
|
1093
|
+
The process involves:
|
|
1094
|
+
1. Writing the SSH private key to a temporary file
|
|
1095
|
+
2. Creating an SSHTunnelForwarder instance
|
|
1096
|
+
3. Connecting to the bastion host using SSH key authentication
|
|
1097
|
+
4. Forwarding traffic from a local port to Neptune's port
|
|
1098
|
+
5. Starting the tunnel and applying network patches
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
SSHTunnelForwarder: An active SSH tunnel instance that forwards
|
|
1102
|
+
traffic from localhost to Neptune through the bastion host
|
|
1103
|
+
|
|
1104
|
+
Raises:
|
|
1105
|
+
paramiko.AuthenticationException: If SSH key authentication fails
|
|
1106
|
+
paramiko.SSHException: If SSH connection to bastion fails
|
|
1107
|
+
socket.gaierror: If bastion host DNS resolution fails
|
|
1108
|
+
OSError: If temporary file creation fails
|
|
1109
|
+
|
|
1110
|
+
Note:
|
|
1111
|
+
The SSH private key is temporarily written to disk during tunnel
|
|
1112
|
+
creation for security reasons (paramiko requirement). The temporary
|
|
1113
|
+
file is automatically cleaned up.
|
|
1114
|
+
|
|
1115
|
+
Example:
|
|
1116
|
+
>>> # This method is called automatically during __init__
|
|
1117
|
+
>>> # Manual usage (not recommended):
|
|
1118
|
+
>>> tunnel = neptune_ssh._AWSNeptuneSSHTunnel__create_ssh_tunnel()
|
|
1119
|
+
>>> print(f"Tunnel running on local port: {tunnel.local_bind_port}")
|
|
1120
|
+
"""
|
|
1121
|
+
from sshtunnel import SSHTunnelForwarder
|
|
1122
|
+
|
|
1123
|
+
assert self.neptune_sparql_endpoint is not None
|
|
1124
|
+
assert self.neptune_port is not None
|
|
1125
|
+
|
|
1126
|
+
# Write pkey to tmpfile
|
|
1127
|
+
with tempfile.NamedTemporaryFile(delete=True) as tmpfile:
|
|
1128
|
+
tmpfile.write(self.bastion_private_key.encode("utf-8"))
|
|
1129
|
+
tmpfile.flush()
|
|
1130
|
+
tmpfile.name
|
|
1131
|
+
|
|
1132
|
+
tunnel = SSHTunnelForwarder(
|
|
1133
|
+
(self.bastion_host, self.bastion_port),
|
|
1134
|
+
ssh_username=self.bastion_user,
|
|
1135
|
+
ssh_pkey=tmpfile.name,
|
|
1136
|
+
remote_bind_address=(
|
|
1137
|
+
socket.gethostbyname(self.neptune_sparql_endpoint),
|
|
1138
|
+
self.neptune_port,
|
|
1139
|
+
),
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
tunnel.start()
|
|
1143
|
+
|
|
1144
|
+
self.__monkey_patch_getaddrinfo()
|
|
1145
|
+
|
|
1146
|
+
return tunnel
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
if __name__ == "__main__":
|
|
1150
|
+
"""
|
|
1151
|
+
Example usage and interactive testing for AWS Neptune adapters.
|
|
1152
|
+
|
|
1153
|
+
This section provides examples of how to use both AWSNeptune and AWSNeptuneSSHTunnel
|
|
1154
|
+
adapters, and includes an interactive SPARQL console for testing.
|
|
1155
|
+
|
|
1156
|
+
Environment Variables Required:
|
|
1157
|
+
AWS_REGION: AWS region (e.g., 'us-east-1')
|
|
1158
|
+
AWS_ACCESS_KEY_ID: AWS access key for authentication
|
|
1159
|
+
AWS_SECRET_ACCESS_KEY: AWS secret access key
|
|
1160
|
+
AWS_NEPTUNE_DB_CLUSTER_IDENTIFIER: Neptune database instance identifier
|
|
1161
|
+
|
|
1162
|
+
For SSH Tunnel (AWSNeptuneSSHTunnel):
|
|
1163
|
+
AWS_BASTION_HOST: Bastion host hostname or IP
|
|
1164
|
+
AWS_BASTION_PORT: SSH port (typically 22)
|
|
1165
|
+
AWS_BASTION_USER: SSH username
|
|
1166
|
+
AWS_BASTION_PRIVATE_KEY: SSH private key content
|
|
1167
|
+
|
|
1168
|
+
Usage Examples:
|
|
1169
|
+
# Direct connection (for publicly accessible Neptune)
|
|
1170
|
+
python AWSNeptune.py
|
|
1171
|
+
|
|
1172
|
+
# SSH tunnel connection (for VPC-deployed Neptune)
|
|
1173
|
+
# Ensure all environment variables are set first
|
|
1174
|
+
python AWSNeptune.py
|
|
1175
|
+
"""
|
|
1176
|
+
import os
|
|
1177
|
+
|
|
1178
|
+
from dotenv import load_dotenv
|
|
1179
|
+
|
|
1180
|
+
load_dotenv()
|
|
1181
|
+
|
|
1182
|
+
try:
|
|
1183
|
+
# Try SSH tunnel connection if bastion host is configured
|
|
1184
|
+
neptune: AWSNeptune
|
|
1185
|
+
if os.getenv("AWS_BASTION_HOST"):
|
|
1186
|
+
print("Initializing Neptune adapter with SSH tunnel...")
|
|
1187
|
+
neptune = AWSNeptuneSSHTunnel(
|
|
1188
|
+
aws_region_name=os.environ["AWS_REGION"],
|
|
1189
|
+
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
|
|
1190
|
+
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
|
|
1191
|
+
db_instance_identifier=os.environ["AWS_NEPTUNE_DB_CLUSTER_IDENTIFIER"],
|
|
1192
|
+
bastion_host=os.environ["AWS_BASTION_HOST"],
|
|
1193
|
+
bastion_port=int(os.environ["AWS_BASTION_PORT"]),
|
|
1194
|
+
bastion_user=os.environ["AWS_BASTION_USER"],
|
|
1195
|
+
bastion_private_key=os.environ["AWS_BASTION_PRIVATE_KEY"],
|
|
1196
|
+
)
|
|
1197
|
+
print("✓ SSH tunnel established successfully")
|
|
1198
|
+
else:
|
|
1199
|
+
print("Initializing Neptune adapter with direct connection...")
|
|
1200
|
+
neptune = AWSNeptune(
|
|
1201
|
+
aws_region_name=os.environ["AWS_REGION"],
|
|
1202
|
+
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
|
|
1203
|
+
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
|
|
1204
|
+
db_instance_identifier=os.environ["AWS_NEPTUNE_DB_CLUSTER_IDENTIFIER"],
|
|
1205
|
+
)
|
|
1206
|
+
print("✓ Direct connection established successfully")
|
|
1207
|
+
|
|
1208
|
+
print(f"Connected to Neptune endpoint: {neptune.neptune_sparql_endpoint}")
|
|
1209
|
+
print("\nAvailable commands:")
|
|
1210
|
+
print(" SELECT/ASK/CONSTRUCT queries - executed as QUERY")
|
|
1211
|
+
print(" INSERT/DELETE/UPDATE statements - executed as UPDATE")
|
|
1212
|
+
print(" create graph <uri> - create a new named graph")
|
|
1213
|
+
print(" clear graph <uri> - clear all triples from a graph")
|
|
1214
|
+
print(" list graphs - show all named graphs")
|
|
1215
|
+
print(" exit - quit the console")
|
|
1216
|
+
print("\nEntering interactive SPARQL console...")
|
|
1217
|
+
print("=" * 50)
|
|
1218
|
+
|
|
1219
|
+
while True:
|
|
1220
|
+
query = input("SPARQL> ").strip()
|
|
1221
|
+
|
|
1222
|
+
if query.lower() == "exit":
|
|
1223
|
+
print("Goodbye!")
|
|
1224
|
+
break
|
|
1225
|
+
elif query.startswith("create graph "):
|
|
1226
|
+
try:
|
|
1227
|
+
graph_uri = query.split(" ", 2)[2]
|
|
1228
|
+
graph_name = URIRef(graph_uri)
|
|
1229
|
+
neptune.create_graph(graph_name)
|
|
1230
|
+
print(f"✓ Graph {graph_uri} created successfully")
|
|
1231
|
+
except Exception as e:
|
|
1232
|
+
print(f"✗ Error creating graph: {e}")
|
|
1233
|
+
elif query.startswith("clear graph "):
|
|
1234
|
+
try:
|
|
1235
|
+
graph_uri = query.split(" ", 2)[2]
|
|
1236
|
+
graph_name = URIRef(graph_uri)
|
|
1237
|
+
neptune.clear_graph(graph_name)
|
|
1238
|
+
print(f"✓ Graph {graph_uri} cleared successfully")
|
|
1239
|
+
except Exception as e:
|
|
1240
|
+
print(f"✗ Error clearing graph: {e}")
|
|
1241
|
+
elif query.lower().startswith("list graphs"):
|
|
1242
|
+
try:
|
|
1243
|
+
result = neptune.query(
|
|
1244
|
+
"SELECT DISTINCT ?g WHERE { GRAPH ?g { ?s ?p ?o } }"
|
|
1245
|
+
)
|
|
1246
|
+
graphs = []
|
|
1247
|
+
for row in result:
|
|
1248
|
+
if isinstance(row, ResultRow) and hasattr(row, "g"):
|
|
1249
|
+
graphs.append(str(row.g))
|
|
1250
|
+
if graphs:
|
|
1251
|
+
print("Named graphs:")
|
|
1252
|
+
for graph in graphs:
|
|
1253
|
+
print(f" - {graph}")
|
|
1254
|
+
else:
|
|
1255
|
+
print("No named graphs found")
|
|
1256
|
+
except Exception as e:
|
|
1257
|
+
print(f"✗ Error listing graphs: {e}")
|
|
1258
|
+
elif query:
|
|
1259
|
+
try:
|
|
1260
|
+
# Determine query mode based on query type
|
|
1261
|
+
query_mode = QueryMode.QUERY
|
|
1262
|
+
if any(
|
|
1263
|
+
query.upper().strip().startswith(cmd)
|
|
1264
|
+
for cmd in [
|
|
1265
|
+
"INSERT",
|
|
1266
|
+
"DELETE",
|
|
1267
|
+
"CREATE",
|
|
1268
|
+
"DROP",
|
|
1269
|
+
"CLEAR",
|
|
1270
|
+
"COPY",
|
|
1271
|
+
"ADD",
|
|
1272
|
+
]
|
|
1273
|
+
):
|
|
1274
|
+
query_mode = QueryMode.UPDATE
|
|
1275
|
+
|
|
1276
|
+
result = neptune.query(query, query_mode)
|
|
1277
|
+
|
|
1278
|
+
if query_mode == QueryMode.UPDATE:
|
|
1279
|
+
print("✓ Update executed successfully")
|
|
1280
|
+
else:
|
|
1281
|
+
# Format and display query results
|
|
1282
|
+
result_list = list(result)
|
|
1283
|
+
if result_list:
|
|
1284
|
+
print(f"Results ({len(result_list)} rows):")
|
|
1285
|
+
for i, row in enumerate(result_list):
|
|
1286
|
+
if i >= 10: # Limit display to first 10 rows
|
|
1287
|
+
print(f"... and {len(result_list) - 10} more rows")
|
|
1288
|
+
break
|
|
1289
|
+
print(f" {row}")
|
|
1290
|
+
else:
|
|
1291
|
+
print("No results returned")
|
|
1292
|
+
except Exception as e:
|
|
1293
|
+
print(f"✗ Query error: {e}")
|
|
1294
|
+
|
|
1295
|
+
except KeyError as e:
|
|
1296
|
+
print(f"✗ Missing required environment variable: {e}")
|
|
1297
|
+
print("Please set all required environment variables and try again.")
|
|
1298
|
+
except Exception as e:
|
|
1299
|
+
print(f"✗ Connection error: {e}")
|
|
1300
|
+
print("Please check your AWS credentials and Neptune configuration.")
|