naas-abi-core 1.0.0__py3-none-any.whl

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