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

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